ltcai 4.0.0 → 4.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.
Files changed (195) hide show
  1. package/README.md +42 -33
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +106 -0
  4. package/docs/REALTIME_COLLABORATION.md +3 -3
  5. package/docs/V3_FRONTEND.md +9 -8
  6. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
  7. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
  8. package/docs/V4_1_VALIDATION_REPORT.md +47 -0
  9. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +95 -45
  10. package/docs/kg-schema.md +6 -2
  11. package/docs/spec-vs-impl.md +10 -10
  12. package/frontend/index.html +24 -0
  13. package/frontend/openapi.json +14190 -0
  14. package/frontend/src/App.tsx +184 -0
  15. package/frontend/src/api/client.ts +317 -0
  16. package/frontend/src/api/openapi.ts +16637 -0
  17. package/frontend/src/components/primitives.tsx +204 -0
  18. package/frontend/src/components/ui/badge.tsx +27 -0
  19. package/frontend/src/components/ui/button.tsx +37 -0
  20. package/frontend/src/components/ui/card.tsx +22 -0
  21. package/frontend/src/components/ui/input.tsx +16 -0
  22. package/frontend/src/components/ui/textarea.tsx +16 -0
  23. package/frontend/src/lib/utils.ts +33 -0
  24. package/frontend/src/main.tsx +23 -0
  25. package/frontend/src/pages/Act.tsx +245 -0
  26. package/frontend/src/pages/Ask.tsx +200 -0
  27. package/frontend/src/pages/Brain.tsx +267 -0
  28. package/frontend/src/pages/Capture.tsx +158 -0
  29. package/frontend/src/pages/Library.tsx +187 -0
  30. package/frontend/src/pages/System.tsx +344 -0
  31. package/frontend/src/routes.ts +85 -0
  32. package/frontend/src/store/appStore.ts +54 -0
  33. package/frontend/src/styles.css +107 -0
  34. package/kg_schema.py +2 -603
  35. package/knowledge_graph.py +37 -4958
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/admin.py +15 -16
  38. package/latticeai/api/agents.py +13 -6
  39. package/latticeai/api/auth.py +19 -11
  40. package/latticeai/api/invitations.py +100 -0
  41. package/latticeai/api/knowledge_graph.py +4 -11
  42. package/latticeai/api/plugins.py +3 -6
  43. package/latticeai/api/realtime.py +4 -7
  44. package/latticeai/api/setup.py +5 -4
  45. package/latticeai/api/static_routes.py +13 -16
  46. package/latticeai/api/ui_redirects.py +26 -0
  47. package/latticeai/api/workflow_designer.py +39 -6
  48. package/latticeai/api/workspace.py +24 -10
  49. package/latticeai/app_factory.py +88 -17
  50. package/latticeai/brain/_kg_common.py +1123 -0
  51. package/latticeai/brain/discovery.py +1455 -0
  52. package/latticeai/brain/documents.py +218 -0
  53. package/latticeai/brain/ingest.py +644 -0
  54. package/latticeai/brain/projection.py +561 -0
  55. package/latticeai/brain/provenance.py +401 -0
  56. package/latticeai/brain/retrieval.py +1316 -0
  57. package/latticeai/brain/schema.py +640 -0
  58. package/latticeai/brain/store.py +216 -0
  59. package/latticeai/brain/write_master.py +225 -0
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/multi_agent.py +1 -1
  63. package/latticeai/core/policy.py +54 -0
  64. package/latticeai/core/realtime.py +65 -44
  65. package/latticeai/core/sessions.py +31 -5
  66. package/latticeai/core/users.py +147 -0
  67. package/latticeai/core/workspace_os.py +420 -20
  68. package/latticeai/services/agent_runtime.py +242 -4
  69. package/latticeai/services/run_executor.py +328 -0
  70. package/latticeai/services/workspace_service.py +27 -19
  71. package/package.json +54 -27
  72. package/scripts/build_frontend_assets.mjs +38 -0
  73. package/scripts/bump_version.py +1 -1
  74. package/scripts/export_openapi.py +31 -0
  75. package/scripts/lint_frontend.mjs +86 -0
  76. package/scripts/run_python.mjs +47 -0
  77. package/src-tauri/Cargo.lock +4833 -0
  78. package/src-tauri/Cargo.toml +19 -0
  79. package/src-tauri/build.rs +3 -0
  80. package/src-tauri/capabilities/default.json +7 -0
  81. package/src-tauri/src/main.rs +78 -0
  82. package/src-tauri/tauri.conf.json +36 -0
  83. package/static/app/asset-manifest.json +32 -0
  84. package/static/app/assets/core-CwxXejkd.js +2 -0
  85. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  86. package/static/app/assets/index-CJRAzNnf.js +333 -0
  87. package/static/app/assets/index-CJRAzNnf.js.map +1 -0
  88. package/static/app/assets/index-CSwBBgf4.css +2 -0
  89. package/static/app/index.html +25 -0
  90. package/static/manifest.json +2 -2
  91. package/static/sw.js +4 -4
  92. package/scripts/build_v3_assets.mjs +0 -170
  93. package/scripts/lint_v3.mjs +0 -97
  94. package/static/account.html +0 -113
  95. package/static/activity.html +0 -73
  96. package/static/admin.html +0 -486
  97. package/static/agents.html +0 -139
  98. package/static/chat.html +0 -841
  99. package/static/css/reference/account.css +0 -439
  100. package/static/css/reference/admin.css +0 -610
  101. package/static/css/reference/base.css +0 -1661
  102. package/static/css/reference/chat.css +0 -4623
  103. package/static/css/reference/graph.css +0 -1016
  104. package/static/css/responsive.css +0 -861
  105. package/static/graph.html +0 -122
  106. package/static/platform.css +0 -104
  107. package/static/plugins.html +0 -136
  108. package/static/scripts/account.js +0 -238
  109. package/static/scripts/admin.js +0 -1614
  110. package/static/scripts/chat.js +0 -5081
  111. package/static/scripts/graph.js +0 -1804
  112. package/static/scripts/platform.js +0 -64
  113. package/static/scripts/ux.js +0 -167
  114. package/static/scripts/workspace.js +0 -948
  115. package/static/v3/asset-manifest.json +0 -56
  116. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  117. package/static/v3/css/lattice.base.css +0 -128
  118. package/static/v3/css/lattice.components.cde18231.css +0 -472
  119. package/static/v3/css/lattice.components.css +0 -472
  120. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  121. package/static/v3/css/lattice.shell.css +0 -452
  122. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  123. package/static/v3/css/lattice.tokens.css +0 -135
  124. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  125. package/static/v3/css/lattice.views.css +0 -360
  126. package/static/v3/index.html +0 -68
  127. package/static/v3/js/app.356e6452.js +0 -26
  128. package/static/v3/js/app.js +0 -26
  129. package/static/v3/js/core/api.7a308b89.js +0 -568
  130. package/static/v3/js/core/api.js +0 -568
  131. package/static/v3/js/core/components.f25b3b93.js +0 -230
  132. package/static/v3/js/core/components.js +0 -230
  133. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  134. package/static/v3/js/core/dom.js +0 -148
  135. package/static/v3/js/core/router.584570f2.js +0 -37
  136. package/static/v3/js/core/router.js +0 -37
  137. package/static/v3/js/core/routes.7222343d.js +0 -93
  138. package/static/v3/js/core/routes.js +0 -93
  139. package/static/v3/js/core/shell.a1657f20.js +0 -391
  140. package/static/v3/js/core/shell.js +0 -391
  141. package/static/v3/js/core/store.204a08b2.js +0 -113
  142. package/static/v3/js/core/store.js +0 -113
  143. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  144. package/static/v3/js/views/admin-audit.js +0 -185
  145. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  146. package/static/v3/js/views/admin-permissions.js +0 -177
  147. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  148. package/static/v3/js/views/admin-policies.js +0 -102
  149. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  150. package/static/v3/js/views/admin-private-vpc.js +0 -135
  151. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  152. package/static/v3/js/views/admin-security.js +0 -180
  153. package/static/v3/js/views/admin-users.03bac88c.js +0 -168
  154. package/static/v3/js/views/admin-users.js +0 -168
  155. package/static/v3/js/views/agents.014d0b74.js +0 -541
  156. package/static/v3/js/views/agents.js +0 -541
  157. package/static/v3/js/views/chat.e6dd7dd0.js +0 -601
  158. package/static/v3/js/views/chat.js +0 -601
  159. package/static/v3/js/views/files.adad14c1.js +0 -365
  160. package/static/v3/js/views/files.js +0 -365
  161. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  162. package/static/v3/js/views/graph-canvas.js +0 -509
  163. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  164. package/static/v3/js/views/home.js +0 -200
  165. package/static/v3/js/views/hooks.37895880.js +0 -220
  166. package/static/v3/js/views/hooks.js +0 -220
  167. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  168. package/static/v3/js/views/hybrid-search.js +0 -194
  169. package/static/v3/js/views/knowledge-graph.5e40cbeb.js +0 -509
  170. package/static/v3/js/views/knowledge-graph.js +0 -509
  171. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  172. package/static/v3/js/views/marketplace.js +0 -141
  173. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  174. package/static/v3/js/views/mcp.js +0 -114
  175. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  176. package/static/v3/js/views/memory.js +0 -147
  177. package/static/v3/js/views/models.a1ffa147.js +0 -256
  178. package/static/v3/js/views/models.js +0 -256
  179. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  180. package/static/v3/js/views/my-computer.js +0 -463
  181. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  182. package/static/v3/js/views/pipeline.js +0 -157
  183. package/static/v3/js/views/planning.9ac3e313.js +0 -153
  184. package/static/v3/js/views/planning.js +0 -153
  185. package/static/v3/js/views/settings.8631fa5e.js +0 -318
  186. package/static/v3/js/views/settings.js +0 -318
  187. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  188. package/static/v3/js/views/skills.js +0 -109
  189. package/static/v3/js/views/tools.e4f11276.js +0 -108
  190. package/static/v3/js/views/tools.js +0 -108
  191. package/static/v3/js/views/workflows.26c57290.js +0 -128
  192. package/static/v3/js/views/workflows.js +0 -128
  193. package/static/workflows.html +0 -146
  194. package/static/workspace.css +0 -1121
  195. package/static/workspace.html +0 -357
