ltcai 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +40 -19
  2. package/docs/CHANGELOG.md +107 -0
  3. package/docs/EDITION_STRATEGY.md +14 -4
  4. package/docs/ENTERPRISE.md +11 -3
  5. package/docs/MULTI_AGENT_RUNTIME.md +410 -0
  6. package/docs/PLUGIN_SDK.md +651 -0
  7. package/docs/REALTIME_COLLABORATION.md +410 -0
  8. package/docs/V2_ARCHITECTURE.md +528 -0
  9. package/docs/WORKFLOW_DESIGNER.md +475 -0
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/api/agents.py +98 -0
  12. package/latticeai/api/plugins.py +115 -0
  13. package/latticeai/api/realtime.py +91 -0
  14. package/latticeai/api/workflow_designer.py +207 -0
  15. package/latticeai/core/multi_agent.py +270 -0
  16. package/latticeai/core/plugins.py +400 -0
  17. package/latticeai/core/realtime.py +190 -0
  18. package/latticeai/core/workflow_engine.py +329 -0
  19. package/latticeai/core/workspace_os.py +165 -2
  20. package/latticeai/server_app.py +76 -2
  21. package/latticeai/services/platform_runtime.py +200 -0
  22. package/package.json +17 -2
  23. package/plugins/README.md +35 -0
  24. package/plugins/git-insights/plugin.json +15 -0
  25. package/plugins/hello-world/plugin.json +16 -0
  26. package/plugins/hello-world/skills/hello_skill/SKILL.md +15 -0
  27. package/static/activity.html +70 -0
  28. package/static/admin.html +62 -0
  29. package/static/agents.html +92 -0
  30. package/static/graph.html +7 -1
  31. package/static/lattice-reference.css +184 -0
  32. package/static/platform.css +75 -0
  33. package/static/plugins.html +82 -0
  34. package/static/scripts/admin.js +121 -1
  35. package/static/scripts/graph.js +296 -14
  36. package/static/scripts/platform.js +64 -0
  37. package/static/scripts/workspace.js +107 -10
  38. package/static/workflows.html +121 -0
  39. package/static/workspace.css +73 -0
  40. package/static/workspace.html +18 -2
@@ -91,8 +91,16 @@ from latticeai.services.model_runtime import (
91
91
  verify_cloud_models,
92
92
  ensure_ollama_server,
93
93
  )
94
- from latticeai.api.workspace import create_workspace_router
94
+ from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
95
95
  from latticeai.api.health import create_health_router
96
+ # ── v2.0 Agentic Workspace Platform layers ───────────────────────────────────
97
+ from latticeai.core.plugins import PluginRegistry
98
+ from latticeai.core.realtime import RealtimeBus
99
+ from latticeai.services.platform_runtime import PlatformRuntime
100
+ from latticeai.api.plugins import create_plugins_router
101
+ from latticeai.api.workflow_designer import create_workflow_designer_router
102
+ from latticeai.api.agents import create_agents_router
103
+ from latticeai.api.realtime import create_realtime_router
96
104
  from latticeai.api.models import create_models_router
97
105
  from latticeai.api.chat import create_chat_router
98
106
  from latticeai.api.tools import create_tools_router
@@ -236,10 +244,16 @@ AUDIT_FILE = DATA_DIR / "audit_log.json"
236
244
  SSO_FILE = DATA_DIR / "sso_config.json"
237
245
  KNOWLEDGE_GRAPH = KnowledgeGraphStore(DATA_DIR / "knowledge_graph.sqlite", DATA_DIR / "knowledge_graph_blobs") if ENABLE_GRAPH else None
238
246
  LOCAL_KG_WATCHER = LocalKnowledgeWatcher(lambda: KNOWLEDGE_GRAPH) if ENABLE_GRAPH else None
