ltcai 3.0.1 → 3.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 (123) hide show
  1. package/README.md +54 -21
  2. package/docs/CHANGELOG.md +90 -0
  3. package/docs/V3_2_AUDIT.md +82 -0
  4. package/docs/V3_FRONTEND.md +20 -17
  5. package/docs/architecture.md +6 -0
  6. package/latticeai/__init__.py +1 -1
  7. package/latticeai/api/agent_registry.py +103 -0
  8. package/latticeai/api/auth.py +4 -1
  9. package/latticeai/api/hooks.py +113 -0
  10. package/latticeai/api/marketplace.py +13 -0
  11. package/latticeai/api/memory.py +109 -0
  12. package/latticeai/api/search.py +4 -0
  13. package/latticeai/core/agent_registry.py +234 -0
  14. package/latticeai/core/config.py +2 -0
  15. package/latticeai/core/embedding_providers.py +123 -0
  16. package/latticeai/core/hooks.py +284 -0
  17. package/latticeai/core/marketplace.py +87 -2
  18. package/latticeai/core/multi_agent.py +1 -1
  19. package/latticeai/core/workspace_os.py +1 -1
  20. package/latticeai/server_app.py +63 -6
  21. package/latticeai/services/memory_service.py +324 -0
  22. package/package.json +9 -4
  23. package/scripts/build_v3_assets.mjs +164 -0
  24. package/scripts/capture/README.md +28 -0
  25. package/scripts/capture/capture_enterprise.js +8 -0
  26. package/scripts/capture/capture_graph.js +8 -0
  27. package/scripts/capture/capture_onboarding.js +8 -0
  28. package/scripts/capture/capture_page.js +43 -0
  29. package/scripts/capture/capture_release_media.js +125 -0
  30. package/scripts/capture/capture_skills.js +8 -0
  31. package/scripts/capture/capture_workspace.js +8 -0
  32. package/scripts/generate_diagrams.py +513 -0
  33. package/scripts/lint_v3.mjs +33 -0
  34. package/scripts/release-0.3.1.sh +105 -0
  35. package/scripts/take_screenshots.js +69 -0
  36. package/scripts/validate_release_artifacts.py +167 -0
  37. package/static/account.html +9 -9
  38. package/static/activity.html +4 -4
  39. package/static/admin.html +8 -8
  40. package/static/agents.html +4 -4
  41. package/static/chat.html +9 -9
  42. package/static/css/tokens.5a595671.css +260 -0
  43. package/static/css/tokens.css +1 -1
  44. package/static/graph.html +9 -9
  45. package/static/plugins.html +4 -4
  46. package/static/sw.js +3 -1
  47. package/static/v3/asset-manifest.json +55 -0
  48. package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
  49. package/static/v3/css/lattice.components.011e988b.css +447 -0
  50. package/static/v3/css/lattice.components.css +2 -2
  51. package/static/v3/css/lattice.shell.4920f42d.css +407 -0
  52. package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
  53. package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
  54. package/static/v3/index.html +38 -9
  55. package/static/v3/js/app.a5adc0f3.js +26 -0
  56. package/static/v3/js/core/api.603b978f.js +408 -0
  57. package/static/v3/js/core/api.js +132 -51
  58. package/static/v3/js/core/components.4c83e0a9.js +222 -0
  59. package/static/v3/js/core/components.js +9 -2
  60. package/static/v3/js/core/dom.a2773eb0.js +148 -0
  61. package/static/v3/js/core/router.584570f2.js +37 -0
  62. package/static/v3/js/core/routes.07ad6696.js +89 -0
  63. package/static/v3/js/core/routes.js +17 -1
  64. package/static/v3/js/core/shell.ea0b9ae5.js +363 -0
  65. package/static/v3/js/core/store.34ebd5e6.js +113 -0
  66. package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
  67. package/static/v3/js/views/admin-audit.js +1 -1
  68. package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
  69. package/static/v3/js/views/admin-permissions.js +4 -5
  70. package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
  71. package/static/v3/js/views/admin-policies.js +4 -5
  72. package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
  73. package/static/v3/js/views/admin-private-vpc.js +2 -5
  74. package/static/v3/js/views/admin-security.07c66b72.js +180 -0
  75. package/static/v3/js/views/admin-security.js +4 -5
  76. package/static/v3/js/views/admin-users.03bac88c.js +168 -0
  77. package/static/v3/js/views/admin-users.js +6 -6
  78. package/static/v3/js/views/agents.c373d48c.js +293 -0
  79. package/static/v3/js/views/agents.js +101 -2
  80. package/static/v3/js/views/chat.718144ce.js +449 -0
  81. package/static/v3/js/views/chat.js +2 -3
  82. package/static/v3/js/views/files.4935197e.js +186 -0
  83. package/static/v3/js/views/files.js +27 -21
  84. package/static/v3/js/views/home.cdde3b32.js +119 -0
  85. package/static/v3/js/views/hooks.f3edebca.js +99 -0
  86. package/static/v3/js/views/hooks.js +99 -0
  87. package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
  88. package/static/v3/js/views/hybrid-search.js +1 -1
  89. package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
  90. package/static/v3/js/views/knowledge-graph.js +2 -3
  91. package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
  92. package/static/v3/js/views/marketplace.js +141 -0
  93. package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
  94. package/static/v3/js/views/mcp.js +114 -0
  95. package/static/v3/js/views/memory.d2ed7a7c.js +146 -0
  96. package/static/v3/js/views/memory.js +146 -0
  97. package/static/v3/js/views/models.a1ffa147.js +256 -0
  98. package/static/v3/js/views/models.js +17 -8
  99. package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
  100. package/static/v3/js/views/my-computer.js +5 -5
  101. package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
  102. package/static/v3/js/views/pipeline.js +3 -7
  103. package/static/v3/js/views/planning.9ac3e313.js +153 -0
  104. package/static/v3/js/views/planning.js +153 -0
  105. package/static/v3/js/views/settings.4f777210.js +250 -0
  106. package/static/v3/js/views/settings.js +6 -14
  107. package/static/v3/js/views/skills.c6c2f965.js +109 -0
  108. package/static/v3/js/views/skills.js +109 -0
  109. package/static/v3/js/views/tools.e4f11276.js +108 -0
  110. package/static/v3/js/views/tools.js +108 -0
  111. package/static/v3/js/views/workflows.26c57290.js +128 -0
  112. package/static/v3/js/views/workflows.js +128 -0
  113. package/static/workflows.html +4 -4
  114. package/static/workspace.html +5 -5
  115. package/docs/images/tmp_frames/frame_00.png +0 -0
  116. package/docs/images/tmp_frames/frame_01.png +0 -0
  117. package/docs/images/tmp_frames/frame_02.png +0 -0
  118. package/docs/images/tmp_frames/frame_03.png +0 -0
  119. package/docs/images/tmp_frames/hero_00.png +0 -0
  120. package/docs/images/tmp_frames/hero_01.png +0 -0
  121. package/docs/images/tmp_frames/hero_02.png +0 -0
  122. package/docs/images/tmp_frames/hero_03.png +0 -0
  123. package/static/v3/js/core/fixtures.js +0 -171