@@ -20,6 +20,7 @@ from typing import Dict, List, Optional
20
20
  from fastapi import APIRouter, HTTPException, Request
21
21
  from pydantic import BaseModel
22
22
 
23
+ from latticeai.api.ui_redirects import app_redirect
23
24
  from latticeai.services.app_context import AppContext
24
25
 
25
26
 
@@ -161,7 +162,6 @@ def create_workspace_router(context: AppContext) -> APIRouter:
161
162
  remove_skill_directory = context.remove_skill_directory
162
163
  redact_secret_text = context.redact_secret_text
163
164
  capability_registry = context.capability_registry
164
- ui_file_response = context.ui_file_response
165
165
 
166
166
  svc = service
167
167
  WORKSPACE_OS = service.store
@@ -173,7 +173,6 @@ def create_workspace_router(context: AppContext) -> APIRouter:
173
173
  KNOWLEDGE_GRAPH = context.knowledge_graph
174
174
  LOCAL_KG_WATCHER = context.local_kg_watcher
175
175
  SKILLS_DIR = context.skills_dir
176
- STATIC_DIR = context.static_dir
177
176
  LOCAL_MODEL = context.local_model
178
177
  PUBLIC_MODEL = context.public_model
179
178
  _fetch_skills_marketplace = context.fetch_skills_marketplace
