ltcai 8.0.0 → 8.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 (103) hide show
  1. package/README.md +32 -50
  2. package/docs/CHANGELOG.md +14 -0
  3. package/docs/V3_FRONTEND.md +11 -5
  4. package/lattice_brain/__init__.py +1 -1
  5. package/lattice_brain/embeddings.py +9 -1
  6. package/lattice_brain/runtime/multi_agent.py +1 -1
  7. package/latticeai/__init__.py +1 -1
  8. package/latticeai/app_factory.py +13 -134
  9. package/latticeai/core/local_embeddings.py +8 -0
  10. package/latticeai/core/marketplace.py +1 -1
  11. package/latticeai/core/mcp_registry.py +140 -1
  12. package/latticeai/core/workspace_os.py +1 -1
  13. package/latticeai/services/architecture_readiness.py +6 -5
  14. package/latticeai/services/model_runtime.py +36 -2
  15. package/latticeai/services/product_readiness.py +18 -18
  16. package/package.json +2 -2
  17. package/scripts/build_frontend_assets.mjs +11 -2
  18. package/src-tauri/Cargo.lock +1 -1
  19. package/src-tauri/Cargo.toml +1 -1
  20. package/src-tauri/tauri.conf.json +1 -1
  21. package/static/app/asset-manifest.json +11 -102
  22. package/static/app/assets/Act-BOO66G-c.js +1 -0
  23. package/static/app/assets/Brain-C6lEYiD7.js +321 -0
  24. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-TATXBRDw.js} +1 -2
  25. package/static/app/assets/Library-DK4FIp8a.js +1 -0
  26. package/static/app/assets/System-Bgs6Ql7x.js +1 -0
  27. package/static/app/assets/core-CwxXejkd.js +1 -2
  28. package/static/app/assets/index-Bvv79nre.js +16 -0
  29. package/static/app/assets/index-Dslqglia.css +2 -0
  30. package/static/app/assets/primitives-B70WOra0.js +1 -0
  31. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-Czrd9gwM.js} +1 -2
  32. package/static/app/index.html +3 -10
  33. package/static/app/theme-boot.js +8 -0
  34. package/static/sw.js +1 -1
  35. package/frontend/index.html +0 -24
  36. package/frontend/openapi.json +0 -15425
  37. package/frontend/src/App.tsx +0 -243
  38. package/frontend/src/api/client.ts +0 -580
  39. package/frontend/src/api/openapi.ts +0 -17892
  40. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  41. package/frontend/src/components/FeedbackState.tsx +0 -45
  42. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  43. package/frontend/src/components/LivingBrain.tsx +0 -220
  44. package/frontend/src/components/ProductFlow.tsx +0 -171
  45. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  46. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  47. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  48. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  49. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  50. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  51. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  52. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  53. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  54. package/frontend/src/components/primitives.tsx +0 -392
  55. package/frontend/src/components/ui/badge.tsx +0 -27
  56. package/frontend/src/components/ui/button.tsx +0 -37
  57. package/frontend/src/components/ui/card.tsx +0 -22
  58. package/frontend/src/components/ui/input.tsx +0 -16
  59. package/frontend/src/components/ui/textarea.tsx +0 -16
  60. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  61. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  62. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  63. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  64. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  65. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  66. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  67. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  68. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  69. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  70. package/frontend/src/features/brain/brainData.ts +0 -246
  71. package/frontend/src/features/brain/graphLayout.ts +0 -37
  72. package/frontend/src/features/brain/types.ts +0 -150
  73. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  74. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  75. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  76. package/frontend/src/i18n.ts +0 -1303
  77. package/frontend/src/lib/utils.ts +0 -33
  78. package/frontend/src/main.tsx +0 -23
  79. package/frontend/src/pages/Act.tsx +0 -458
  80. package/frontend/src/pages/Ask.tsx +0 -14
  81. package/frontend/src/pages/Brain.tsx +0 -914
  82. package/frontend/src/pages/Capture.tsx +0 -258
  83. package/frontend/src/pages/Library.tsx +0 -486
  84. package/frontend/src/pages/System.tsx +0 -621
  85. package/frontend/src/routes.ts +0 -92
  86. package/frontend/src/store/appStore.ts +0 -94
  87. package/frontend/src/styles.css +0 -6579
  88. package/static/app/assets/Act-DOvf59ru.js +0 -2
  89. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  90. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  91. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  92. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  93. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  94. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  95. package/static/app/assets/System-D6t9jo9V.js +0 -2
  96. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  97. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  98. package/static/app/assets/index-C7g26IF6.css +0 -2
  99. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  100. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  101. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  102. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  103. package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