239
- WORKSPACE_OS = WorkspaceOSStore(DATA_DIR)
247
+ # ── v2.0 Realtime bus: constructed first so the store can fan every timeline
248
+ # event into the realtime feed via a single additive sink (no per-call wiring).
249
+ REALTIME_BUS = RealtimeBus()
250
+ WORKSPACE_OS = WorkspaceOSStore(DATA_DIR, event_sink=REALTIME_BUS)
240
251
  # Service layer (latticeai.services) wraps the store with scope/permission
241
252
  # guardrails; routers and the app assembly share this single instance.
242
253
  WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS)
254
+ # ── v2.0 Plugin SDK registry (extends skills; discovers plugins/<id>/plugin.json)
255
+ PLUGINS_DIR = Path(os.getenv("LATTICEAI_PLUGINS_DIR") or (BASE_DIR / "plugins"))
256
+ PLUGIN_REGISTRY = PluginRegistry(PLUGINS_DIR, store=WORKSPACE_OS)
243
257
 
244
258
  def _require_graph():
245
259
  if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
@@ -1187,6 +1201,66 @@ app.include_router(create_workspace_router(
1187
1201
  ))
1188
1202
 
1189
1203
 
1204
+ # ── v2.0 Agentic Workspace Platform: cross-system wiring ─────────────────────
1205
+ # All cross-subsystem closures live in latticeai.services.platform_runtime to
1206
+ # keep this assembly file lean; server_app only constructs it and mounts routers.
1207
+ PLATFORM = PlatformRuntime(
1208
+ store=WORKSPACE_OS,
1209
+ workspace_service=WORKSPACE_SERVICE,
1210
+ plugin_registry=PLUGIN_REGISTRY,
1211
+ get_current_user=get_current_user,
1212
+ workspace_graph=_workspace_graph,
1213
+ workspace_scope_from_request=_workspace_scope_from_request,
1214
+ get_tool_permission=get_tool_permission,
1215
+ )
1216
+
1217
+ app.include_router(create_plugins_router(
1218
+ registry=PLUGIN_REGISTRY,
1219
+ require_user=require_user,
1220
+ require_admin=require_admin,
1221
+ append_audit_event=append_audit_event,
1222
+ register_skill=PLATFORM.register_plugin_skill,
1223
+ plugin_runners_factory=lambda: PLATFORM.plugin_capability_runners(None, None),
1224
+ ui_file_response=ui_file_response,
1225
+ static_dir=STATIC_DIR,
1226
+ ))
1227
+
1228
+ app.include_router(create_workflow_designer_router(
1229
+ store=WORKSPACE_OS,
1230
+ require_user=require_user,
1231
+ get_current_user=get_current_user,
1232
+ gate_read=PLATFORM.gate_read,
1233
+ gate_write=PLATFORM.gate_write,
1234
+ workspace_graph=_workspace_graph,
1235
+ build_runners=PLATFORM.build_workflow_runners,
1236
+ append_audit_event=append_audit_event,
1237
+ ui_file_response=ui_file_response,
1238
+ static_dir=STATIC_DIR,
1239
+ ))
1240
+
1241
+ app.include_router(create_agents_router(
1242
+ store=WORKSPACE_OS,
1243
+ orchestrator_factory=PLATFORM.build_orchestrator,
1244
+ require_user=require_user,
1245
+ get_current_user=get_current_user,
1246
+ gate_read=PLATFORM.gate_read,
1247
+ gate_write=PLATFORM.gate_write,
1248
+ workspace_graph=_workspace_graph,
1249
+ append_audit_event=append_audit_event,
1250
+ ui_file_response=ui_file_response,
1251
+ static_dir=STATIC_DIR,
1252
+ ))
1253
+
1254
+ app.include_router(create_realtime_router(
1255
+ bus=REALTIME_BUS,
1256
+ require_user=require_user,
1257
+ get_current_user=get_current_user,
1258
+ allowed_scopes=PLATFORM.allowed_scopes,
1259
+ ui_file_response=ui_file_response,
1260
+ static_dir=STATIC_DIR,
1261
+ ))
1262
+
1263
+
1190
1264
  # ── Health & Info ──────────────────────────────────────────────────────────────