@@ -214,18 +213,12 @@ def create_workspace_router(context: AppContext) -> APIRouter:
214
213
  @router.get("/workspace")
215
214
  async def workspace_page(request: Request):
216
215
  require_user(request)
217
- workspace_path = STATIC_DIR / "workspace.html"
218
- if not workspace_path.exists():
219
- raise HTTPException(status_code=404, detail="Workspace OS UI not found.")
220
- return ui_file_response(workspace_path)
216
+ return app_redirect("workspace-admin", request)
221
217
 
222
218
  @router.get("/onboarding")
223
219
  async def onboarding_page(request: Request):
224
220
  require_user(request)
225
- workspace_path = STATIC_DIR / "workspace.html"
226
- if not workspace_path.exists():
227
- raise HTTPException(status_code=404, detail="Workspace OS UI not found.")
228
- return ui_file_response(workspace_path)
221
+ return app_redirect("workspace-admin", request)
229
222
 
230
223
  # ── Workspace OS summary / onboarding ─────────────────────────────────
231
224
 
@@ -387,6 +380,27 @@ def create_workspace_router(context: AppContext) -> APIRouter:
387
380
  append_audit_event("workspace_snapshot_export", user_email=current_user, snapshot_id=snapshot_id, path=result.get("export_path"))
388
381
  return result
