ltcai 4.0.0 → 4.0.1

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 (108) hide show
  1. package/README.md +37 -33
  2. package/docs/CHANGELOG.md +64 -0
  3. package/docs/REALTIME_COLLABORATION.md +3 -3
  4. package/docs/V3_FRONTEND.md +9 -8
  5. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +86 -43
  6. package/docs/kg-schema.md +6 -2
  7. package/docs/spec-vs-impl.md +10 -10
  8. package/kg_schema.py +2 -603
  9. package/knowledge_graph.py +37 -4958
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/api/admin.py +15 -16
  12. package/latticeai/api/agents.py +13 -6
  13. package/latticeai/api/auth.py +19 -11
  14. package/latticeai/api/invitations.py +100 -0
  15. package/latticeai/api/knowledge_graph.py +4 -11
  16. package/latticeai/api/plugins.py +3 -6
  17. package/latticeai/api/realtime.py +4 -7
  18. package/latticeai/api/static_routes.py +9 -12
  19. package/latticeai/api/ui_redirects.py +26 -0
  20. package/latticeai/api/workflow_designer.py +39 -6
  21. package/latticeai/api/workspace.py +24 -10
  22. package/latticeai/app_factory.py +88 -17
  23. package/latticeai/brain/_kg_common.py +1123 -0
  24. package/latticeai/brain/discovery.py +1455 -0
  25. package/latticeai/brain/documents.py +218 -0
  26. package/latticeai/brain/ingest.py +644 -0
  27. package/latticeai/brain/projection.py +561 -0
  28. package/latticeai/brain/provenance.py +401 -0
  29. package/latticeai/brain/retrieval.py +1316 -0
  30. package/latticeai/brain/schema.py +640 -0
  31. package/latticeai/brain/store.py +216 -0
  32. package/latticeai/brain/write_master.py +225 -0
  33. package/latticeai/core/invitations.py +131 -0
  34. package/latticeai/core/marketplace.py +1 -1
  35. package/latticeai/core/multi_agent.py +1 -1
  36. package/latticeai/core/policy.py +54 -0
  37. package/latticeai/core/realtime.py +65 -44
  38. package/latticeai/core/sessions.py +31 -5
  39. package/latticeai/core/users.py +147 -0
  40. package/latticeai/core/workspace_os.py +420 -20
  41. package/latticeai/services/agent_runtime.py +242 -4
  42. package/latticeai/services/run_executor.py +328 -0
  43. package/latticeai/services/workspace_service.py +27 -19
  44. package/package.json +2 -14
  45. package/scripts/lint_v3.mjs +23 -0
  46. package/static/v3/asset-manifest.json +21 -14
  47. package/static/v3/js/{app.356e6452.js → app.c5c80c46.js} +1 -1
  48. package/static/v3/js/core/{api.7a308b89.js → api.ba0fbf14.js} +58 -1
  49. package/static/v3/js/core/api.js +57 -0
  50. package/static/v3/js/core/i18n.880e1fec.js +575 -0
  51. package/static/v3/js/core/i18n.js +575 -0
  52. package/static/v3/js/core/routes.37522821.js +101 -0
  53. package/static/v3/js/core/routes.js +71 -63
  54. package/static/v3/js/core/{shell.a1657f20.js → shell.e3f6bbfa.js} +67 -38
  55. package/static/v3/js/core/shell.js +65 -36
  56. package/static/v3/js/core/{store.204a08b2.js → store.7b2aa044.js} +10 -0
  57. package/static/v3/js/core/store.js +10 -0
  58. package/static/v3/js/views/account.eff40715.js +143 -0
  59. package/static/v3/js/views/account.js +143 -0
  60. package/static/v3/js/views/activity.0d271ef9.js +67 -0
  61. package/static/v3/js/views/activity.js +67 -0
  62. package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
  63. package/static/v3/js/views/admin-users.js +4 -6
  64. package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
  65. package/static/v3/js/views/agents.js +35 -12
  66. package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
  67. package/static/v3/js/views/chat.js +23 -0
  68. package/static/v3/js/views/{knowledge-graph.5e40cbeb.js → knowledge-graph.4d09c537.js} +27 -7
  69. package/static/v3/js/views/knowledge-graph.js +27 -7
  70. package/static/v3/js/views/network.52a4f181.js +97 -0
  71. package/static/v3/js/views/network.js +97 -0
  72. package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
  73. package/static/v3/js/views/planning.js +26 -5
  74. package/static/v3/js/views/runs.b63b2afa.js +144 -0
  75. package/static/v3/js/views/runs.js +144 -0
  76. package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
  77. package/static/v3/js/views/settings.js +7 -8
  78. package/static/v3/js/views/snapshots.6f5db095.js +135 -0
  79. package/static/v3/js/views/snapshots.js +135 -0
  80. package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
  81. package/static/v3/js/views/workflows.js +87 -2
  82. package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
  83. package/static/v3/js/views/workspace-admin.js +156 -0
  84. package/static/account.html +0 -113
  85. package/static/activity.html +0 -73
  86. package/static/admin.html +0 -486
  87. package/static/agents.html +0 -139
  88. package/static/chat.html +0 -841
  89. package/static/css/reference/account.css +0 -439
  90. package/static/css/reference/admin.css +0 -610
  91. package/static/css/reference/base.css +0 -1661
  92. package/static/css/reference/chat.css +0 -4623
  93. package/static/css/reference/graph.css +0 -1016
  94. package/static/css/responsive.css +0 -861
  95. package/static/graph.html +0 -122
  96. package/static/platform.css +0 -104
  97. package/static/plugins.html +0 -136
  98. package/static/scripts/account.js +0 -238
  99. package/static/scripts/admin.js +0 -1614
  100. package/static/scripts/chat.js +0 -5081
  101. package/static/scripts/graph.js +0 -1804
  102. package/static/scripts/platform.js +0 -64
  103. package/static/scripts/ux.js +0 -167
  104. package/static/scripts/workspace.js +0 -948
  105. package/static/v3/js/core/routes.7222343d.js +0 -93
  106. package/static/workflows.html +0 -146
  107. package/static/workspace.css +0 -1121
  108. package/static/workspace.html +0 -357
@@ -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(