package/README.md CHANGED
@@ -64,9 +64,9 @@ You need Lattice AI when:
64
64
 
65
65
  ## Living Brain Flow
66
66
 
67
- The screenshots below are release evidence captures. The 8.0 line keeps the
68
- same first-run flow and rebuilds the Brain Chat Home layout so chat and the
69
- composer are the first visible action.
67
+ The screenshots below are release evidence captures. The 8.1 line keeps the
68
+ first-run flow and refreshes Brain Home so the living Brain, recent memory,
69
+ next action, and composer are understandable in one glance.
70
70
 
71
71
  ### 1. Wake Brain
72
72
 
@@ -78,43 +78,36 @@ confirm owner, check the computer, choose the Brain voice.
78
78
  Choose the owner of the Brain. The profile is not a SaaS account by default; it
79
79
  is the local identity for the knowledge you keep.
80
80
 
81
- ![Login](output/release/v7.8.0/screenshots/01-login.png)
81
+ ![Login](output/release/v8.1.0/screenshots/01-login.png)
82
82
 
83
- ### 3. Environment Analysis
84
-
85
- See what kind of local AI experience this computer can support before choosing a
86
- model.
87
-
88
- ![Environment Analysis](output/release/v7.8.0/screenshots/02-environment-analysis.png)
89
-
90
- ### 4. Recommended Models
83
+ ### 3. Recommended Models
91
84
 
92
85
  Start with a short list: safest recommendation, faster model, stronger model.
93
86
  Advanced details stay available without overwhelming first-time users.
94
87
 
95
- ![Recommended Models](output/release/v7.8.0/screenshots/03-recommended-models.png)
88
+ ![Recommended Models](output/release/v8.1.0/screenshots/02-recommended-models.png)
96
89
 
97
- ### 5. Install And Load
90
+ ### 4. Install And Load
98
91
 
99
92
  Download and load only after consent. Lattice explains model size, local
100
93
  execution, and network use before work starts.
101
94
 
102
- ![Install and Load](output/release/v7.8.0/screenshots/04-install-load-progress.png)
95
+ ![Install and Load](output/release/v8.1.0/screenshots/03-install-load-progress.png)
103
96
 
104
- ### 6. Brain Chat
97
+ ### 5. Brain Chat
105
98
 
106
99
  Talk normally. Useful decisions and context become memory, then appear later as
107
100
  topics, relationships, graph structure, and the concentric memory rings around
108
101
  the Brain.
109
102
 
110
- ![Brain Chat Home](output/release/v7.8.0/screenshots/05-brain-chat-home.png)
103
+ ![Brain Chat Home](output/release/v8.1.0/screenshots/04-brain-chat-home.png)
111
104
 
112
- ### 7. Review Center
105
+ ### 6. Review Center
113
106
 
114
107
  Automation results are staged for review before they become durable decisions.
115
108
  Snooze, unsnooze, run now, approve, and dismiss actions stay explicit.
116
109
 
117
- ![Review Center](output/release/v7.8.0/screenshots/13-review-center.png)
110
+ ![Review Center](output/release/v8.1.0/screenshots/12-review-center.png)
118
111
 
119
112
  ## Brain Depths
120
113
 
@@ -130,10 +123,10 @@ The user travels inward from everyday memory to deeper structure:
130
123
 
131
124
  Walkthrough:
132
125
 
133
- ![v7.8.0 Living Brain walkthrough](output/release/v7.8.0/gifs/v7.8.0-living-brain-walkthrough.gif)
126
+ ![v8.1.0 Living Brain walkthrough](output/release/v8.1.0/gifs/v8.1.0-living-brain-walkthrough.gif)
134
127
 
135
128
  Screenshot index and capture notes:
136
- [output/release/v7.8.0/SCREENSHOT_INDEX.md](output/release/v7.8.0/SCREENSHOT_INDEX.md)
129
+ [output/release/v8.1.0/SCREENSHOT_INDEX.md](output/release/v8.1.0/SCREENSHOT_INDEX.md)
137
130
 
138
131
  ## Install
139
132
 
@@ -210,39 +203,27 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
210
203
 
211
204
  ## Current Release
212
205
 