389
382
 
383
+ @router.post("/workspace/snapshots/{snapshot_id}/restore")
384
+ async def workspace_snapshot_restore(snapshot_id: str, request: Request):
385
+ current_user = require_user(request)
386
+ snapshot = _load_snapshot_authorized(request, snapshot_id)
387
+ scope = _gate_write(request)
388
+ if snapshot.get("workspace_id") and snapshot.get("workspace_id") != scope:
389
+ raise HTTPException(status_code=403, detail="snapshot belongs to a different workspace")
390
+ try:
391
+ result = WORKSPACE_OS.restore_snapshot(
392
+ snapshot_id,
393
+ graph=_workspace_graph(),
394
+ workspace_id=scope,
395
+ user_email=current_user or None,
396
+ )
397
+ except FileNotFoundError as exc:
398
+ raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
399
+ except ValueError as exc:
400
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
401
+ append_audit_event("workspace_snapshot_restore", user_email=current_user, snapshot_id=snapshot_id, restore_id=result.get("restore", {}).get("id"))
402
+ return result
403
+
390
404
  @router.get("/workspace/time-machine")
391
405
  async def workspace_time_machine(request: Request, limit: int = 100):
392
406
  require_user(request)
@@ -94,6 +94,16 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
94
94
  from latticeai.core.enterprise import (
95
95
  capability_registry,
96
96
  )
97
+ from latticeai.core.invitations import InvitationStore
98
+ from latticeai.core.policy import normalize_role, policy_matrix, require_capability
99
+ from latticeai.core.users import (
100
+ ensure_user_identity,
101
+ load_users_file,
102
+ migrate_knowledge_graph_identity,
103
+ normalize_email,
104
+ save_users_file,
105
+ user_id_for_email as _user_id_for_email,
106
+ )
97
107
  from latticeai.services.app_context import AppContext
98
108
  from latticeai.services.workspace_service import WorkspaceService
99
109
  from latticeai.services.model_service import ModelService
@@ -127,10 +137,12 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
127
137
  from latticeai.core.realtime import RealtimeBus
128
138
  from latticeai.core.marketplace import TemplateCatalog
129
139
  from latticeai.services.platform_runtime import PlatformRuntime
140
+ from latticeai.services.run_executor import RunExecutor
130
141
  from latticeai.api.plugins import create_plugins_router
131
142
  from latticeai.api.workflow_designer import create_workflow_designer_router
132
143
  from latticeai.api.agents import create_agents_router
133
144
  from latticeai.api.realtime import create_realtime_router
145
+ from latticeai.api.invitations import create_invitations_router
134
146
  from latticeai.api.marketplace import create_marketplace_router
135
147
  from latticeai.api.models import create_models_router
136
148
  from latticeai.api.chat import create_chat_router
@@ -277,12 +289,18 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
277
289
  def _client_ip(request: Request) -> str:
278
290
  return _client_ip_impl(request)
279
291
 
292
+ def user_id_for_email(email: Optional[str]) -> Optional[str]:
293
+ return _user_id_for_email(load_users(), email)
294
+
280
295
  def create_session(email: str) -> str:
281
- return _session_store.create(email)
296
+ return _session_store.create(user_id_for_email(email) or email, email=email)
282
297
 
283
298
  def get_session_email(token: str) -> Optional[str]:
284
299
  return _session_store.get_email(token)
285
300
 
301
+ def get_session_user_id(token: str) -> Optional[str]:
302
+ return _session_store.get_subject(token)
303
+
286
304
  def invalidate_session(token: str) -> None:
287
305
  _session_store.invalidate(token)
288
306
 
@@ -345,7 +363,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
345
363
  WORKSPACE_OS = WorkspaceOSStore(DATA_DIR, event_sink=REALTIME_BUS)
