ltcai 11.2.0 → 11.4.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.
- package/README.md +46 -53
- package/docs/CHANGELOG.md +61 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/multimodal/images.py +498 -0
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +9 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +31 -2
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1525
- package/lattice_brain/multimodal.py +0 -1258
- package/latticeai/core/agent.py +0 -1465
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1450
- package/latticeai/services/brain_intelligence.py +0 -1083
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1310
- package/static/app/assets/Act-AWf0SAKp.js +0 -1
- package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
- package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
- package/static/app/assets/Capture-CqOSzyPr.js +0 -1
- package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
- package/static/app/assets/Library-CX-bbhmK.js +0 -1
- package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
- package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
- package/static/app/assets/System-Bu2t5hn1.js +0 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
- package/static/app/assets/brain-DJMoqrwx.js +0 -1
- package/static/app/assets/index-BpYkzcVm.js +0 -10
- package/static/app/assets/input-DSlJJxRs.js +0 -1
- package/static/app/assets/primitives-BCx6TvfG.js +0 -1
- package/static/app/assets/search-Cgy8cCFJ.js +0 -1
- package/static/app/assets/utils-zqPZJxdx.js +0 -4
- package/static/app/assets/workspace-DXTihhfU.js +0 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Verification, repair, and the SSE install stream.
|
|
2
|
+
|
|
3
|
+
The acting half of the wizard: prove a binary or module is really there, try a
|
|
4
|
+
bounded automatic repair when it is not, and stream one SSE event per step
|
|
5
|
+
while pip/brew/model-load/auth actions run. Every subprocess goes through the
|
|
6
|
+
process-audit command plan, and every plan is confirmed against the token the
|
|
7
|
+
caller returned.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, AsyncIterator, Dict, List, Tuple
|
|
20
|
+
|
|
21
|
+
from latticeai.core.quiet import quiet
|
|
22
|
+
from latticeai.services.process_audit import (
|
|
23
|
+
CommandConfirmationError,
|
|
24
|
+
append_process_audit_event,
|
|
25
|
+
command_plan,
|
|
26
|
+
require_command_confirmation,
|
|
27
|
+
)
|
|
28
|
+
from latticeai.setup.wizard.paths import (
|
|
29
|
+
_module_available,
|
|
30
|
+
_package_module,
|
|
31
|
+
_which_any,
|
|
32
|
+
repair_path_for,
|
|
33
|
+
)
|
|
34
|
+
from latticeai.setup.wizard.plans import _sse, _verify_action_confirmation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _verify_binary(binary: str, version_args: List[str] | None = None, timeout: int = 20) -> Tuple[bool, str]:
|
|
38
|
+
repair_path_for(binary)
|
|
39
|
+
found = _which_any(binary)
|
|
40
|
+
if not found:
|
|
41
|
+
return False, f"{binary} 실행 파일을 PATH에서 찾지 못했습니다."
|
|
42
|
+
args = [found, *(version_args or ["--version"])]
|
|
43
|
+
try:
|
|
44
|
+
completed = subprocess.run(args, capture_output=True, text=True, timeout=timeout, check=False)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return False, str(e)
|
|
47
|
+
output = (completed.stdout or completed.stderr or "").strip().splitlines()
|
|
48
|
+
if completed.returncode == 0:
|
|
49
|
+
return True, output[0] if output else found
|
|
50
|
+
return False, (completed.stderr or completed.stdout or f"returncode={completed.returncode}")[-400:]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _wait_for_binary(binary: str, seconds: int = 300) -> Tuple[bool, str]:
|
|
54
|
+
deadline = time.time() + seconds
|
|
55
|
+
while time.time() < deadline:
|
|
56
|
+
ok, msg = _verify_binary(binary)
|
|
57
|
+
if ok:
|
|
58
|
+
return True, msg
|
|
59
|
+
await asyncio.sleep(2)
|
|
60
|
+
return False, f"{binary} 설치 완료를 제한 시간 안에 감지하지 못했습니다."
|
|
61
|
+
|
|
62
|
+
# ── Installation Stream ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
def _verify_action(action: Dict[str, Any]) -> Tuple[bool, str]:
|
|
65
|
+
atype = action.get("type")
|
|
66
|
+
if atype == "pip":
|
|
67
|
+
modules = action.get("verify_modules") or [_package_module(pkg) for pkg in action.get("packages", [])]
|
|
68
|
+
missing = [module for module in modules if not _module_available(module)]
|
|
69
|
+
if missing:
|
|
70
|
+
return False, "Python 모듈 감지 실패: " + ", ".join(missing)
|
|
71
|
+
return True, "Python 모듈 import 테스트 통과"
|
|
72
|
+
binary = action.get("binary")
|
|
73
|
+
if binary:
|
|
74
|
+
return _verify_binary(binary)
|
|
75
|
+
return True, "검증 항목 없음"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def _repair_action(
|
|
79
|
+
action: Dict[str, Any],
|
|
80
|
+
*,
|
|
81
|
+
confirmation_token: str | None = None,
|
|
82
|
+
actor: str | None = None,
|
|
83
|
+
) -> Tuple[bool, str]:
|
|
84
|
+
binary = action.get("binary")
|
|
85
|
+
if binary:
|
|
86
|
+
repair_path_for(binary)
|
|
87
|
+
ok, msg = _verify_binary(binary)
|
|
88
|
+
if ok:
|
|
89
|
+
return True, f"PATH 자동 보정 완료: {msg}"
|
|
90
|
+
if action.get("type") == "pip":
|
|
91
|
+
packages = action.get("packages", [])
|
|
92
|
+
if packages:
|
|
93
|
+
if not _verify_action_confirmation(action, confirmation_token, name="repair_action"):
|
|
94
|
+
return False, "설치 명령 확인 토큰이 일치하지 않습니다."
|
|
95
|
+
for pkg in packages:
|
|
96
|
+
success, err = await _pip_install(pkg, confirmed=True, actor=actor)
|
|
97
|
+
if not success:
|
|
98
|
+
return False, err
|
|
99
|
+
return _verify_action(action)
|
|
100
|
+
return False, "자동 복구 방법을 찾지 못했습니다."
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def install_stream(
|
|
104
|
+
items: List[Dict],
|
|
105
|
+
router: Any,
|
|
106
|
+
*,
|
|
107
|
+
confirmation_token: str | None = None,
|
|
108
|
+
user_email: str | None = None,
|
|
109
|
+
) -> AsyncIterator[str]:
|
|
110
|
+
for item in items:
|
|
111
|
+
item_id = item.get("id", "unknown")
|
|
112
|
+
name = item.get("name", item_id)
|
|
113
|
+
action = item.get("action") or {}
|
|
114
|
+
atype = action.get("type")
|
|
115
|
+
|
|
116
|
+
if not atype:
|
|
117
|
+
yield _sse({"id": item_id, "status": "skipped", "msg": f"{name} — 이미 준비됨"})
|
|
118
|
+
await asyncio.sleep(0.04)
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
yield _sse({"id": item_id, "status": "starting", "msg": f"{name} 준비 중..."})
|
|
122
|
+
|
|
123
|
+
if atype == "pip":
|
|
124
|
+
packages = action.get("packages", [])
|
|
125
|
+
token = confirmation_token or action.get("confirmation_token") or (action.get("command_plan") or {}).get("confirmation_token")
|
|
126
|
+
if not _verify_action_confirmation(action, token, name=str(item_id)):
|
|
127
|
+
yield _sse({"id": item_id, "status": "error", "msg": "설치 명령 확인 토큰이 일치하지 않습니다."})
|
|
128
|
+
continue
|
|
129
|
+
ok = True
|
|
130
|
+
for pkg in packages:
|
|
131
|
+
yield _sse({"id": item_id, "status": "running", "msg": f"pip install {pkg} ..."})
|
|
132
|
+
success, err = await _pip_install(pkg, confirmed=True, actor=user_email)
|
|
133
|
+
if success:
|
|
134
|
+
yield _sse({"id": item_id, "status": "progress", "msg": f"{pkg} 설치 완료"})
|
|
135
|
+
else:
|
|
136
|
+
yield _sse({"id": item_id, "status": "error", "msg": f"{pkg} 실패: {err[:400]}"})
|
|
137
|
+
ok = False
|
|
138
|
+
break
|
|
139
|
+
if ok:
|
|
140
|
+
yield _sse({"id": item_id, "status": "running", "msg": f"{name} 동작 테스트 중..."})
|
|
141
|
+
verified, detail = _verify_action(action)
|
|
142
|
+
if verified:
|
|
143
|
+
yield _sse({"id": item_id, "status": "done", "msg": f"{name} 설치 · 검증 완료 ✅\n{detail}"})
|
|
144
|
+
else:
|
|
145
|
+
yield _sse({"id": item_id, "status": "running", "msg": f"검증 실패 — 자동 복구 중...\n{detail}"})
|
|
146
|
+
repaired, repair_msg = await _repair_action(action, confirmation_token=token, actor=user_email)
|
|
147
|
+
yield _sse({"id": item_id, "status": "done" if repaired else "error", "msg": repair_msg[:500]})
|
|
148
|
+
|
|
149
|
+
elif atype == "brew":
|
|
150
|
+
pkg = action.get("package", "")
|
|
151
|
+
token = confirmation_token or action.get("confirmation_token") or (action.get("command_plan") or {}).get("confirmation_token")
|
|
152
|
+
if not _verify_action_confirmation(action, token, name=str(item_id)):
|
|
153
|
+
yield _sse({"id": item_id, "status": "error", "msg": "설치 명령 확인 토큰이 일치하지 않습니다."})
|
|
154
|
+
continue
|
|
155
|
+
yield _sse({"id": item_id, "status": "running", "msg": f"brew install {pkg} ..."})
|
|
156
|
+
success, err = await _brew_install(pkg, confirmed=True, actor=user_email)
|
|
157
|
+
if success:
|
|
158
|
+
yield _sse({"id": item_id, "status": "running", "msg": "설치 완료 감지 · PATH 보정 중..."})
|
|
159
|
+
binary = action.get("binary")
|
|
160
|
+
if binary:
|
|
161
|
+
repair_path_for(binary)
|
|
162
|
+
verified, detail = _verify_action(action)
|
|
163
|
+
if verified:
|
|
164
|
+
yield _sse({"id": item_id, "status": "done", "msg": f"{name} 설치 · 연결 · 검증 완료 ✅\n{detail}"})
|
|
165
|
+
else:
|
|
166
|
+
yield _sse({"id": item_id, "status": "running", "msg": f"검증 실패 — 자동 복구 중...\n{detail}"})
|
|
167
|
+
repaired, repair_msg = await _repair_action(action, confirmation_token=token, actor=user_email)
|
|
168
|
+
yield _sse({"id": item_id, "status": "done" if repaired else "error", "msg": repair_msg[:500]})
|
|
169
|
+
else:
|
|
170
|
+
url = action.get("official_url") or action.get("url")
|
|
171
|
+
if url:
|
|
172
|
+
yield _sse({"id": item_id, "status": "auth", "msg": f"자동 설치 실패 — 공식 다운로드 페이지를 엽니다.\n{err[:240]}", "auth_url": url})
|
|
173
|
+
open_url(url)
|
|
174
|
+
yield _sse({"id": item_id, "status": "error", "msg": f"실패: {err[:400]}"})
|
|
175
|
+
|
|
176
|
+
elif atype == "load_model":
|
|
177
|
+
model_id = action.get("model_id", "")
|
|
178
|
+
yield _sse({"id": item_id, "status": "running",
|
|
179
|
+
"msg": f"모델 다운로드 · 로딩 중...\n{model_id}\n(용량에 따라 수 분 소요)"})
|
|
180
|
+
try:
|
|
181
|
+
await router.load_model(model_id)
|
|
182
|
+
yield _sse({"id": item_id, "status": "done", "msg": f"{name} 로드 완료 ✅"})
|
|
183
|
+
except Exception as e:
|
|
184
|
+
yield _sse({"id": item_id, "status": "error", "msg": f"로드 실패: {str(e)[:400]}"})
|
|
185
|
+
|
|
186
|
+
elif atype == "auth":
|
|
187
|
+
url = action.get("url", "")
|
|
188
|
+
yield _sse({"id": item_id, "status": "auth",
|
|
189
|
+
"msg": "브라우저에서 인증 페이지를 엽니다...", "auth_url": url})
|
|
190
|
+
open_url(url)
|
|
191
|
+
yield _sse({"id": item_id, "status": "waiting",
|
|
192
|
+
"msg": "브라우저에서 인증 완료 후 계속하세요"})
|
|
193
|
+
|
|
194
|
+
elif atype == "url":
|
|
195
|
+
url = action.get("url", "")
|
|
196
|
+
yield _sse({"id": item_id, "status": "auth",
|
|
197
|
+
"msg": "설치 페이지를 브라우저에서 엽니다...", "auth_url": url})
|
|
198
|
+
open_url(url)
|
|
199
|
+
binary = action.get("binary")
|
|
200
|
+
if binary:
|
|
201
|
+
yield _sse({"id": item_id, "status": "waiting",
|
|
202
|
+
"msg": f"{binary} 설치 완료를 자동 감지하는 중입니다..."})
|
|
203
|
+
ok, detail = await _wait_for_binary(binary)
|
|
204
|
+
if ok:
|
|
205
|
+
repair_path_for(binary)
|
|
206
|
+
yield _sse({"id": item_id, "status": "done",
|
|
207
|
+
"msg": f"{name} 설치 · PATH 연결 · 검증 완료 ✅\n{detail}"})
|
|
208
|
+
else:
|
|
209
|
+
yield _sse({"id": item_id, "status": "error",
|
|
210
|
+
"msg": f"{detail}\n공식 페이지에서 설치 후 다시 시도하세요."})
|
|
211
|
+
else:
|
|
212
|
+
yield _sse({"id": item_id, "status": "waiting",
|
|
213
|
+
"msg": "브라우저에서 설치 또는 인증을 완료한 뒤 다시 시도하세요"})
|
|
214
|
+
|
|
215
|
+
else:
|
|
216
|
+
yield _sse({"id": item_id, "status": "error", "msg": f"알 수 없는 액션: {atype}"})
|
|
217
|
+
|
|
218
|
+
yield _sse({"status": "complete", "msg": "모든 항목 처리 완료!"})
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def _pip_install(
|
|
222
|
+
package: str,
|
|
223
|
+
*,
|
|
224
|
+
confirmation_token: str | None = None,
|
|
225
|
+
confirmed: bool = False,
|
|
226
|
+
actor: str | None = None,
|
|
227
|
+
) -> Tuple[bool, str]:
|
|
228
|
+
command = [sys.executable, "-m", "pip", "install", "--upgrade", package]
|
|
229
|
+
plan = command_plan(
|
|
230
|
+
command,
|
|
231
|
+
name=f"pip:{package}",
|
|
232
|
+
purpose="setup_wizard_install",
|
|
233
|
+
metadata={"package": package},
|
|
234
|
+
)
|
|
235
|
+
try:
|
|
236
|
+
if not confirmed:
|
|
237
|
+
require_command_confirmation(command, confirmation_token, purpose="setup_wizard_install")
|
|
238
|
+
append_process_audit_event("setup_wizard_install", plan=plan, status="started", user_email=actor)
|
|
239
|
+
proc = await asyncio.create_subprocess_exec(
|
|
240
|
+
*command,
|
|
241
|
+
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
|
|
242
|
+
)
|
|
243
|
+
_, stderr = await asyncio.wait_for(proc.communicate(), timeout=600)
|
|
244
|
+
stderr_text = stderr.decode(errors="replace")
|
|
245
|
+
append_process_audit_event(
|
|
246
|
+
"setup_wizard_install",
|
|
247
|
+
plan=plan,
|
|
248
|
+
status="finished",
|
|
249
|
+
user_email=actor,
|
|
250
|
+
returncode=proc.returncode,
|
|
251
|
+
stderr=stderr_text,
|
|
252
|
+
)
|
|
253
|
+
if proc.returncode == 0:
|
|
254
|
+
return True, ""
|
|
255
|
+
return False, stderr_text
|
|
256
|
+
except CommandConfirmationError as e:
|
|
257
|
+
append_process_audit_event("setup_wizard_install", plan=plan, status="denied", user_email=actor, error=str(e))
|
|
258
|
+
return False, str(e)
|
|
259
|
+
except asyncio.TimeoutError:
|
|
260
|
+
append_process_audit_event("setup_wizard_install", plan=plan, status="timeout", user_email=actor)
|
|
261
|
+
return False, "설치 시간 초과 (10분)"
|
|
262
|
+
except Exception as e:
|
|
263
|
+
append_process_audit_event("setup_wizard_install", plan=plan, status="error", user_email=actor, error=str(e))
|
|
264
|
+
return False, str(e)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def _brew_install(
|
|
268
|
+
package: str,
|
|
269
|
+
*,
|
|
270
|
+
confirmation_token: str | None = None,
|
|
271
|
+
confirmed: bool = False,
|
|
272
|
+
actor: str | None = None,
|
|
273
|
+
) -> Tuple[bool, str]:
|
|
274
|
+
brew = shutil.which("brew")
|
|
275
|
+
if not brew:
|
|
276
|
+
return False, "Homebrew 미설치 — https://brew.sh 에서 설치하세요"
|
|
277
|
+
command = [brew, "install", package]
|
|
278
|
+
plan = command_plan(
|
|
279
|
+
command,
|
|
280
|
+
name=f"brew:{package}",
|
|
281
|
+
purpose="setup_wizard_install",
|
|
282
|
+
metadata={"package": package},
|
|
283
|
+
)
|
|
284
|
+
try:
|
|
285
|
+
if not confirmed:
|
|
286
|
+
require_command_confirmation(command, confirmation_token, purpose="setup_wizard_install")
|
|
287
|
+
append_process_audit_event("setup_wizard_install", plan=plan, status="started", user_email=actor)
|
|
288
|
+
proc = await asyncio.create_subprocess_exec(
|
|
289
|
+
*command,
|
|
290
|
+
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
|
|
291
|
+
)
|
|
292
|
+
_, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
|
|
293
|
+
stderr_text = stderr.decode(errors="replace")
|
|
294
|
+
append_process_audit_event(
|
|
295
|
+
"setup_wizard_install",
|
|
296
|
+
plan=plan,
|
|
297
|
+
status="finished",
|
|
298
|
+
user_email=actor,
|
|
299
|
+
returncode=proc.returncode,
|
|
300
|
+
stderr=stderr_text,
|
|
301
|
+
)
|
|
302
|
+
if proc.returncode == 0:
|
|
303
|
+
return True, ""
|
|
304
|
+
return False, stderr_text
|
|
305
|
+
except CommandConfirmationError as e:
|
|
306
|
+
append_process_audit_event("setup_wizard_install", plan=plan, status="denied", user_email=actor, error=str(e))
|
|
307
|
+
return False, str(e)
|
|
308
|
+
except asyncio.TimeoutError:
|
|
309
|
+
append_process_audit_event("setup_wizard_install", plan=plan, status="timeout", user_email=actor)
|
|
310
|
+
return False, "설치 시간 초과 (5분)"
|
|
311
|
+
except Exception as e:
|
|
312
|
+
append_process_audit_event("setup_wizard_install", plan=plan, status="error", user_email=actor, error=str(e))
|
|
313
|
+
return False, str(e)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def open_url(url: str) -> None:
|
|
317
|
+
command: List[str]
|
|
318
|
+
try:
|
|
319
|
+
system = platform.system()
|
|
320
|
+
if system == "Darwin":
|
|
321
|
+
command = ["open", url]
|
|
322
|
+
plan = command_plan(command, name="open_url", purpose="setup_wizard_open_url")
|
|
323
|
+
append_process_audit_event("setup_wizard_open_url", plan=plan, status="started")
|
|
324
|
+
subprocess.Popen(command)
|
|
325
|
+
append_process_audit_event("setup_wizard_open_url", plan=plan, status="spawned")
|
|
326
|
+
elif system == "Windows":
|
|
327
|
+
command = ["os.startfile", url]
|
|
328
|
+
plan = command_plan(command, name="open_url", purpose="setup_wizard_open_url")
|
|
329
|
+
append_process_audit_event("setup_wizard_open_url", plan=plan, status="started")
|
|
330
|
+
os.startfile(url) # type: ignore[attr-defined] # noqa: S606 — fixed program, arguments validated by the caller
|
|
331
|
+
append_process_audit_event("setup_wizard_open_url", plan=plan, status="spawned")
|
|
332
|
+
else:
|
|
333
|
+
command = ["xdg-open", url]
|
|
334
|
+
plan = command_plan(command, name="open_url", purpose="setup_wizard_open_url")
|
|
335
|
+
append_process_audit_event("setup_wizard_open_url", plan=plan, status="started")
|
|
336
|
+
subprocess.Popen(command)
|
|
337
|
+
append_process_audit_event("setup_wizard_open_url", plan=plan, status="spawned")
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
try:
|
|
340
|
+
append_process_audit_event(
|
|
341
|
+
"setup_wizard_open_url",
|
|
342
|
+
plan=command_plan(["open_url", url], name="open_url", purpose="setup_wizard_open_url"),
|
|
343
|
+
status="error",
|
|
344
|
+
error=str(exc),
|
|
345
|
+
)
|
|
346
|
+
except Exception:
|
|
347
|
+
quiet()
|
|
348
|
+
pass
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""PATH discovery, binary lookup, and the ``.env`` handoff for the wizard.
|
|
2
|
+
|
|
3
|
+
The base layer of the setup wizard: it knows where installers put things on
|
|
4
|
+
each platform, how to get those directories onto ``PATH`` for the running
|
|
5
|
+
process, and how to persist that repair into the project ``.env``. Nothing
|
|
6
|
+
here imports another wizard submodule, which is also what lets
|
|
7
|
+
``tests/unit/test_cov_wp04_wizard_detect.py`` re-execute this file under a
|
|
8
|
+
throwaway name to reach the Windows-only import-time branch.
|
|
9
|
+
|
|
10
|
+
``COMMON_PATH_DIRS`` is module-level state that the import-time Windows block
|
|
11
|
+
extends and tests replace, so it lives here and only here — the modules that
|
|
12
|
+
repair PATH call :func:`repair_path_for` rather than copying the list.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import platform
|
|
19
|
+
import shutil
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List
|
|
22
|
+
|
|
23
|
+
OFFICIAL_DOWNLOADS: Dict[str, str] = {
|
|
24
|
+
"homebrew": "https://brew.sh",
|
|
25
|
+
"python": "https://www.python.org/downloads/",
|
|
26
|
+
"node": "https://nodejs.org/en/download",
|
|
27
|
+
"git": "https://git-scm.com/downloads",
|
|
28
|
+
"ollama": "https://ollama.com/download",
|
|
29
|
+
"lmstudio": "https://lmstudio.ai/download",
|
|
30
|
+
"cuda": "https://developer.nvidia.com/cuda-downloads",
|
|
31
|
+
"mlx": "https://ml-explore.github.io/mlx/build/html/install.html",
|
|
32
|
+
"cloudflared": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
|
|
33
|
+
"tesseract": "https://tesseract-ocr.github.io/tessdoc/Installation.html",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
COMMON_PATH_DIRS = [
|
|
37
|
+
"/opt/homebrew/bin",
|
|
38
|
+
"/usr/local/bin",
|
|
39
|
+
"/usr/bin",
|
|
40
|
+
"/bin",
|
|
41
|
+
str(Path.home() / ".local" / "bin"),
|
|
42
|
+
str(Path.home() / ".cargo" / "bin"),
|
|
43
|
+
str(Path.home() / ".latticeai" / "bin"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
if platform.system() == "Windows":
|
|
47
|
+
_local_appdata = os.environ.get("LOCALAPPDATA", "")
|
|
48
|
+
_program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
|
|
49
|
+
_program_files_x86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
|
50
|
+
COMMON_PATH_DIRS.extend([
|
|
51
|
+
str(Path(_local_appdata) / "Programs" / "Ollama") if _local_appdata else "",
|
|
52
|
+
str(Path(_program_files) / "Ollama"),
|
|
53
|
+
str(Path(_program_files) / "LM Studio"),
|
|
54
|
+
str(Path(_program_files) / "NVIDIA Corporation" / "NVSMI"),
|
|
55
|
+
str(Path(_program_files_x86) / "NVIDIA Corporation" / "NVSMI"),
|
|
56
|
+
])
|
|
57
|
+
COMMON_PATH_DIRS = [p for p in COMMON_PATH_DIRS if p]
|
|
58
|
+
|
|
59
|
+
WINDOWS_BINARY_CANDIDATES: Dict[str, List[str]] = {
|
|
60
|
+
"ollama": [
|
|
61
|
+
str(Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "Ollama" / "ollama.exe"),
|
|
62
|
+
str(Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "Ollama" / "ollama.exe"),
|
|
63
|
+
],
|
|
64
|
+
"lms": [
|
|
65
|
+
str(Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "LM Studio" / "resources" / "app" / ".webpack" / "lms.exe"),
|
|
66
|
+
str(Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "LM Studio" / "resources" / "app" / ".webpack" / "lms.exe"),
|
|
67
|
+
],
|
|
68
|
+
"nvidia-smi": [
|
|
69
|
+
str(Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "NVIDIA Corporation" / "NVSMI" / "nvidia-smi.exe"),
|
|
70
|
+
str(Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")) / "NVIDIA Corporation" / "NVSMI" / "nvidia-smi.exe"),
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
PACKAGE_MODULES: Dict[str, str] = {
|
|
75
|
+
"mlx-vlm": "mlx_vlm",
|
|
76
|
+
"huggingface_hub[cli]": "huggingface_hub",
|
|
77
|
+
"openai-whisper": "whisper",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _project_env_file() -> Path:
|
|
82
|
+
return Path(__file__).resolve().parents[2] / ".env"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _update_env_file(env_file: Path, key: str, value: str) -> None:
|
|
86
|
+
lines: List[str] = []
|
|
87
|
+
found = False
|
|
88
|
+
if env_file.exists():
|
|
89
|
+
lines = env_file.read_text(encoding="utf-8").splitlines()
|
|
90
|
+
updated: List[str] = []
|
|
91
|
+
for line in lines:
|
|
92
|
+
if line.startswith(f"{key}="):
|
|
93
|
+
updated.append(f"{key}={value}")
|
|
94
|
+
found = True
|
|
95
|
+
else:
|
|
96
|
+
updated.append(line)
|
|
97
|
+
if not found:
|
|
98
|
+
updated.append(f"{key}={value}")
|
|
99
|
+
env_file.write_text("\n".join(updated) + "\n", encoding="utf-8")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _merge_path_dirs(dirs: List[str]) -> List[str]:
|
|
103
|
+
current = os.environ.get("PATH", "")
|
|
104
|
+
parts = [p for p in current.split(os.pathsep) if p]
|
|
105
|
+
for item in dirs:
|
|
106
|
+
expanded = str(Path(item).expanduser())
|
|
107
|
+
if Path(expanded).exists() and expanded not in parts:
|
|
108
|
+
parts.insert(0, expanded)
|
|
109
|
+
os.environ["PATH"] = os.pathsep.join(parts)
|
|
110
|
+
return parts
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _persist_extra_path(dirs: List[str]) -> None:
|
|
114
|
+
existing = [
|
|
115
|
+
p for p in os.environ.get("LATTICEAI_EXTRA_PATH", "").split(os.pathsep)
|
|
116
|
+
if p
|
|
117
|
+
]
|
|
118
|
+
merged = existing[:]
|
|
119
|
+
for item in dirs:
|
|
120
|
+
expanded = str(Path(item).expanduser())
|
|
121
|
+
if Path(expanded).exists() and expanded not in merged:
|
|
122
|
+
merged.append(expanded)
|
|
123
|
+
if merged:
|
|
124
|
+
os.environ["LATTICEAI_EXTRA_PATH"] = os.pathsep.join(merged)
|
|
125
|
+
_update_env_file(_project_env_file(), "LATTICEAI_EXTRA_PATH", os.environ["LATTICEAI_EXTRA_PATH"])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def repair_path_for(binary: str | None = None) -> List[str]:
|
|
129
|
+
before = _which_any(binary) if binary else None
|
|
130
|
+
paths = _merge_path_dirs(COMMON_PATH_DIRS)
|
|
131
|
+
if binary and not before and _which_any(binary):
|
|
132
|
+
_persist_extra_path(COMMON_PATH_DIRS)
|
|
133
|
+
return paths
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _which_any(binary: str) -> str | None:
|
|
137
|
+
path = shutil.which(binary)
|
|
138
|
+
if path:
|
|
139
|
+
return path
|
|
140
|
+
if platform.system() == "Windows":
|
|
141
|
+
for candidate in WINDOWS_BINARY_CANDIDATES.get(binary, []):
|
|
142
|
+
if candidate and Path(candidate).exists():
|
|
143
|
+
return candidate
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _which_detail(binary: str) -> Dict[str, Any]:
|
|
148
|
+
path = _which_any(binary)
|
|
149
|
+
return {"installed": path is not None, "path": path}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _module_available(module_name: str) -> bool:
|
|
153
|
+
import importlib.util
|
|
154
|
+
return importlib.util.find_spec(module_name) is not None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _package_module(package: str) -> str:
|
|
158
|
+
return PACKAGE_MODULES.get(package, package.replace("-", "_").split("[", 1)[0])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _component_detail(name: str, binary: str | None = None, module: str | None = None) -> Dict[str, Any]:
|
|
162
|
+
detail: Dict[str, Any] = {"official_url": OFFICIAL_DOWNLOADS.get(name)}
|
|
163
|
+
if binary:
|
|
164
|
+
detail.update(_which_detail(binary))
|
|
165
|
+
if module:
|
|
166
|
+
detail["module_available"] = _module_available(module)
|
|
167
|
+
detail["installed"] = bool(detail.get("installed") or detail["module_available"])
|
|
168
|
+
return detail
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""SSE framing and the confirmation-token plans for wizard install actions.
|
|
2
|
+
|
|
3
|
+
Every install action the wizard offers is turned into an explicit command plan
|
|
4
|
+
before it is shown, so the token the browser sends back can be compared against
|
|
5
|
+
the exact commands that would run. An action with nothing to execute has no
|
|
6
|
+
plan and therefore nothing to confirm.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json as _json
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Dict, List
|
|
14
|
+
|
|
15
|
+
from latticeai.services.process_audit import command_plan_for_commands
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _sse(data: Dict) -> str:
|
|
19
|
+
return f"data: {_json.dumps(data, ensure_ascii=False)}\n\n"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _action_commands(action: Dict[str, Any]) -> List[List[str]]:
|
|
23
|
+
atype = action.get("type")
|
|
24
|
+
if atype == "pip":
|
|
25
|
+
return [[sys.executable, "-m", "pip", "install", "--upgrade", str(pkg)] for pkg in action.get("packages", [])]
|
|
26
|
+
if atype == "brew":
|
|
27
|
+
package = str(action.get("package") or "")
|
|
28
|
+
return [["brew", "install", package]] if package else []
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _action_command_plan(action: Dict[str, Any], *, name: str) -> Dict[str, Any] | None:
|
|
33
|
+
commands = _action_commands(action)
|
|
34
|
+
if not commands:
|
|
35
|
+
return None
|
|
36
|
+
return command_plan_for_commands(
|
|
37
|
+
commands,
|
|
38
|
+
name=name,
|
|
39
|
+
purpose="setup_wizard_install",
|
|
40
|
+
metadata={"action_type": action.get("type")},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _attach_action_plan(action: Dict[str, Any] | None, *, name: str) -> Dict[str, Any] | None:
|
|
45
|
+
if not isinstance(action, dict):
|
|
46
|
+
return action
|
|
47
|
+
plan = _action_command_plan(action, name=name)
|
|
48
|
+
if not plan:
|
|
49
|
+
return action
|
|
50
|
+
hydrated = dict(action)
|
|
51
|
+
hydrated["command_plan"] = plan
|
|
52
|
+
hydrated["confirmation_token"] = plan["confirmation_token"]
|
|
53
|
+
return hydrated
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _hydrate_install_actions(groups: Dict[str, Any]) -> Dict[str, Any]:
|
|
57
|
+
for group_name in ("components", "engines", "models", "mcps"):
|
|
58
|
+
items = groups.get(group_name)
|
|
59
|
+
if not isinstance(items, list):
|
|
60
|
+
continue
|
|
61
|
+
for item in items:
|
|
62
|
+
if isinstance(item, dict):
|
|
63
|
+
item["action"] = _attach_action_plan(
|
|
64
|
+
item.get("action"),
|
|
65
|
+
name=str(item.get("id") or item.get("name") or group_name),
|
|
66
|
+
)
|
|
67
|
+
return groups
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _verify_action_confirmation(action: Dict[str, Any], token: str | None, *, name: str) -> bool:
|
|
71
|
+
plan = _action_command_plan(action, name=name)
|
|
72
|
+
if not plan:
|
|
73
|
+
return True
|
|
74
|
+
return str(token or "").strip() == plan["confirmation_token"]
|