1191
1265
 
1192
1266
  # ── Model runtime/provider helpers moved to latticeai.services.model_runtime ──
@@ -0,0 +1,200 @@
1
+ """v2.0 Agentic Workspace Platform runtime — cross-system wiring.
2
+
3
+ This is the single place the four v2.0 subsystems (Plugin SDK, Workflow
4
+ Designer, Multi-Agent Runtime, Realtime) connect to one another and to the
5
+ workspace. Keeping it out of ``server_app`` honours the AGENTS.md preference for
6
+ small, composable modules and keeps the wiring independently testable.
7
+
8
+ Recursion is bounded by construction: a workflow's ``agent`` node runs an
9
+ orchestrator *without* a workflow runner, and an orchestrator's workflow runner
10
+ runs an engine *without* an ``agent`` runner — so the deepest chains are
11
+ ``agent → workflow → (tool|skill|plugin)`` and ``workflow → agent → plugin``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Callable, Dict, Optional, Set
17
+
18
+ from fastapi import HTTPException, Request
19
+
20
+ from latticeai.core.multi_agent import MultiAgentOrchestrator, default_role_runner
21
+ from latticeai.core.workflow_engine import WorkflowEngine
22
+
23
+
24
+ class PlatformRuntime:
25
+ def __init__(
26
+ self,
27
+ *,
28
+ store,
29
+ workspace_service,
30
+ plugin_registry,
31
+ get_current_user: Callable[[Request], Optional[str]],
32
+ workspace_graph: Callable[[], Any],
33
+ workspace_scope_from_request: Callable[[Request], Optional[str]],
34
+ get_tool_permission: Callable[..., Dict[str, Any]],
35
+ ):
36
+ self.store = store
37
+ self.svc = workspace_service
38
+ self.registry = plugin_registry
39
+ self.get_current_user = get_current_user
40
+ self.workspace_graph = workspace_graph
41
+ self.scope_from_request = workspace_scope_from_request
42
+ self.get_tool_permission = get_tool_permission
43
+
44
+ # ── request gating ────────────────────────────────────────────────────
45
+
46
+ def gate_read(self, request: Request) -> Optional[str]:
47
+ try:
48
+ return self.svc.resolve_read_scope(self.scope_from_request(request), self.get_current_user(request))
49
+ except PermissionError as exc:
50
+ raise HTTPException(status_code=403, detail=str(exc)) from exc
51
+
52
+ def gate_write(self, request: Request) -> Optional[str]:
53
+ try:
54
+ return self.svc.resolve_write_scope(self.scope_from_request(request), self.get_current_user(request))
55
+ except PermissionError as exc:
56
+ raise HTTPException(status_code=403, detail=str(exc)) from exc
57
+
58
+ def allowed_scopes(self, user: Optional[str]) -> Optional[Set[str]]:
59
+ try:
60
+ workspaces = self.svc.list_workspaces(user or None).get("workspaces", [])
61
+ return {ws.get("workspace_id") for ws in workspaces if ws.get("workspace_id")}
62
+ except Exception:
63
+ return None
64
+
65
+ # ── plugin lifecycle hooks ────────────────────────────────────────────
66
+
67
+ def register_plugin_skill(self, skill_name: str, plugin_id: str):
68
+ return self.store.mark_skill_installed(
69
+ skill_name, version=f"plugin:{plugin_id}", metadata={"source": f"plugin:{plugin_id}"}
70
+ )
71
+
72
+ # ── shared node runners ───────────────────────────────────────────────
73
+
74
+ def _tool_node_runner(self):
75
+ """Workflow tool node: records the invocation + governance decision but
76
+ never silently executes exec/destructive tools (those need approval)."""
77
+ def runner(*, node, context):
78
+ cfg = node.get("config") or {}
79
+ name = cfg.get("tool") or ""
80
+ try:
81
+ permission = dict(self.get_tool_permission(name))
82
+ except Exception:
83
+ permission = {"tool": name, "risk": "unknown"}
84
+ return {"tool": name, "args": cfg.get("args") or {}, "recorded": True, "permission": permission}
85
+ return runner
86
+
87
+ def _skill_node_runner(self):
88
+ def runner(*, node, context):
89
+ cfg = node.get("config") or {}
90
+ name = cfg.get("skill") or ""
91
+ entry = self.store.load_state().get("skill_registry", {}).get(name) or {}
92
+ return {"skill": name, "found": bool(entry), "enabled": bool(entry.get("enabled"))}
93
+ return runner
94
+
95
+ def _context_provider(self, user, scope):
96
+ def provider(goal: str):
97
+ try:
98
+ mems = self.store.search_memories(goal, user_email=user, workspace_id=scope).get("memories", [])
99
+ return [str(m.get("content") or "")[:160] for m in mems[:5]]
100
+ except Exception:
101
+ return []
102
+ return provider
103
+
104
+ def plugin_capability_runners(self, user, scope) -> Dict[str, Callable[..., Any]]:
105
+ """Runners the Plugin SDK boundary dispatches to (one per capability)."""
106
+ def run_skill(*, plugin_id, action, args, manifest):
107
+ return {"plugin": plugin_id, "ran_skills": manifest.provides.get("skills", [])}
108
+
109
+ def run_tool(*, plugin_id, action, args, manifest):
110
+ tool = args.get("tool") or (manifest.provides.get("tools") or [None])[0]
111
+ try:
112
+ permission = dict(self.get_tool_permission(tool)) if tool else {}
113
+ except Exception:
114
+ permission = {}
115
+ return {"plugin": plugin_id, "tool": tool, "permission": permission, "recorded": True}
116
+
117
+ def run_workflow(*, plugin_id, action, args, manifest):
118
+ wf_id = args.get("workflow_id")
119
+ if not wf_id:
120
+ return {"plugin": plugin_id, "skipped": "no workflow_id"}
121
+ return self.run_workflow_by_id(wf_id, user, scope, with_agent=False, inputs=args.get("inputs"))
122
+
123
+ def run_agent(*, plugin_id, action, args, manifest):
124
+ goal = args.get("goal") or f"Plugin {plugin_id} agent task"
125
+ return self.run_agent(goal, user, scope, with_workflow=False, inputs=args.get("inputs"))
126
+
127
+ return {"skills": run_skill, "tools": run_tool, "workflows": run_workflow, "agents": run_agent}
128
+
129
+ def _plugin_node_runner(self, user, scope):
130
+ def runner(*, node, context):
131
+ cfg = node.get("config") or {}
132
+ plugin_id = cfg.get("plugin_id") or cfg.get("plugin") or ""
133
+ action = cfg.get("action") or "run_skill"
134
+ result = self.registry.execute_action(
135
+ plugin_id, action, cfg.get("args") or {}, runners=self.plugin_capability_runners(user, scope)
136
+ )
137
+ return result.as_dict()
138
+ return runner
139
+
140
+ def _agent_node_runner(self, user, scope):
141
+ def runner(*, node, context):
142
+ cfg = node.get("config") or {}
143
+ goal = cfg.get("goal") or context.get("goal") or "Run agent"
144
+ return self.run_agent(goal, user, scope, with_workflow=False, roles=cfg.get("roles"), inputs=context.get("inputs"))
145
+ return runner
146
+
147
+ # ── cross-system runs ─────────────────────────────────────────────────
148
+
149
+ def run_workflow_by_id(self, workflow_id, user, scope, *, with_agent: bool, inputs=None) -> Dict[str, Any]:
150
+ try:
151
+ workflow = self.store.get_workflow(workflow_id, workspace_id=scope)
152
+ except FileNotFoundError:
153
+ return {"error": f"workflow not found: {workflow_id}"}
154
+ runners = {
155
+ "tool": self._tool_node_runner(),
156
+ "skill": self._skill_node_runner(),
157
+ "plugin": self._plugin_node_runner(user, scope),
158
+ }
159
+ if with_agent:
160
+ runners["agent"] = self._agent_node_runner(user, scope)
161
+ result = WorkflowEngine(runners).run(workflow, inputs=inputs or {})
162
+ run = self.store.record_workflow_run(
163
+ workflow_id=workflow_id, name=workflow.get("name") or "workflow",
164
+ status=result.status, timeline=result.timeline, outputs=result.outputs,
165
+ user_email=user, graph=self.workspace_graph(), workspace_id=scope,
166
+ )
167
+ return {"workflow_run_id": run["id"], "status": result.status}
168
+
169
+ def run_agent(self, goal, user, scope, *, with_workflow: bool, roles=None, inputs=None) -> Dict[str, Any]:
170
+ role_runner = default_role_runner(
171
+ workflow_runner=(lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs)) if with_workflow else None,
172
+ plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope)).as_dict(),
173
+ context_provider=self._context_provider(user, scope),
174
+ )
175
+ result = MultiAgentOrchestrator(role_runner=role_runner).run(
176
+ goal, user_email=user, workspace_id=scope, roles=roles, inputs=inputs or {}
177
+ )
178
+ run = self.store.record_agent_run(
179
+ agent_id=result.agent_id, status=result.status, input_text=goal,
180
+ output_text=result.output, timeline=result.timeline, relationships=[],
181
+ user_email=user, graph=self.workspace_graph(), workspace_id=scope,
182
+ )
183
+ return {"agent_run_id": run["id"], "status": result.status, "output": result.output}
184
+
185
+ # ── factories passed to routers ───────────────────────────────────────
186
+
187
+ def build_workflow_runners(self, user, scope) -> Dict[str, Callable[..., Any]]:
188
+ return {
189
+ "tool": self._tool_node_runner(),
190
+ "skill": self._skill_node_runner(),
191
+ "plugin": self._plugin_node_runner(user, scope),
192
+ "agent": self._agent_node_runner(user, scope),
193
+ }
194
+
195
+ def build_orchestrator(self, user, scope) -> MultiAgentOrchestrator:
196
+ return MultiAgentOrchestrator(role_runner=default_role_runner(
197
+ workflow_runner=lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs),
198
+ plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope)).as_dict(),
199
+ context_provider=self._context_provider(user, scope),
200
+ ))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "1.6.0",
3
+ "version": "2.0.0",
4
4
  "description": "Lattice AI Workspace OS for local-first graph, memory, agent, workflow, and skill operations",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -19,10 +19,16 @@
19
19
  "dev": "python3 ltcai_cli.py --reload",
20
20
  "build": "npm run build:python",
21
21
  "build:python": "python3 -m build",
22
- "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
22
+ "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
23
23
  "test": "python3 -m pytest tests/ -v",
24
24
  "test:unit": "python3 -m pytest tests/unit/ -v",
25
25
  "test:integration": "python3 -m pytest tests/integration/ -v",
26
+ "test:visual": "playwright test",
27
+ "capture:workspace": "node scripts/capture/capture_workspace.js",
28
+ "capture:graph": "node scripts/capture/capture_graph.js",
29
+ "capture:skills": "node scripts/capture/capture_skills.js",
30
+ "capture:enterprise": "node scripts/capture/capture_enterprise.js",
31
+ "capture:onboarding": "node scripts/capture/capture_onboarding.js",
26
32
  "publish:npm": "npm publish --access public",
27
33
  "publish:pypi": "python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl"
28
34
  },
@@ -62,18 +68,27 @@
62
68
  "static/admin.html",
63
69
  "static/graph.html",
64
70
  "static/workspace.html",
71
+ "static/plugins.html",
72
+ "static/workflows.html",
73
+ "static/agents.html",
74
+ "static/activity.html",
65
75
  "static/manifest.json",
66
76
  "static/sw.js",
67
77
  "static/lattice-reference.css",
68
78
  "static/workspace.css",
79
+ "static/platform.css",
69
80
  "static/scripts/",
70
81
  "static/css/",
71
82
  "static/icons/",
83
+ "plugins/",
72
84
  "docs/",
73
85
  "requirements.txt",
74
86
  "README.md"
75
87
  ],
76
88
  "publishConfig": {
77
89
  "access": "public"
90
+ },
91
+ "devDependencies": {
92
+ "@playwright/test": "^1.60.0"
78
93
  }
79
94
  }
@@ -0,0 +1,35 @@
1
+ # Lattice AI Plugins
2
+
3
+ This directory holds **Plugin SDK** packages discovered by the Lattice AI server
4
+ at runtime. Each plugin is a folder containing a `plugin.json` manifest.
5
+
6
+ A plugin is an additive, versioned, permissioned unit that **extends** the
7
+ existing Skill / Tool / Workflow surfaces — it never replaces them. Installed
8
+ standalone skills keep working unchanged.
9
+
10
+ See [`docs/PLUGIN_SDK.md`](../docs/PLUGIN_SDK.md) for the full manifest schema,
11
+ permission model, lifecycle, and execution boundary.
12
+
13
+ ## Bundled examples
14
+
15
+ | Plugin | What it shows |
16
+ | --- | --- |
17
+ | `hello-world` | Bundling a skill + a workflow template + a declarative action |
18
+ | `git-insights` | Declaring `run_tools` and surfacing read-only git insights through the permission boundary |
19
+
20
+ ## Minimal manifest
21
+
22
+ ```json
23
+ {
24
+ "id": "my-plugin",
25
+ "name": "My Plugin",
26
+ "version": "1.0.0",
27
+ "description": "What it does.",
28
+ "lattice_version": ">=2.0.0",
29
+ "permissions": ["read_workspace"],
30
+ "provides": { "skills": [], "tools": [], "workflows": [], "actions": [] }
31
+ }
32
+ ```
33
+
34
+ Permissions must be drawn from the SDK allow-list; the execution boundary
35
+ refuses any capability a plugin did not declare and was not granted at install.
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "git-insights",
3
+ "name": "Git Insights",
4
+ "version": "1.0.0",
5
+ "description": "Example plugin that surfaces read-only git status and log insights through the permissioned tool execution boundary.",
6
+ "author": "Lattice AI",
7
+ "lattice_version": ">=2.0.0",
8
+ "permissions": ["read_workspace", "run_tools"],
9
+ "provides": {
10
+ "tools": ["git_status", "git_log"],
11
+ "actions": ["summarize_repo"]
12
+ },
13
+ "entrypoint": "",
14
+ "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md"
15
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "hello-world",
3
+ "name": "Hello World",
4
+ "version": "1.0.0",
5
+ "description": "Example plugin demonstrating the Lattice AI Plugin SDK: bundles a skill, a workflow template, and a greet action.",
6
+ "author": "Lattice AI",
7
+ "lattice_version": "2.0.0",
8
+ "permissions": ["read_workspace", "run_skills"],
9
+ "provides": {
10
+ "skills": ["hello_skill"],
11
+ "workflows": ["hello-workflow"],
12
+ "actions": ["greet"]
13
+ },
14
+ "entrypoint": "",
15
+ "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md"
16
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: hello_skill
3
+ description: A minimal example skill bundled by the hello-world plugin to show that plugins extend (not replace) the existing skill system.
4
+ ---
5
+
6
+ # Hello Skill
7
+
8
+ This skill is contributed by the `hello-world` plugin. When the plugin is
9
+ installed, the Plugin SDK registers this skill into the existing Workspace skill
10
+ registry via the same `mark_skill_installed` path used by standalone skills —
11
+ demonstrating that plugins are an additive layer on top of skills.
12
+
13
+ ## Usage
14
+
15
+ Ask Lattice AI to greet a workspace member, or invoke the plugin action `greet`.
@@ -0,0 +1,70 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Realtime Activity — Lattice AI</title>
7
+ <link rel="stylesheet" href="/static/platform.css" />
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Realtime Activity</h1>
12
+ <p class="sub" id="sub">Live presence + activity feed across workspace, graph, agent, and workflow events (SSE).</p>
13
+
14
+ <div class="row">
15
+ <span class="badge" id="connBadge">connecting…</span>
16
+ <span class="badge" id="presenceBadge">presence: 0</span>
17
+ <div class="spacer"></div>
18
+ <button class="ghost" id="clearBtn">Clear view</button>
19
+ </div>
20
+
21
+ <div class="section">
22
+ <h3>Live feed</h3>
23
+ <div id="feed"><div class="empty">Waiting for events…</div></div>
24
+ </div>
25
+ </main>
26
+
27
+ <script type="module">
28
+ import { mountHeader, api, escapeHtml, badge } from "/static/scripts/platform.js";
29
+ mountHeader("/activity");
30
+
31
+ const feed = document.getElementById("feed");
32
+ const seen = new Set();
33
+
34
+ function renderEvent(ev) {
35
+ if (ev.seq && seen.has(ev.seq)) return;
36
+ if (ev.seq) seen.add(ev.seq);
37
+ if (feed.querySelector(".empty")) feed.innerHTML = "";
38
+ const node = document.createElement("div");
39
+ node.className = "timeline-item";
40
+ node.innerHTML = `<div class="row">${badge(ev.area || "event")} <strong>${escapeHtml(ev.event_type || "")}</strong>
41
+ <span class="t-meta">${escapeHtml(ev.workspace_id || "personal")} · ${escapeHtml(ev.received_at || ev.timestamp || "")}</span></div>
42
+ <pre style="max-height:160px">${escapeHtml(JSON.stringify(ev.payload || {}, null, 2))}</pre>`;
43
+ feed.prepend(node);
44
+ while (feed.children.length > 100) feed.lastChild.remove();
45
+ }
46
+
47
+ async function refreshPresence() {
48
+ try {
49
+ const data = await api("/realtime/presence");
50
+ document.getElementById("presenceBadge").textContent = `presence: ${data.presence.length}`;
51
+ document.getElementById("sub").textContent =
52
+ `Transport: ${data.stats.transport.toUpperCase()} · subscribers: ${data.stats.subscribers} · feed: ${data.stats.feed_size}`;
53
+ } catch {}
54
+ }
55
+
56
+ document.getElementById("clearBtn").addEventListener("click", () => { feed.innerHTML = `<div class="empty">Cleared.</div>`; });
57
+
58
+ // Seed with recent feed, then connect to the SSE stream.
59
+ api("/realtime/feed?limit=30").then((d) => (d.events || []).reverse().forEach(renderEvent)).catch(() => {});
60
+
61
+ const es = new EventSource("/realtime/stream");
62
+ es.onopen = () => { document.getElementById("connBadge").textContent = "● live"; document.getElementById("connBadge").className = "badge ok"; };
63
+ es.onerror = () => { document.getElementById("connBadge").textContent = "○ reconnecting"; document.getElementById("connBadge").className = "badge warn"; };
64
+ es.onmessage = (e) => { try { renderEvent(JSON.parse(e.data)); } catch {} };
65
+
66
+ refreshPresence();
67
+ setInterval(refreshPresence, 8000);
68
+ </script>
69
+ </body>
70
+ </html>
package/static/admin.html CHANGED
@@ -26,6 +26,7 @@
26
26
  <a href="#users" data-admin-nav="users"><i class="ti ti-users"></i> <span data-i18n="nav_users">사용자 관리</span></a>
27
27
  <a href="#permissions" data-admin-nav="permissions"><i class="ti ti-key"></i> <span data-i18n="nav_permissions">권한 관리</span></a>
28
28
  <a href="#sso" data-admin-nav="sso"><i class="ti ti-lock-access"></i> <span data-i18n="nav_sso">SSO 관리</span></a>
29
+ <a href="#enterprise" data-admin-nav="enterprise"><i class="ti ti-building-skyscraper"></i> <span data-i18n="nav_enterprise">Enterprise</span></a>
29
30
  <a href="#security" data-admin-nav="security"><i class="ti ti-shield-check"></i> <span data-i18n="nav_security">보안 모니터링</span></a>
30
31
  <a href="#audit" data-admin-nav="audit"><i class="ti ti-report-search"></i> <span data-i18n="nav_audit">감사 로그</span></a>
31
32
  <a href="/workspace"><i class="ti ti-layout-dashboard"></i> <span>Workspace OS</span></a>
@@ -256,6 +257,67 @@
256
257
  </section>
257
258
  </section>
258
259
 
260
+ <section class="admin-view" id="admin-view-enterprise" data-admin-view="enterprise">
261
+ <section class="panel">
262
+ <div class="panel-header">
263
+ <div>
264
+ <h3 data-i18n="enterprise_title">Enterprise Admin</h3>
265
+ <p data-i18n="enterprise_desc">Admin policies, audit export, SIEM export, organization settings, and capability status.</p>
266
+ </div>
267
+ <div class="tag-row" id="enterprise-status-tags"></div>
268
+ </div>
269
+ <div class="panel-body">
270
+ <div class="enterprise-grid" id="enterprise-capability-status"></div>
271
+ </div>
272
+ </section>
273
+
274
+ <section class="panel-grid">
275
+ <article class="panel">
276
+ <div class="panel-header">
277
+ <div>
278
+ <h3 data-i18n="enterprise_policies">Admin Policies</h3>
279
+ <p data-i18n="enterprise_policies_desc">Effective Community policy and Enterprise policy-pack status.</p>
280
+ </div>
281
+ </div>
282
+ <div class="panel-body" id="enterprise-admin-policies"></div>
283
+ </article>
284
+ <article class="panel">
285
+ <div class="panel-header">
286
+ <div>
287
+ <h3 data-i18n="enterprise_org">Organization Settings</h3>
288
+ <p data-i18n="enterprise_org_desc">Workspace governance and organization capability status.</p>
289
+ </div>
290
+ </div>
291
+ <div class="panel-body" id="enterprise-org-settings"></div>
292
+ </article>
293
+ </section>
294
+
295
+ <section class="panel-grid">
296
+ <article class="panel">
297
+ <div class="panel-header">
298
+ <div>
299
+ <h3 data-i18n="enterprise_audit_export">Audit Export</h3>
300
+ <p data-i18n="enterprise_audit_export_desc">Local export remains available in Community; retention is an Enterprise extension point.</p>
301
+ </div>
302
+ </div>
303
+ <div class="panel-body" id="enterprise-audit-export"></div>
304
+ </article>
305
+ <article class="panel">
306
+ <div class="panel-header">
307
+ <div>
308
+ <h3 data-i18n="enterprise_siem">SIEM Export</h3>
309
+ <p data-i18n="enterprise_siem_desc">Preview the SIEM envelope without streaming external events in Community.</p>
310
+ </div>
311
+ <button class="btn" id="refresh-siem-btn" type="button"><i class="ti ti-refresh"></i> <span>SIEM</span></button>
312
+ </div>
313
+ <div class="panel-body">
314
+ <div id="enterprise-siem-export"></div>
315
+ <pre class="enterprise-json" id="enterprise-siem-preview"></pre>
316
+ </div>
317
+ </article>
318
+ </section>
319
+ </section>
320
+
259
321
  <section class="admin-view" id="admin-view-security" data-admin-view="security">
260
322
  <!-- Security & Audit Command Center (피드백 #5) -->
261
323
  <section class="panel" id="security-overview-panel">