346
364
  # Service layer (latticeai.services) wraps the store with scope/permission
347
365
  # guardrails; routers and the app assembly share this single instance.
348
- WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS)
366
+ WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS, resolve_user_id=user_id_for_email)
367
+ INVITATION_STORE = InvitationStore(DATA_DIR / "invitations.json")
349
368
  # ── v2 Plugin SDK registry (extends skills; discovers plugins/<id>/plugin.json)
350
369
  PLUGINS_DIR = Path(os.getenv("LATTICEAI_PLUGINS_DIR") or (BASE_DIR / "plugins"))
351
370
  PLUGIN_REGISTRY = PluginRegistry(PLUGINS_DIR, store=WORKSPACE_OS)
@@ -496,14 +515,24 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
496
515
 
497
516
 
498
517
  def load_users():
499
- if not os.path.exists(USERS_FILE):
500
- return {}
501
- with open(USERS_FILE, "r", encoding="utf-8") as f:
502
- return json.load(f)
518
+ users = load_users_file(USERS_FILE)
519
+ email_to_id = {
520
+ email: user.get("id")
521
+ for email, user in users.items()
522
+ if isinstance(user, dict) and user.get("id")
523
+ }
524
+ try:
525
+ migrate_knowledge_graph_identity(DATA_DIR / "knowledge_graph.sqlite", email_to_id)
526
+ except Exception as exc:
527
+ logging.warning("knowledge graph identity migration skipped: %s", exc)
528
+ try:
529
+ WORKSPACE_OS.migrate_workspace_identities(email_to_id)
530
+ except Exception as exc:
531
+ logging.warning("workspace identity migration skipped: %s", exc)
532
+ return users
503
533
 
504
534
  def save_users(users):
505
- with open(USERS_FILE, "w", encoding="utf-8") as f:
506
- json.dump(users, f, ensure_ascii=False, indent=2)
535
+ save_users_file(USERS_FILE, users)
507
536
 
508
537
  def load_vpc_config() -> Dict:
509
538
  if not os.path.exists(VPC_FILE):
@@ -786,14 +815,22 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
786
815
 
787
816
  def get_user_role(email: str, users: Optional[Dict] = None) -> str:
788
817
  users = users or load_users()
789
- user = users.get(email) or {}
790
- if user.get("role") in {"admin", "user"}:
791
- return user["role"]
792
- admin_emails = set(CONFIG.admin_emails)
793
- if email.lower() in admin_emails:
818
+ identity = str(email or "")
819
+ normalized_email = normalize_email(identity)
820
+ user = users.get(normalized_email) or users.get(identity) or next(
821
+ (
822
+ item for item in users.values()
823
+ if isinstance(item, dict) and item.get("id") == identity
824
+ ),
825
+ {},
826
+ )
827
+ if isinstance(user, dict) and user.get("role"):
828
+ return normalize_role(user["role"])
829
+ admin_emails = {normalize_email(item) for item in CONFIG.admin_emails}
830
+ if normalized_email in admin_emails:
794
831
  return "admin"
795
832
  first_email = next(iter(users), None)
796
- return "admin" if first_email == email else "user"
833
+ return "admin" if first_email == normalized_email else "user"
797
834
 
798
835
  def _extract_bearer_token(request: Request) -> Optional[str]:
799
836
  auth = request.headers.get("Authorization", "")
@@ -888,16 +925,24 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
888
925
  if token:
889
926
  email = get_session_email(token)
890
927
  if email:
891
- if get_user_role(email, users) == "admin":
928
+ role = get_user_role(email, users)
929
+ try:
930
+ require_capability(role, "admin:users")
892
931
  return email, users
932
+ except PermissionError:
933
+ pass
893
934
  raise HTTPException(status_code=403, detail="관리자 권한이 필요합니다.")
894
935
 
895
936
  def public_user(email: str, user: Dict, users: Dict) -> Dict:
937
+ role = get_user_role(email, users)
938
+ user_id = user.get("id") or _user_id_for_email(users, email)
896
939
  return {
940
+ "id": user_id,
897
941
  "email": email,
942
+ "identity": user_id,
898
943
  "name": user.get("name", ""),
899
944
  "nickname": user.get("nickname", ""),
900
- "role": get_user_role(email, users),
945
+ "role": role,
901
946
  "disabled": bool(user.get("disabled", False)),
902
947
  }