@@ -0,0 +1,284 @@
1
+ """Hooks platform — a persisted registry of lifecycle extension points.
2
+
3
+ Lattice AI runs several behaviours at well-defined points in the agent / tool /
4
+ workflow lifecycle (audit logging, secret redaction, sensitive-data
5
+ classification, tool-permission gating, memory snapshots, workflow replay
6
+ logging). v3.2.0 makes those points *first-class and inspectable*: every hook is
7
+ listed, ordered, and individually enable/disable-able, and users can register
8
+ their own custom hooks alongside the built-ins.
9
+
10
+ The registry owns metadata, ordering and the enabled flag (persisted to
11
+ ``data_dir/hooks.json``). Built-in hooks carry ``source="builtin"`` and map onto
12
+ behaviour the platform already performs; ``managed`` records whether the
13
+ behaviour is enforced by the platform (``platform``) or is an advisory hook the
14
+ user has registered (``user``). The registry never silently drops a hook: the
15
+ full set is always returned so the UI can show exactly what runs and in which
16
+ order.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import tempfile
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+ from typing import Any, Dict, List, Optional
27
+
28
+
29
+ HOOK_KINDS = (
30
+ "pre_run",
31
+ "post_run",
32
+ "pre_tool",
33
+ "post_tool",
34
+ "agent",
35
+ "pipeline",
36
+ "workflow",
37
+ )
38
+
39
+
40
+ def _now() -> str:
41
+ return datetime.now().isoformat(timespec="seconds")
42
+
43
+
44
+ # Built-in hooks describe lifecycle points the platform already exercises. They
45
+ # are honest reflections of existing behaviour (see the `binding` field), made
46
+ # visible and orderable here. Disabling a `managed="platform"` hook is recorded
47
+ # and surfaced, but core safety behaviours remain enforced by their owning
48
+ # subsystem — the UI states this explicitly so nothing is misrepresented.
49
+ BUILTIN_HOOKS: List[Dict[str, Any]] = [
50
+ {
51
+ "id": "builtin:redact-secrets",
52
+ "name": "Redact secrets",
53
+ "kind": "pre_run",
54
+ "order": 10,
55
+ "description": "Strip secret-like fields (token, password, api_key…) from agent context packets before a run.",
56
+ "binding": "latticeai.core.multi_agent._redact",
57
+ "managed": "platform",
58
+ },
59
+ {
60
+ "id": "builtin:research-memory-snapshot",
61
+ "name": "Research memory snapshot",
62
+ "kind": "agent",
63
+ "order": 20,
64
+ "description": "Capture a short-term memory snapshot after the researcher stage gathers context.",
65
+ "binding": "latticeai.core.multi_agent.default_role_runner",
66
+ "managed": "platform",
67
+ },
68
+ {
69
+ "id": "builtin:tool-permission-gate",
70
+ "name": "Tool permission gate",
71
+ "kind": "pre_tool",
72
+ "order": 10,
73
+ "description": "Require explicit approval for tools whose governance policy is not auto-approve.",
74
+ "binding": "latticeai.core.tool_registry.ToolRegistry.permission",
75
+ "managed": "platform",
76
+ },
77
+ {
78
+ "id": "builtin:sensitive-data-guard",
79
+ "name": "Sensitive-data guard",
80
+ "kind": "pre_tool",
81
+ "order": 20,
82
+ "description": "Classify outgoing content for sensitive data before tool execution.",
83
+ "binding": "server_app.classify_sensitive_message",
84
+ "managed": "platform",
85
+ },
86
+ {
87
+ "id": "builtin:audit-agent-run",
88
+ "name": "Audit agent run",
89
+ "kind": "post_run",
90
+ "order": 10,
91
+ "description": "Append every completed agent run to the workspace audit log.",
92
+ "binding": "latticeai.services.agent_runtime.AgentRuntime.start",
93
+ "managed": "platform",
94
+ },
95
+ {
96
+ "id": "builtin:workflow-replay-log",
97
+ "name": "Workflow replay log",
98
+ "kind": "workflow",
99
+ "order": 10,
100
+ "description": "Record each workflow run's timeline so it can be replayed step by step.",
101
+ "binding": "latticeai.api.workflow_designer",
102
+ "managed": "platform",
103
+ },
104
+ {
105
+ "id": "builtin:pipeline-index-status",
106
+ "name": "Pipeline index status",
107
+ "kind": "pipeline",
108
+ "order": 10,
109
+ "description": "Publish ingest / embed / graph-build pipeline state to the retrieval index status.",
110
+ "binding": "latticeai.api.search",
111
+ "managed": "platform",
112
+ },
113
+ ]
114
+
115
+
116
+ class HooksRegistry:
117
+ """Persisted registry of lifecycle hooks (built-in + user-registered)."""
118
+
119
+ def __init__(self, path: Path):
120
+ self.path = Path(path)
121
+ self._state: Dict[str, Any] = self._load()
122
+
123
+ # ── persistence ───────────────────────────────────────────────────────
124
+ def _load(self) -> Dict[str, Any]:
125
+ if self.path.exists():
126
+ try:
127
+ with open(self.path, "r", encoding="utf-8") as fh:
128
+ data = json.load(fh)
129
+ if isinstance(data, dict):
130
+ data.setdefault("custom", [])
131
+ data.setdefault("overrides", {})
132
+ return data
133
+ except Exception:
134
+ pass
135
+ return {"custom": [], "overrides": {}}
136
+
137
+ def _save(self) -> None:
138
+ self.path.parent.mkdir(parents=True, exist_ok=True)
139
+ fd, tmp = tempfile.mkstemp(dir=str(self.path.parent), suffix=".tmp")
140
+ try:
141
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
142
+ json.dump(self._state, fh, ensure_ascii=False, indent=2)
143
+ os.replace(tmp, self.path)
144
+ finally:
145
+ if os.path.exists(tmp):
146
+ os.unlink(tmp)
147
+
148
+ # ── views ─────────────────────────────────────────────────────────────
149
+ def _materialize(self) -> List[Dict[str, Any]]:
150
+ overrides: Dict[str, Any] = self._state.get("overrides", {})
151
+ hooks: List[Dict[str, Any]] = []
152
+ for base in BUILTIN_HOOKS:
153
+ ov = overrides.get(base["id"], {})
154
+ hook = dict(base)
155
+ hook["source"] = "builtin"
156
+ hook["enabled"] = bool(ov.get("enabled", True))
157
+ if "order" in ov:
158
+ hook["order"] = ov["order"]
159
+ hook["removable"] = False
160
+ hooks.append(hook)
161
+ for custom in self._state.get("custom", []):
162
+ hook = dict(custom)
163
+ hook["source"] = "user"
164
+ hook.setdefault("managed", "user")
165
+ hook.setdefault("binding", "advisory")
166
+ hook["enabled"] = bool(custom.get("enabled", True))
167
+ hook["removable"] = True
168
+ hooks.append(hook)
169
+ hooks.sort(key=lambda h: (HOOK_KINDS.index(h["kind"]) if h["kind"] in HOOK_KINDS else 99, h.get("order", 100), h["id"]))
170
+ return hooks
171
+
172
+ def list(self, kind: Optional[str] = None) -> Dict[str, Any]:
173
+ hooks = self._materialize()
174
+ if kind:
175
+ hooks = [h for h in hooks if h["kind"] == kind]
176
+ counts: Dict[str, Dict[str, int]] = {}
177
+ for h in self._materialize():
178
+ bucket = counts.setdefault(h["kind"], {"total": 0, "enabled": 0})
179
+ bucket["total"] += 1
180
+ if h["enabled"]:
181
+ bucket["enabled"] += 1
182
+ return {
183
+ "hooks": hooks,
184
+ "kinds": list(HOOK_KINDS),
185
+ "counts": counts,
186
+ "total": len(hooks),
187
+ "enabled": sum(1 for h in hooks if h["enabled"]),
188
+ "generated_at": _now(),
189
+ }
190
+
191
+ def get(self, hook_id: str) -> Optional[Dict[str, Any]]:
192
+ return next((h for h in self._materialize() if h["id"] == hook_id), None)
193
+
194
+ def inspect(self, hook_id: str) -> Dict[str, Any]:
195
+ hook = self.get(hook_id)
196
+ if hook is None:
197
+ raise KeyError(hook_id)
198
+ detail = dict(hook)
199
+ detail["advisory"] = hook.get("managed") != "platform"
200
+ detail["note"] = (
201
+ "Enforced by its owning subsystem; the registry controls visibility and ordering."
202
+ if hook.get("managed") == "platform"
203
+ else "User-registered hook: listed, ordered and inspectable; runs advisory in this build."
204
+ )
205
+ return detail
206
+
207
+ # ── mutations ─────────────────────────────────────────────────────────
208
+ def set_enabled(self, hook_id: str, enabled: bool) -> Dict[str, Any]:
209
+ if self.get(hook_id) is None:
210
+ raise KeyError(hook_id)
211
+ if hook_id.startswith("builtin:"):
212
+ self._state.setdefault("overrides", {}).setdefault(hook_id, {})["enabled"] = bool(enabled)
213
+ else:
214
+ for custom in self._state.get("custom", []):
215
+ if custom["id"] == hook_id:
216
+ custom["enabled"] = bool(enabled)
217
+ self._save()
218
+ return self.get(hook_id) # type: ignore[return-value]
219
+
220
+ def set_order(self, hook_id: str, order: int) -> Dict[str, Any]:
221
+ if self.get(hook_id) is None:
222
+ raise KeyError(hook_id)
223
+ order = int(order)
224
+ if hook_id.startswith("builtin:"):
225
+ self._state.setdefault("overrides", {}).setdefault(hook_id, {})["order"] = order
226
+ else:
227
+ for custom in self._state.get("custom", []):
228
+ if custom["id"] == hook_id:
229
+ custom["order"] = order
230
+ self._save()
231
+ return self.get(hook_id) # type: ignore[return-value]
232
+
233
+ def reorder(self, kind: str, ordered_ids: List[str]) -> Dict[str, Any]:
234
+ for idx, hook_id in enumerate(ordered_ids):
235
+ try:
236
+ self.set_order(hook_id, (idx + 1) * 10)
237
+ except KeyError:
238
+ continue
239
+ return self.list(kind=kind)
240
+
241
+ def register(
242
+ self,
243
+ *,
244
+ name: str,
245
+ kind: str,
246
+ description: str = "",
247
+ command: str = "",
248
+ order: Optional[int] = None,
249
+ enabled: bool = True,
250
+ ) -> Dict[str, Any]:
251
+ if not str(name).strip():
252
+ raise ValueError("name is required")
253
+ if kind not in HOOK_KINDS:
254
+ raise ValueError(f"kind must be one of {', '.join(HOOK_KINDS)}")
255
+ slug = str(name).strip().lower().replace(" ", "-")
256
+ hook_id = f"user:{slug}"
257
+ existing = {c["id"] for c in self._state.get("custom", [])}
258
+ if hook_id in existing:
259
+ hook_id = f"user:{slug}-{len(existing) + 1}"
260
+ entry = {
261
+ "id": hook_id,
262
+ "name": str(name).strip(),
263
+ "kind": kind,
264
+ "description": str(description or "").strip(),
265
+ "command": str(command or "").strip(),
266
+ "order": int(order) if order is not None else 100,
267
+ "enabled": bool(enabled),
268
+ "managed": "user",
269
+ "binding": "advisory",
270
+ "created_at": _now(),
271
+ }
272
+ self._state.setdefault("custom", []).append(entry)
273
+ self._save()
274
+ return entry
275
+
276
+ def remove(self, hook_id: str) -> Dict[str, Any]:
277
+ if hook_id.startswith("builtin:"):
278
+ raise ValueError("Built-in hooks cannot be removed; disable them instead.")
279
+ before = len(self._state.get("custom", []))
280
+ self._state["custom"] = [c for c in self._state.get("custom", []) if c["id"] != hook_id]
281
+ if len(self._state["custom"]) == before:
282
+ raise KeyError(hook_id)
283
+ self._save()
284
+ return {"removed": hook_id}
@@ -11,10 +11,39 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "2.2.0"
14
+ MARKETPLACE_VERSION = "3.2.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
18
+ def _agent_template(
19
+ template_id: str,
20
+ name: str,
21
+ description: str,
22
+ *,
23
+ roles: List[str],
24
+ capabilities: List[str],
25
+ suggested_tools: List[str],
26
+ category: str,
27
+ max_retries: int = 2,
28
+ ) -> Dict[str, Any]:
29
+ """Build a portable agent template entry (Part 4 reusable templates)."""
30
+ return {
31
+ "id": template_id,
32
+ "kind": "agent",
33
+ "name": name,
34
+ "version": "1.0.0",
35
+ "description": description,
36
+ "metadata": {"category": category, "installable": True, "agent_template": True},
37
+ "definition": {
38
+ "roles": roles,
39
+ "max_retries": max_retries,
40
+ "capabilities": capabilities,
41
+ "suggested_tools": suggested_tools,
42
+ "constraints": ["workspace scoped", "no secret leakage", "replayable timeline"],
43
+ },
44
+ }
45
+
46
+
18
47
  class MarketplaceError(Exception):
19
48
  """Raised for invalid template operations."""
20
49
 
@@ -73,7 +102,47 @@ BUILTIN_TEMPLATES: Dict[str, List[Dict[str, Any]]] = {
73
102
  "max_retries": 2,
74
103
  "constraints": ["workspace scoped", "no secret leakage", "replayable timeline"],
75
104
  },
76
- }
105
+ },
106
+ _agent_template(
107
+ "agent-research-assistant", "Research Assistant",
108
+ "Retrieves workspace context, plans an inquiry, and synthesizes a reviewed answer.",
109
+ roles=["researcher", "planner", "reviewer"],
110
+ capabilities=["context-retrieval", "hybrid-search", "memory-recall", "synthesis"],
111
+ suggested_tools=["knowledge_search", "knowledge_graph_search", "read_file"],
112
+ category="research",
113
+ ),
114
+ _agent_template(
115
+ "agent-coding-assistant", "Coding Assistant",
116
+ "Plans a change, edits files, runs the build, and reviews the result before finishing.",
117
+ roles=["planner", "executor", "reviewer"],
118
+ capabilities=["task-decomposition", "tool-use", "file-write", "verification"],
119
+ suggested_tools=["edit_file", "write_file", "run_command", "build_project", "git_diff"],
120
+ category="coding",
121
+ ),
122
+ _agent_template(
123
+ "agent-knowledge-curator", "Knowledge Curator",
124
+ "Captures, structures, and saves knowledge into the graph and memory.",
125
+ roles=["researcher", "executor"],
126
+ capabilities=["context-retrieval", "graph-read", "knowledge-save"],
127
+ suggested_tools=["knowledge_save", "knowledge_graph_ingest", "knowledge_tree"],
128
+ category="knowledge",
129
+ ),
130
+ _agent_template(
131
+ "agent-documentation-writer", "Documentation Writer",
132
+ "Plans a document, drafts and writes it, and reviews for completeness.",
133
+ roles=["planner", "executor", "reviewer"],
134
+ capabilities=["task-decomposition", "file-write", "summarize", "verification"],
135
+ suggested_tools=["create_docx", "create_pdf", "write_file", "read_document"],
136
+ category="documentation",
137
+ ),
138
+ _agent_template(
139
+ "agent-workflow-builder", "Workflow Builder",
140
+ "Plans and assembles a multi-step workflow definition for repeatable automation.",
141
+ roles=["planner", "executor"],
142
+ capabilities=["task-decomposition", "workflow-run", "delegation"],
143
+ suggested_tools=["todo_write", "workspace_tree"],
144
+ category="automation",
145
+ ),
77
146
  ],
78
147
  }
79
148
 
@@ -110,6 +179,22 @@ class TemplateCatalog:
110
179
  return deepcopy(template)
111
180
  raise MarketplaceError(f"template not found: {item_kind}/{template_id}")
112
181
 
182
+ def clone_template(self, kind: str, template_id: str, new_name: Optional[str] = None) -> Dict[str, Any]:
183
+ """Return an editable copy of a template with a fresh id (Part 4 clone)."""
184
+ template = self.get_template(kind, template_id)
185
+ name = (new_name or f"{template['name']} (Copy)").strip()
186
+ slug = name.lower().replace(" ", "-").replace("(", "").replace(")", "")
187
+ clone = deepcopy(template)
188
+ clone["id"] = f"{template['id']}-copy-{slug}"[:80]
189
+ clone["name"] = name
190
+ clone["version"] = "1.0.0"
191
+ clone["metadata"] = {
192
+ **(template.get("metadata") or {}),
193
+ "cloned_from": template_id,
194
+ "editable": True,
195
+ }
196
+ return clone
197
+
113
198
  def export_template(self, kind: str, template_id: str) -> Dict[str, Any]:
114
199
  template = self.get_template(kind, template_id)
115
200
  return {
@@ -14,7 +14,7 @@ from datetime import datetime
14
14
  from typing import Any, Callable, Dict, List, Optional
15
15
 
16
16
 
17
- MULTI_AGENT_VERSION = "2.2.0"
17
+ MULTI_AGENT_VERSION = "3.2.0"
18
18
 
19
19
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
20
20
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -18,7 +18,7 @@ from pathlib import Path
18
18
  from typing import Any, Callable, Dict, Iterable, List, Optional
19
19
 
20
20
 
21
- WORKSPACE_OS_VERSION = "3.0.1"
21
+ WORKSPACE_OS_VERSION = "3.2.0"
22
22
 
23
23
  # Workspace types separate single-user Personal workspaces from shared
24
24
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -73,7 +73,7 @@ from latticeai.services.workspace_service import WorkspaceService
73
73
  from latticeai.services.model_service import ModelService
74
74
  from latticeai.services.chat_service import ChatService
75
75
  from latticeai.services.search_service import SearchService
76
- from latticeai.core.embedding_providers import resolve_embedder
76
+ from latticeai.core.embedding_providers import resolve_embedder, resolve_embedding_profile
77
77
  from latticeai.services.agent_runtime import AgentRuntime
78
78
  from latticeai.services.model_runtime import (
79
79
  CLOUD_VERIFY_TTL_SECONDS,
@@ -113,6 +113,12 @@ from latticeai.api.tools import create_tools_router
113
113
  from latticeai.api.static_routes import create_static_routes_router
114
114
  from latticeai.api.garden import create_garden_router
115
115
  from latticeai.api.setup import create_setup_router
116
+ from latticeai.api.hooks import create_hooks_router
117
+ from latticeai.core.hooks import HooksRegistry
118
+ from latticeai.api.agent_registry import create_agent_registry_router
119
+ from latticeai.core.agent_registry import AgentRegistry
120
+ from latticeai.api.memory import create_memory_router
121
+ from latticeai.services.memory_service import MemoryService
116
122
  from latticeai.services.tool_dispatch import (
117
123
  LOCAL_WRITE_BLOCKED_PREFIXES as _LOCAL_WRITE_BLOCKED_PREFIXES,
118
124
  TOOL_GOVERNANCE,
@@ -122,6 +128,7 @@ from latticeai.services.tool_dispatch import (
122
128
  configure_tool_dispatch,
123
129
  get_tool_permission,
124
130
  list_tool_permissions,
131
+ tool_response as _tool_response,
125
132
  )
126
133
  from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF
127
134
  from mcp_registry import (
@@ -251,15 +258,26 @@ SSO_FILE = DATA_DIR / "sso_config.json"
251
258
  # Resolve the configured embedding provider once at startup. Degrades to the
252
259
  # offline hash fallback when the requested provider is unavailable, while
253
260
  # recording the requested-vs-active provider for the Embeddings status surface.
261
+ try:
262
+ EMBEDDING_PROFILE = resolve_embedding_profile(CONFIG.embedding_profile)
263
+ except ValueError as exc:
264
+ logging.warning("Embedding profile ignored: %s", exc)
265
+ EMBEDDING_PROFILE = {}
266
+ _embedding_provider = CONFIG.embedding_provider
267
+ _embedding_model = CONFIG.embedding_model or str(EMBEDDING_PROFILE.get("model") or "")
268
+ _embedding_dim = CONFIG.embedding_dim or int(EMBEDDING_PROFILE.get("dimensions") or 0)
269
+ if CONFIG.embedding_profile and CONFIG.embedding_provider in {"", "hash", "local", "fallback"}:
270
+ _embedding_provider = str(EMBEDDING_PROFILE.get("provider") or CONFIG.embedding_provider)
271
+
254
272
  EMBEDDER = resolve_embedder(
255
- CONFIG.embedding_provider,
256
- model=CONFIG.embedding_model,
273
+ _embedding_provider,
274
+ model=_embedding_model,
257
275
  base_url=CONFIG.embedding_base_url,
258
276
  api_key=CONFIG.embedding_api_key,
259
- dim=CONFIG.embedding_dim,
277
+ dim=_embedding_dim,
260
278
  timeout=CONFIG.embedding_timeout,
261
279
  extra={"target": CONFIG.embedding_custom_target},
262
- probe=CONFIG.embedding_provider not in {"", "hash", "local", "fallback"},
280
+ probe=_embedding_provider not in {"", "hash", "local", "fallback"},
263
281
  )
264
282
  if EMBEDDER.fell_back:
265
283
  logging.warning("Embedding provider %s unavailable: %s", EMBEDDER.requested, EMBEDDER.detail)
@@ -280,6 +298,19 @@ WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS)
280
298
  PLUGINS_DIR = Path(os.getenv("LATTICEAI_PLUGINS_DIR") or (BASE_DIR / "plugins"))
281
299
  PLUGIN_REGISTRY = PluginRegistry(PLUGINS_DIR, store=WORKSPACE_OS)
282
300
  TEMPLATE_CATALOG = TemplateCatalog()
301
+ # ── v3.2 platform registries: lifecycle hooks + agent registry, persisted under
302
+ # DATA_DIR so the /app Hooks and Agent Registry views read/write real state.
303
+ HOOKS_REGISTRY = HooksRegistry(DATA_DIR / "hooks.json")
304
+ AGENT_REGISTRY = AgentRegistry(DATA_DIR / "agent_registry.json")
305
+ # Unified long-term memory platform fronting workspace memories, agent
306
+ # snapshots, conversation history, and the KG graph/vector index.
307
+ MEMORY_SERVICE = MemoryService(
308
+ store=WORKSPACE_OS,
309
+ data_dir=DATA_DIR,
310
+ knowledge_graph=KNOWLEDGE_GRAPH,
311
+ enable_graph=ENABLE_GRAPH,
312
+ history_file=HISTORY_FILE,
313
+ )
283
314
 
284
315
  def _require_graph():
285
316
  if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
@@ -805,6 +836,8 @@ def _require_local_approval(
805
836
 
806
837
  def require_admin(request: Request) -> tuple[str, Dict]:
807
838
  users = load_users()
839
+ if not REQUIRE_AUTH:
840
+ return "", users
808
841
  token = _extract_bearer_token(request)
809
842
  if token:
810
843
  email = get_session_email(token)
@@ -1100,6 +1133,7 @@ app.include_router(create_auth_router(
1100
1133
  get_sso_settings=get_sso_settings, get_sso_discovery=_get_sso_discovery,
1101
1134
  public_sso_config=public_sso_config,
1102
1135
  open_registration=OPEN_REGISTRATION, session_ttl=_SESSION_TTL,
1136
+ require_auth=REQUIRE_AUTH,
1103
1137
  ))
1104
1138
 
1105
1139
  def _graph_stats_safe():
@@ -1387,9 +1421,11 @@ app.include_router(create_chat_router(
1387
1421
  ))
1388
1422
 
1389
1423
  def _embedding_info() -> dict:
1390
- from latticeai.core.embedding_providers import PROVIDER_TYPES
1424
+ from latticeai.core.embedding_providers import PROVIDER_TYPES, embedding_provider_profiles
1391
1425
  info = EMBEDDER.as_dict()
1392
1426
  info["available_providers"] = list(PROVIDER_TYPES)
1427
+ info["profile"] = CONFIG.embedding_profile or ""
1428
+ info["profiles"] = embedding_provider_profiles()
1393
1429
  return info
1394
1430
 
1395
1431
 
@@ -1423,6 +1459,27 @@ app.include_router(create_tools_router(
1423
1459
  mcp_public_item=mcp_public_item,
1424
1460
  ))
1425
1461
 
1462
+ app.include_router(create_hooks_router(
1463
+ registry=HOOKS_REGISTRY,
1464
+ require_user=require_user,
1465
+ append_audit_event=append_audit_event,
1466
+ ))
1467
+
1468
+ app.include_router(create_agent_registry_router(
1469
+ registry=AGENT_REGISTRY,
1470
+ require_user=require_user,
1471
+ append_audit_event=append_audit_event,
1472
+ ))
1473
+
1474
+ app.include_router(create_memory_router(
1475
+ service=MEMORY_SERVICE,
1476
+ require_user=require_user,
1477
+ get_current_user=get_current_user,
1478
+ gate_read=PLATFORM.gate_read,
1479
+ gate_write=PLATFORM.gate_write,
1480
+ append_audit_event=append_audit_event,
1481
+ ))
1482
+
1426
1483
  app.include_router(create_garden_router(gardener=gardener, require_user=require_user))
1427
1484
  app.include_router(create_setup_router(model_router=router, require_user=require_user))
1428
1485