213
- The current release is **8.0.0 — Runtime Architecture Contract**:
214
-
215
- - `architecture_readiness()` now emits `lattice-architecture-contract/v1`,
216
- including the preferred refactoring order and concrete ownership for
217
- AgentRuntime, ToolRegistry, central Config, server decomposition, and KG
218
- hardening.
219
- - The product AgentRuntime facade in `lattice_brain.runtime.agent_runtime`
220
- remains the unambiguous `/agents` execution and observability boundary.
221
- - ToolRegistry manifest output now carries `tool-registry-contract/v1` with
222
- dispatch, policy, and permission ownership, so runtime/admin surfaces can
223
- detect catalog drift from one source of truth.
224
- - Knowledge Graph hardening is represented as an additive reprojection strategy
225
- with legacy read compatibility and non-destructive rollback/export paths.
226
- - KG v2 read-equivalence is locked for document listing, node lookup,
227
- relationship search, and traversal, while logical imports preserve colliding
228
- legacy edge labels without breaking native canonical dedupe.
229
- - Product readiness is machine-checkable through `product_readiness()`, which
230
- composes the architecture contract with exact artifact/docs/workflow evidence.
231
- - CI and release workflows continue to run frontend lint/typecheck/build gates
232
- plus the 8.0 product readiness score before a tag can be treated as release-safe.
233
-
234
- Expected artifacts for 8.0.0 release must use exact filenames:
235
-
236
- - `dist/ltcai-8.0.0-py3-none-any.whl`
237
- - `dist/ltcai-8.0.0.tar.gz`
238
- - `ltcai-8.0.0.tgz`
239
- - `dist/ltcai-8.0.0.vsix`
240
- - `src-tauri/target/release/bundle/dmg/Lattice AI_8.0.0_aarch64.dmg`
206
+ The current release is **8.1.0 — Intuitive Brain Home**:
207
+
208
+ - Brain Home now opens with the living Brain directly above the conversation composer, so the primary action is visible before users read any utility panels.
209
+ - The previous dashboard-style growth strip is replaced by a product narrative: Brain state, what was just remembered, the strongest connected topic, and the next best action.
210
+ - Primary actions are explicit and click-tested: talk to Brain, add sources, or open the memory graph.
211
+ - Mobile and narrow layouts keep the Brain and composer in the first viewport, avoiding the previous clipped/overcrowded first screen.
212
+ - The 8.0 runtime architecture contract remains active: AgentRuntime, ToolRegistry, Config, server decomposition, and KG hardening stay machine-checkable through readiness gates.
213
+ - CI and release workflows continue to run frontend lint/typecheck/build gates plus product readiness before a tag can be treated as release-safe.
214
+
215
+ Expected artifacts for 8.1.0 release must use exact filenames:
216
+
217
+ - `dist/ltcai-8.1.0-py3-none-any.whl`
218
+ - `dist/ltcai-8.1.0.tar.gz`
219
+ - `ltcai-8.1.0.tgz`
220
+ - `dist/ltcai-8.1.0.vsix`
221
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_8.1.0_aarch64.dmg`
241
222
 
242
223
  Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
243
224
 
244
225
  See [docs/ROADMAP_RECOMMENDATIONS.md](docs/ROADMAP_RECOMMENDATIONS.md) for the
245
- strategic roadmap slices applied through 8.0.0 and the follow-up tracks.
226
+ strategic roadmap slices applied through 8.1.0 and the follow-up tracks.
246
227
 
247
228
  ## Known Limitations
248
229
 
@@ -259,6 +240,7 @@ strategic roadmap slices applied through 8.0.0 and the follow-up tracks.
259
240
 
260
241
  | Version | Theme |
261
242
  | --- | --- |
243
+ | 8.1.0 | Intuitive Brain Home: living Brain, recent memory, connected topic, next action, and composer are visible in one product-first screen with refreshed 8.1.0 evidence and artifacts |
262
244
  | 8.0.0 | Runtime Architecture Contract: AgentRuntime, ToolRegistry, central Config, server decomposition, and KG hardening are captured as machine-checkable release boundaries with exact 8.0.0 artifacts |
263
245
  | 7.9.0 | Agent Runtime Boundary Hardening: explicit `SingleAgentRuntime`, compatibility alias preservation, injected rollback port, and release/readiness docs aligned to the product AgentRuntime facade |
264
246
  | 7.8.0 | Brain Chat Home UX Simplification: chat-first first viewport, visible workspace navigation, collapsed source/status utilities, hidden default depth controls, and removal of obsolete Brain UX components |
package/docs/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ existed at that release.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [8.1.0] - 2026-06-27
10
+
11
+ ### Changed
12
+ - Rebuilt Brain Home around an intuitive first screen with LivingBrain, recent
13
+ memory, connected topic, next-best action, and the composer visible together.
14
+ - Replaced dashboard-style Brain growth metrics with narrative, product-facing
15
+ copy and focused primary actions.
16
+ - Tightened mobile and narrow viewport behavior so the Brain and composer remain
17
+ visible without horizontal overflow.
18
+ - Refreshed release screenshots, walkthrough GIF/WebM, static app assets, and
19
+ exact 8.1.0 artifact metadata.
20
+ - Synchronized Python, npm, VS Code extension, Tauri, workspace, readiness, and
21
+ current-release documentation versions to 8.1.0.
22
+
9
23
  ## [8.0.0] - 2026-06-24
10
24
 
11
25
  ### Changed
@@ -1,10 +1,15 @@
1
1
  # Lattice AI v3 — Frontend Product Shell
2
2
 
3
+ > Historical v3 shell notes. The current product shell is the React/Vite app
4
+ > built from `frontend/` into `static/app/` with `npm run build:assets`; current
5
+ > linting uses `scripts/lint_frontend.mjs`.
6
+
3
7
  > A token-native single-page workspace shell for Lattice AI v3. It ships as the
4
8
  > primary product experience and calls the real v3 retrieval/chat APIs while
5
9
  > rendering clear unavailable states for local services that are not running.
6
10
 
7
11
  Entry point: **`/app`** (served by `latticeai/api/static_routes.py` →
12
+ the current `static/app/index.html` bundle; the historical v3 shell used
8
13
  `static/v3/index.html`). Login, auto-login after registration, SSO callback, and
9
14
  the PWA manifest land on `/app`. Legacy multi-page static screens have been
10
15
  deleted; compatibility routes (`/workspace`, `/chat`, `/graph`, `/admin`, …)
@@ -27,7 +32,8 @@ Deep-linking into an `admin/*` route auto-promotes the shell to Admin mode.
27
32
 
28
33
  ## Information architecture
29
34
 
30
- One declarative table (`static/v3/js/core/routes.js`) drives the nav rail, the
35
+ One declarative table (historically `static/v3/js/core/routes.js`, now
36
+ represented by the React routes under `frontend/src/`) drives the nav rail, the
31
37
  command palette, the router, breadcrumbs, and lazy view loading.
32
38
 
33
39
  ```
@@ -50,8 +56,8 @@ Administration Users · Permissions · Audit Logs · Security · Policies · Pri
50
56
  Token-native, layered on the existing **single color source**
51
57
  (`static/css/tokens.css`, `data-lt-theme` light/dark). No legacy override layers
52
58
  (`responsive.css` / `workspace.css` / `platform.css`) are loaded by v3.
53
- Runtime assets are loaded through `static/v3/asset-manifest.json`; the build
54
- step writes hashed CSS/JS siblings and `/app` reads the manifest instead of
59
+ Runtime assets are loaded through `static/app/asset-manifest.json`; the build
60
+ step writes hashed CSS/JS assets and `/app` reads the manifest instead of
55
61
  using manual `?v=` query strings.
56
62
 
57
63
  | File | Responsibility |
@@ -133,8 +139,8 @@ focus rings, skip link, keyboard command palette (⌘K / Ctrl-K).
133
139
 
134
140
  ## Validation
135
141
 
136
- - `npm run lint` (extended to cover `static/v3/**` via `scripts/lint_v3.mjs`).
137
- - `npm run build:assets` (writes hashed v3 assets and `asset-manifest.json`).
142
+ - `npm run lint` (covers the current frontend through `scripts/lint_frontend.mjs`).
143
+ - `npm run build:assets` (writes hashed `static/app` assets and `asset-manifest.json`).
138
144
  - `npm run test:visual` (`tests/visual/v3.spec.js` against the mock server, which
139
145
  serves `/app` and mocks the future API surfaces).
140
146
  - Browser-rendered smoke checks of every route in light and dark themes.
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "8.0.0"
29
+ __version__ = "8.1.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -15,6 +15,10 @@ DEFAULT_EMBEDDING_DIM = int(os.getenv("LATTICEAI_VECTOR_DIM", "384"))
15
15
  EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
16
16
 
17
17
 
18
+ def embedding_model_id(dim: int) -> str:
19
+ return f"lattice-local-hash-v1:{int(dim)}"
20
+
21
+
18
22
  def _tokenize(text: str) -> List[str]:
19
23
  raw = str(text or "").lower()
20
24
  tokens = re.findall(r"[a-z0-9][a-z0-9_.:/+-]{1,}|[가-힣]{2,}", raw)
@@ -49,6 +53,10 @@ class LocalEmbeddingModel:
49
53
  dim: int = DEFAULT_EMBEDDING_DIM
50
54
  model_id: str = EMBEDDING_MODEL_ID
51
55
 
56
+ def __post_init__(self) -> None:
57
+ if self.model_id == EMBEDDING_MODEL_ID and self.dim != DEFAULT_EMBEDDING_DIM:
58
+ object.__setattr__(self, "model_id", embedding_model_id(self.dim))
59
+
52
60
  def embed(self, text: str) -> List[float]:
53
61
  vector = [0.0] * self.dim
54
62
  features = _tokenize(text)
@@ -79,4 +87,4 @@ class LocalEmbeddingModel:
79
87
  return list(struct.unpack(f"<{count}f", payload[: count * 4]))
80
88
 
81
89
 
82
- __all__ = ["DEFAULT_EMBEDDING_DIM", "EMBEDDING_MODEL_ID", "LocalEmbeddingModel"]
90
+ __all__ = ["DEFAULT_EMBEDDING_DIM", "EMBEDDING_MODEL_ID", "LocalEmbeddingModel", "embedding_model_id"]
@@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, List, Optional
21
21
  from .contracts import multi_agent_contract
22
22
 
23
23
 
24
- MULTI_AGENT_VERSION = "8.0.0"
24
+ MULTI_AGENT_VERSION = "8.1.0"
25
25
 
26
26
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
27
27
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "8.0.0"
3
+ __version__ = "8.1.0"
@@ -77,8 +77,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
77
77
  import re
78
78
  import secrets
79
79
  import threading
80
- import subprocess
81
- import sys
82
80
  import time
83
81
  from pathlib import Path
84
82
 
@@ -195,8 +193,10 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
195
193
  )
196
194
  from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF # noqa: F401
197
195
  from latticeai.core.mcp_registry import (
198
- _get_combined_registry,
199
- _fetch_skills_marketplace, install_skill, SKILLS_DIR,
196
+ _fetch_skills_marketplace,
197
+ install_skill,
198
+ SKILLS_DIR,
199
+ create_mcp_install_state,
200
200
  )
201
201
  from latticeai.services.p_reinforce import PReinforceGardener
202
202
  from setup_wizard import get_recommendations, scan_environment
@@ -322,6 +322,15 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
322
322
  MCP_FILE = DATA_DIR / "mcp_installs.json"
323
323
  AUDIT_FILE = DATA_DIR / "audit_log.json"
324
324
  SSO_FILE = DATA_DIR / "sso_config.json"
325
+
326
+ # MCP state extracted to mcp_registry.create_mcp_install_state (server decomp)
327
+ _mcp_state = create_mcp_install_state(DATA_DIR)
328
+ load_mcp_installs = _mcp_state["load_mcp_installs"]
329
+ save_mcp_installs = _mcp_state["save_mcp_installs"]
330
+ mcp_public_item = _mcp_state["mcp_public_item"]
331
+ recommend_mcps = _mcp_state["recommend_mcps"]
332
+ install_mcp = _mcp_state["install_mcp"]
333
+
325
334
  # Resolve the configured embedding provider once at startup. Degrades to the
326
335
  # offline hash fallback when the requested provider is unavailable, while
327
336
  # recording the requested-vs-active provider for the Embeddings status surface.
@@ -546,136 +555,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
546
555
  with open(VPC_FILE, "w", encoding="utf-8") as f:
547
556
  json.dump(config, f, ensure_ascii=False, indent=2)
548
557
 
549
- def load_mcp_installs() -> Dict:
550
- if not os.path.exists(MCP_FILE):
551
- return {"installed": {}, "updated_at": None}
552
- try:
553
- with open(MCP_FILE, "r", encoding="utf-8") as f:
554
- data = json.load(f)
555
- if "installed" not in data:
556
- data["installed"] = {}
557
- return data
558
- except Exception as e:
559
- logging.warning("load_mcp_installs failed: %s", e)
560
- return {"installed": {}, "updated_at": None}
561
-
562
- def save_mcp_installs(data: Dict):
563
- data["updated_at"] = datetime.now().isoformat()
564
- with open(MCP_FILE, "w", encoding="utf-8") as f:
565
- json.dump(data, f, ensure_ascii=False, indent=2)
566
-
567
- def mcp_public_item(item: Dict, installed_state: Dict) -> Dict:
568
- state = installed_state.get(item["id"]) or {}
569
- installed = item["install_mode"] in {"builtin", "bundled"} or bool(state.get("installed"))
570
- connector_pending = item["install_mode"] == "connector" and not state.get("authenticated")
571
- authenticated = item["install_mode"] != "connector" or bool(state.get("authenticated"))
572
- return {
573
- "id": item["id"],
574
- "name": item["name"],
575
- "category": item.get("category", ""),
576
- "install_mode": item["install_mode"],
577
- "description": item.get("description", ""),
578
- "capabilities": item.get("capabilities", []),
579
- "connector_url": item.get("connector_url"),
580
- "external_url": item.get("external_url"),
581
- "package": item.get("package"),
582
- "homepage": item.get("homepage"),
583
- "source": item.get("source", "local"),
584
- "installed": installed,
585
- "status": state.get("status") or ("active" if installed and not connector_pending else "needs_auth" if connector_pending else "available"),
586
- "authenticated": authenticated,
587
- "updated_at": state.get("updated_at"),
588
- }
589
-
590
- async def recommend_mcps(query: str, limit: int = 5) -> List[Dict]:
591
- text = (query or "").lower()
592
- installed = load_mcp_installs().get("installed", {})
593
- registry = await _get_combined_registry()
594
- scored = []
595
- for item in registry:
596
- score = 0
597
- hits = []
598
- for keyword in item.get("keywords", []):
599
- if keyword.lower() in text:
600
- score += 3 if len(keyword) > 2 else 1
601
- hits.append(keyword)
602
- # description 키워드 매칭 (remote 항목 보완)
603
- if not hits and text:
604
- desc_words = item.get("description", "").lower().split()
605
- for word in text.split():
606
- if len(word) > 2 and word in desc_words:
607
- score += 1
608
- hits.append(word)
609
- if item["id"] == "filesystem" and any(word in text for word in ["만들", "구현", "build", "deploy", "코드", "앱"]):
610
- score += 2
611
- if score:
612
- public = mcp_public_item(item, installed)
613
- public["score"] = score
614
- public["matched_keywords"] = hits[:6]
615
- scored.append(public)
616
- if not scored:
617
- fallback_ids = ["filesystem", "browser", "documents"]
618
- scored = [
619
- {**mcp_public_item(item, installed), "score": 1, "matched_keywords": []}
620
- for item in registry
621
- if item["id"] in fallback_ids
622
- ]
623
- return sorted(scored, key=lambda item: item["score"], reverse=True)[: max(1, min(limit, 24))]
624
-
625
- async def install_mcp(mcp_id: str) -> Dict:
626
- registry = await _get_combined_registry()
627
- item = next((entry for entry in registry if entry["id"] == mcp_id), None)
628
- if not item:
629
- raise HTTPException(status_code=404, detail="MCP를 찾을 수 없습니다.")
630
- data = load_mcp_installs()
631
- state = data.setdefault("installed", {})
632
- status = "active"
633
- message = "MCP가 활성화되었습니다."
634
- if item["install_mode"] == "connector":
635
- status = "needs_auth"
636
- message = "커넥터 인증이 필요합니다. Codex 앱의 connector 설정에서 계정을 연결하면 바로 사용할 수 있습니다."
637
- elif item["install_mode"] == "pip":
638
- packages = item.get("pip_packages") or []
639
- for pkg in packages:
640
- completed = subprocess.run(
641
- [sys.executable, "-m", "pip", "install", "--upgrade", pkg],
642
- capture_output=True, text=True, timeout=900, check=False,
643
- )
644
- if completed.returncode != 0:
645
- raise HTTPException(status_code=500, detail=completed.stderr[-2000:] or f"{pkg} 설치 실패")
646
- message = f"필수 패키지 설치 완료: {', '.join(packages)}"
647
- elif item["install_mode"] == "pypi":
648
- pkg = item.get("package", "")
649
- version = item.get("package_version")
650
- pkg_str = f"{pkg}=={version}" if version else pkg
651
- completed = subprocess.run(
652
- [sys.executable, "-m", "pip", "install", pkg_str],
653
- capture_output=True, text=True, timeout=300, check=False,
654
- )
655
- if completed.returncode != 0:
656
- raise HTTPException(status_code=500, detail=completed.stderr[-2000:] or f"{pkg} 설치 실패")
657
- message = f"pip 패키지 설치 완료: {pkg_str}"
658
- elif item["install_mode"] == "npm":
659
- pkg = item.get("package", "")
660
- version = item.get("package_version")
661
- pkg_str = f"{pkg}@{version}" if version else pkg
662
- completed = subprocess.run(
663
- ["npm", "install", "-g", pkg_str],
664
- capture_output=True, text=True, timeout=300, check=False,
665
- )
666
- if completed.returncode != 0:
667
- raise HTTPException(status_code=500, detail=completed.stderr[-2000:] or f"{pkg} 설치 실패")
668
- message = f"npm 패키지 설치 완료: {pkg_str}"
669
- state[mcp_id] = {
670
- "installed": True,
671
- "status": status,
672
- "authenticated": item["install_mode"] != "connector",
673
- "updated_at": datetime.now().isoformat(),
674
- }
675
- save_mcp_installs(data)
676
- public = mcp_public_item(item, state)
677
- public["message"] = message
678
- return public
679
558
 
680
559
  _history_lock = threading.Lock()
681
560
 
@@ -23,6 +23,10 @@ DEFAULT_EMBEDDING_DIM = 384
23
23
  EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
24
24
 
25
25
 
26
+ def embedding_model_id(dim: int) -> str:
27
+ return f"lattice-local-hash-v1:{int(dim)}"
28
+
29
+
26
30
  def _tokenize(text: str) -> List[str]:
27
31
  raw = str(text or "").lower()
28
32
  tokens = re.findall(r"[a-z0-9][a-z0-9_.:/+-]{1,}|[가-힣]{2,}", raw)
@@ -57,6 +61,10 @@ class LocalEmbeddingModel:
57
61
  dim: int = DEFAULT_EMBEDDING_DIM
58
62
  model_id: str = EMBEDDING_MODEL_ID
59
63
 
64
+ def __post_init__(self) -> None:
65
+ if self.model_id == EMBEDDING_MODEL_ID and self.dim != DEFAULT_EMBEDDING_DIM:
66
+ object.__setattr__(self, "model_id", embedding_model_id(self.dim))
67
+
60
68
  def embed(self, text: str) -> List[float]:
61
69
  vector = [0.0] * self.dim
62
70
  features = _tokenize(text)
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "8.0.0"
14
+ MARKETPLACE_VERSION = "8.1.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -6,9 +6,12 @@ Extracted from server.py to reduce module size.
6
6
 
7
7
  import json
8
8
  import logging
9
+ import os
10
+ import subprocess
11
+ import sys
9
12
  from datetime import datetime, timedelta
10
13
  from pathlib import Path
11
- from typing import Dict, List, Optional
14
+ from typing import Callable, Dict, List, Optional
12
15
 
13
16
  import httpx
14
17
  from fastapi import HTTPException
@@ -789,3 +792,139 @@ async def install_skill(plugin: str, skill: str) -> Dict:
789
792
  "license": entry["license"],
790
793
  "author": entry["author"],
791
794
  }
795
+
796
+
797
+ def create_mcp_install_state(data_dir: Path) -> Dict[str, Callable]:
798
+ """Return bound MCP state helpers for given data_dir. No global side effects."""
799
+ MCP_FILE = Path(data_dir) / "mcp_installs.json"
800
+
801
+ def load_mcp_installs() -> Dict:
802
+ if not os.path.exists(MCP_FILE):
803
+ return {"installed": {}, "updated_at": None}
804
+ try:
805
+ with open(MCP_FILE, "r", encoding="utf-8") as f:
806
+ data = json.load(f)
807
+ if "installed" not in data:
808
+ data["installed"] = {}
809
+ return data
810
+ except Exception as e:
811
+ logging.warning("load_mcp_installs failed: %s", e)
812
+ return {"installed": {}, "updated_at": None}
813
+
814
+ def save_mcp_installs(data: Dict) -> None:
815
+ data["updated_at"] = datetime.now().isoformat()
816
+ with open(MCP_FILE, "w", encoding="utf-8") as f:
817
+ json.dump(data, f, ensure_ascii=False, indent=2)
818
+
819
+ def mcp_public_item(item: Dict, installed_state: Dict) -> Dict:
820
+ state = installed_state.get(item.get("id"), {}) or {}
821
+ installed = item.get("install_mode") in {"builtin", "bundled"} or bool(state.get("installed"))
822
+ connector_pending = item.get("install_mode") == "connector" and not state.get("authenticated")
823
+ authenticated = item.get("install_mode") != "connector" or bool(state.get("authenticated"))
824
+ return {
825
+ "id": item.get("id"),
826
+ "name": item.get("name"),
827
+ "category": item.get("category", ""),
828
+ "install_mode": item.get("install_mode"),
829
+ "description": item.get("description", ""),
830
+ "capabilities": item.get("capabilities", []),
831
+ "connector_url": item.get("connector_url"),
832
+ "external_url": item.get("external_url"),
833
+ "package": item.get("package"),
834
+ "homepage": item.get("homepage"),
835
+ "source": item.get("source", "local"),
836
+ "installed": installed,
837
+ "status": state.get("status") or ("active" if installed and not connector_pending else "needs_auth" if connector_pending else "available"),
838
+ "authenticated": authenticated,
839
+ "updated_at": state.get("updated_at"),
840
+ }
841
+
842
+ async def recommend_mcps(query: str, limit: int = 5) -> List[Dict]:
843
+ text = (query or "").lower()
844
+ installed = load_mcp_installs().get("installed", {})
845
+ registry = await _get_combined_registry()
846
+ scored: List[Dict] = []
847
+ for item in registry:
848
+ score = 0
849
+ hits: List[str] = []
850
+ for keyword in item.get("keywords", []):
851
+ if keyword.lower() in text:
852
+ score += 3 if len(keyword) > 2 else 1
853
+ hits.append(keyword)
854
+ if not hits and text:
855
+ desc_words = item.get("description", "").lower().split()
856
+ for word in text.split():
857
+ if len(word) > 2 and word in desc_words:
858
+ score += 1
859
+ hits.append(word)
860
+ if item.get("id") == "filesystem" and any(w in text for w in ["만들", "구현", "build", "deploy", "코드", "앱"]):
861
+ score += 2
862
+ if score:
863
+ public = mcp_public_item(item, installed)
864
+ public["score"] = score
865
+ public["matched_keywords"] = hits[:6]
866
+ scored.append(public)
867
+ if not scored:
868
+ fallback_ids = ["filesystem", "browser", "documents"]
869
+ scored = [
870
+ {**mcp_public_item(item, installed), "score": 1, "matched_keywords": []}
871
+ for item in registry if item.get("id") in fallback_ids
872
+ ]
873
+ return sorted(scored, key=lambda x: x["score"], reverse=True)[:max(1, min(limit, 24))]
874
+
875
+ async def install_mcp(mcp_id: str) -> Dict:
876
+ registry = await _get_combined_registry()
877
+ item = next((entry for entry in registry if entry.get("id") == mcp_id), None)
878
+ if not item:
879
+ raise HTTPException(status_code=404, detail="MCP를 찾을 수 없습니다.")
880
+ data = load_mcp_installs()
881
+ state = data.setdefault("installed", {})
882
+ status = "active"
883
+ message = "MCP가 활성화되었습니다."
884
+ if item.get("install_mode") == "connector":
885
+ status = "needs_auth"
886
+ message = "커넥터 인증이 필요합니다. Codex 앱의 connector 설정에서 계정을 연결하면 바로 사용할 수 있습니다."
887
+ elif item.get("install_mode") == "pip":
888
+ packages = item.get("pip_packages") or []
889
+ for pkg in packages:
890
+ completed = subprocess.run(
891
+ [sys.executable, "-m", "pip", "install", "--upgrade", pkg],
892
+ capture_output=True, text=True, timeout=900, check=False,
893
+ )
894
+ if completed.returncode != 0:
895
+ raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
896
+ message = f"필수 패키지 설치 완료: {', '.join(packages)}"
897
+ elif item.get("install_mode") == "pypi":
898
+ pkg = item.get("package", "")
899
+ version = item.get("package_version")
900
+ pkg_str = f"{pkg}=={version}" if version else pkg
901
+ completed = subprocess.run([sys.executable, "-m", "pip", "install", pkg_str], capture_output=True, text=True, timeout=300, check=False)
902
+ if completed.returncode != 0:
903
+ raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
904
+ message = f"pip 패키지 설치 완료: {pkg_str}"
905
+ elif item.get("install_mode") == "npm":
906
+ pkg = item.get("package", "")
907
+ version = item.get("package_version")
908
+ pkg_str = f"{pkg}@{version}" if version else pkg
909
+ completed = subprocess.run(["npm", "install", "-g", pkg_str], capture_output=True, text=True, timeout=300, check=False)
910
+ if completed.returncode != 0:
911
+ raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
912
+ message = f"npm 패키지 설치 완료: {pkg_str}"
913
+ state[mcp_id] = {
914
+ "installed": True,
915
+ "status": status,
916
+ "authenticated": item.get("install_mode") != "connector",
917
+ "updated_at": datetime.now().isoformat(),
918
+ }
919
+ save_mcp_installs(data)
920
+ public = mcp_public_item(item, state)
921
+ public["message"] = message
922
+ return public
923
+
924
+ return {
925
+ "load_mcp_installs": load_mcp_installs,
926
+ "save_mcp_installs": save_mcp_installs,
927
+ "mcp_public_item": mcp_public_item,
928
+ "recommend_mcps": recommend_mcps,
929
+ "install_mcp": install_mcp,
930
+ }
@@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, Iterable, List, Optional
21
21
  from lattice_brain.runtime.contracts import realtime_event_contract, run_record_contract, workflow_run_contract
22
22
 
23
23
 
24
- WORKSPACE_OS_VERSION = "8.0.0"
24
+ WORKSPACE_OS_VERSION = "8.1.0"
25
25
 
26
26
  # Workspace types separate single-user Personal workspaces from shared
27
27
  # Organization workspaces. Both keep the same local-first JSON store; the type