903
948
 
@@ -968,6 +1013,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
968
1013
  "role": "user",
969
1014
  "disabled": False,
970
1015
  }
1016
+ ensure_user_identity(email, user)
971
1017
  api_keys = user.get("api_keys") or {}
972
1018
  api_keys[provider] = key
973
1019
  user["api_keys"] = api_keys
@@ -1194,6 +1240,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1194
1240
  public_sso_config=public_sso_config,
1195
1241
  open_registration=OPEN_REGISTRATION, session_ttl=_SESSION_TTL,
1196
1242
  require_auth=REQUIRE_AUTH,
1243
+ ensure_identity=ensure_user_identity,
1197
1244
  ))
1198
1245
 
1199
1246
  def _graph_stats_safe():
@@ -1215,6 +1262,16 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1215
1262
  get_graph_stats=_graph_stats_safe, enable_graph=ENABLE_GRAPH,
1216
1263
  invite_code=INVITE_CODE, invite_gate_enabled=INVITE_GATE_ENABLED,
1217
1264
  default_port=DEFAULT_PORT,
1265
+ policy_matrix=policy_matrix,
1266
+ ))
1267
+
1268
+ app.include_router(create_invitations_router(
1269
+ invitation_store=INVITATION_STORE,
1270
+ workspace_service=WORKSPACE_SERVICE,
1271
+ require_admin=require_admin,
1272
+ require_user=require_user,
1273
+ user_id_for_email=user_id_for_email,
1274
+ append_audit_event=append_audit_event,
1218
1275
  ))
1219
1276
 
1220
1277
  # ── Security & Audit Command Center (피드백 #5) ──────────────────────────────
@@ -1426,7 +1483,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1426
1483
  description="Fires brain_event workflow triggers when knowledge enters the brain.",
1427
1484
  )["id"]
1428
1485
  HOOKS_REGISTRY.register_hook(_trigger_hook_id, TRIGGER_SERVICE.hook_runner())
1429
- TRIGGER_SERVICE.start()
1430
1486
 
1431
1487
  # Single AgentRuntime boundary over the orchestrator + run store.
1432
1488
  AGENT_RUNTIME = AgentRuntime(
@@ -1436,6 +1492,18 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1436
1492
  append_audit_event=append_audit_event,
1437
1493
  hooks=HOOKS_REGISTRY,
1438
1494
  )
1495
+ RUN_EXECUTOR = RunExecutor(
1496
+ store=WORKSPACE_OS,
1497
+ agent_runtime=AGENT_RUNTIME,
1498
+ build_workflow_runners=PLATFORM.build_workflow_runners,
1499
+ workspace_graph=_workspace_graph,
1500
+ append_audit_event=append_audit_event,
1501
+ hooks=HOOKS_REGISTRY,
1502
+ )
1503
+ AGENT_RUNTIME.attach_executor(RUN_EXECUTOR)
1504
+ app.state.run_executor = RUN_EXECUTOR
1505
+ app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
1506
+ TRIGGER_SERVICE.start()
1439
1507
 
1440
1508
  # ── Hooks dispatch: bind real built-in runners ───────────────────────────────
1441
1509
  # The registry lists built-in hooks; binding a runner here makes them *execute*
@@ -1472,6 +1540,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1472
1540
  ui_file_response=ui_file_response,
1473
1541
  static_dir=STATIC_DIR,
1474
1542
  hooks=HOOKS_REGISTRY,
1543
+ run_executor=RUN_EXECUTOR,
1544
+ trigger_service=TRIGGER_SERVICE,
1475
1545
  ))
1476
1546
 
1477
1547
  app.include_router(create_agents_router(
@@ -1486,6 +1556,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1486
1556
  ui_file_response=ui_file_response,
1487
1557
  static_dir=STATIC_DIR,
1488
1558
  agent_runtime=AGENT_RUNTIME,
1559
+ run_executor=RUN_EXECUTOR,
1489
1560
  ))
1490
1561
 
1491
1562
  app.include_router(create_marketplace_router(