ltcai 8.8.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +33 -37
  2. package/auto_setup.py +84 -70
  3. package/docs/CHANGELOG.md +113 -237
  4. package/docs/CODE_REVIEW_2026-07-06.md +764 -0
  5. package/docs/COMMUNITY_AND_PLUGINS.md +3 -3
  6. package/docs/DEVELOPMENT.md +10 -10
  7. package/docs/LEGACY_COMPATIBILITY.md +1 -1
  8. package/docs/ONBOARDING.md +2 -2
  9. package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
  10. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  11. package/docs/TRUST_MODEL.md +5 -1
  12. package/docs/WHY_LATTICE.md +4 -3
  13. package/docs/architecture.md +4 -0
  14. package/docs/kg-schema.md +1 -1
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/archive.py +4 -9
  17. package/lattice_brain/conversations.py +156 -21
  18. package/lattice_brain/embeddings.py +38 -2
  19. package/lattice_brain/graph/_kg_common.py +39 -496
  20. package/lattice_brain/graph/_kg_constants.py +243 -0
  21. package/lattice_brain/graph/_kg_fsutil.py +297 -0
  22. package/lattice_brain/graph/discovery.py +0 -948
  23. package/lattice_brain/graph/discovery_index.py +972 -0
  24. package/lattice_brain/graph/json_utils.py +25 -0
  25. package/lattice_brain/graph/retrieval.py +66 -597
  26. package/lattice_brain/graph/retrieval_docgen.py +210 -0
  27. package/lattice_brain/graph/retrieval_vector.py +460 -0
  28. package/lattice_brain/graph/runtime.py +16 -0
  29. package/lattice_brain/graph/store.py +6 -0
  30. package/lattice_brain/ingestion.py +68 -0
  31. package/lattice_brain/portability.py +1 -9
  32. package/lattice_brain/quality.py +98 -4
  33. package/lattice_brain/runtime/agent_runtime.py +166 -0
  34. package/lattice_brain/runtime/multi_agent.py +1 -1
  35. package/lattice_brain/utils.py +28 -0
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/chat.py +368 -418
  38. package/latticeai/api/chat_helpers.py +227 -0
  39. package/latticeai/api/computer_use.py +149 -31
  40. package/latticeai/api/marketplace.py +11 -0
  41. package/latticeai/api/mcp.py +3 -2
  42. package/latticeai/api/models.py +4 -1
  43. package/latticeai/api/permissions.py +72 -33
  44. package/latticeai/api/setup.py +17 -2
  45. package/latticeai/api/tools.py +105 -62
  46. package/latticeai/app_factory.py +101 -296
  47. package/latticeai/core/agent.py +25 -7
  48. package/latticeai/core/io_utils.py +37 -0
  49. package/latticeai/core/legacy_compatibility.py +1 -1
  50. package/latticeai/core/local_embeddings.py +2 -4
  51. package/latticeai/core/marketplace.py +33 -2
  52. package/latticeai/core/mcp_catalog.py +450 -0
  53. package/latticeai/core/mcp_registry.py +2 -441
  54. package/latticeai/core/sessions.py +11 -3
  55. package/latticeai/core/tool_registry.py +15 -4
  56. package/latticeai/core/users.py +4 -9
  57. package/latticeai/core/workspace_os.py +1 -1
  58. package/latticeai/core/workspace_os_utils.py +3 -17
  59. package/latticeai/integrations/telegram_bot.py +7 -2
  60. package/latticeai/models/model_providers.py +111 -0
  61. package/latticeai/models/router.py +58 -136
  62. package/latticeai/runtime/audit_runtime.py +27 -16
  63. package/latticeai/runtime/automation_runtime.py +9 -0
  64. package/latticeai/runtime/bootstrap.py +1 -1
  65. package/latticeai/runtime/history_runtime.py +163 -0
  66. package/latticeai/runtime/namespace_runtime.py +173 -0
  67. package/latticeai/runtime/network_config_runtime.py +56 -0
  68. package/latticeai/runtime/sso_config_runtime.py +128 -0
  69. package/latticeai/runtime/user_key_runtime.py +106 -0
  70. package/latticeai/services/app_context.py +1 -0
  71. package/latticeai/services/architecture_readiness.py +2 -2
  72. package/latticeai/services/memory_service.py +213 -0
  73. package/latticeai/services/model_engines.py +79 -12
  74. package/latticeai/services/model_runtime.py +24 -4
  75. package/latticeai/services/platform_runtime.py +9 -1
  76. package/latticeai/services/process_audit.py +208 -0
  77. package/latticeai/services/product_readiness.py +11 -11
  78. package/latticeai/services/review_queue.py +64 -11
  79. package/latticeai/services/run_executor.py +21 -0
  80. package/latticeai/services/search_service.py +106 -30
  81. package/latticeai/services/setup_detection.py +80 -0
  82. package/latticeai/services/tool_dispatch.py +66 -0
  83. package/latticeai/services/workspace_service.py +15 -0
  84. package/package.json +1 -1
  85. package/scripts/check_i18n_literals.mjs +20 -8
  86. package/scripts/i18n_literal_allowlist.json +34 -0
  87. package/scripts/lint_frontend.mjs +6 -2
  88. package/setup_wizard.py +196 -74
  89. package/src-tauri/Cargo.lock +1 -1
  90. package/src-tauri/Cargo.toml +1 -1
  91. package/src-tauri/tauri.conf.json +1 -1
  92. package/static/app/asset-manifest.json +11 -11
  93. package/static/app/assets/{Act-C7K9wsO9.js → Act-21lIXx2E.js} +1 -1
  94. package/static/app/assets/Brain-BqUd5UJJ.js +321 -0
  95. package/static/app/assets/{Capture-B3V4_5Xp.js → Capture-BA7Z2Q1u.js} +1 -1
  96. package/static/app/assets/{Library-Cgj-EF50.js → Library-bFMtyni3.js} +1 -1
  97. package/static/app/assets/System-K6krGCqn.js +1 -0
  98. package/static/app/assets/index-C4R3ws30.js +17 -0
  99. package/static/app/assets/index-ChSeOB02.css +2 -0
  100. package/static/app/assets/primitives-sQU3it5I.js +1 -0
  101. package/static/app/assets/{textarea-CVQkN2Tk.js → textarea-DK3Fd_lR.js} +1 -1
  102. package/static/app/index.html +2 -2
  103. package/static/css/tokens.css +4 -2
  104. package/static/sw.js +1 -1
  105. package/tools/local_files.py +6 -0
  106. package/latticeai/runtime/sso_runtime.py +0 -52
  107. package/static/app/assets/Brain-I1OSzxJu.js +0 -321
  108. package/static/app/assets/System-D1Lkei3I.js +0 -1
  109. package/static/app/assets/index--P0ksosz.js +0 -17
  110. package/static/app/assets/index-DCh5AoXt.css +0 -2
  111. package/static/app/assets/primitives-BLqaKk5g.js +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Lattice AI
2
2
 
3
- **Lattice AI 8.8.0 is the local-first Digital Brain platform. This release removes internal-only Brain compatibility layers, strengthens recall proof quality, and keeps package, desktop, extension, static app, and documentation metadata synchronized to the 8.8.0 line.**
3
+ **Lattice AI 9.0.0 is the local-first Digital Brain platform. This release closes the July 8 code-review follow-up, hardens chat/runtime reliability, pays down cleanup debt across runtime utilities and setup detection, and keeps package, desktop, extension, static app, and documentation metadata synchronized to the 9.0.0 line.**
4
4
 
5
5
  **Lattice AI는 모델이 바뀌어도 내 지식과 맥락을 보존하는 로컬 우선 AI 브레인입니다.**
6
6
 
@@ -67,7 +67,9 @@ You need Lattice AI when:
67
67
  The screenshots below are the latest checked-in visual evidence captures. They
68
68
  keep the first-run Brain flow, memory graph, source capture, model library,
69
69
  system view, admin console, and review center visible as release gates while
70
- 8.8.0 focuses on Brain Core extraction readiness and recall proof hardening.
70
+ 9.0.0 focuses on code-review closure, chat/runtime reliability,
71
+ AgentRuntime and ToolRegistry maintainability, release artifact correctness,
72
+ and frontend/runtime cleanup.
71
73
 
72
74
  ### 1. Wake Brain
73
75
 
@@ -169,7 +171,9 @@ pip install "ltcai[local]"
169
171
  - **Backend**: FastAPI on localhost is the UI source of truth.
170
172
  - **Brain Core**: independent `lattice_brain` package for graph, memory,
171
173
  context, conversations, ingestion, runtime, workflow, storage, and portability.
172
- - **Storage**: SQLite default; PostgreSQL/pgvector is optional scale mode.
174
+ - **Storage**: SQLite is the live local Brain store; PostgreSQL/pgvector tooling
175
+ is optional scale-mode planning/migration support, not the default live graph
176
+ backend.
173
177
  - **Portability**: encrypted `.latticebrain` archives plus backup, restore,
174
178
  inspect, verify, import dry-run, and confirmed restore/import flows.
175
179
  - **Trust boundary**: local-first by default; cloud calls, downloads, Telegram,
@@ -206,38 +210,38 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
206
210
 
207
211
  ## Current Release
208
212
 
209
- The current release is **8.8.0 — Brain Core Extraction & Recall Proof Hardening**:
210
-
211
- - Internal-only Brain shim layers are removed: pre-graph flat modules,
212
- the deprecated `latticeai.brain` namespace, and the service-layer
213
- AgentRuntime alias are tracked as intentional 8.8.0 removals.
214
- - Root compatibility shims remain managed for external entrypoints while Brain
215
- Core now has one physical import surface guarded by isolation tests.
216
- - AgentRuntime rejects unknown roles at the boundary, exposes contract envelopes
217
- for legacy run rows, and persists clamped retry budgets.
218
- - Brain Chat adds resumable conversation history, stop/regenerate/copy actions,
219
- richer ingestion panels, and answer proof explainability with matched terms
220
- and confidence labels.
221
- - Memory recall drops zero-evidence noise when lexical evidence exists, so
222
- citations explain why each source was selected.
223
-
224
- Expected artifacts for 8.8.0 release must use exact filenames:
225
-
226
- - `dist/ltcai-8.8.0-py3-none-any.whl`
227
- - `dist/ltcai-8.8.0.tar.gz`
228
- - `ltcai-8.8.0.tgz`
229
- - `dist/ltcai-8.8.0.vsix`
230
- - `src-tauri/target/release/bundle/dmg/Lattice AI_8.8.0_aarch64.dmg`
213
+ The current release is **9.0.0 — Code Review Closure & Runtime Cleanup**:
214
+
215
+ - July 8 code-review follow-ups are fixed across chat, agent runs, permissions,
216
+ Telegram integration, runtime audit timestamps, embeddings, and Brain UI copy.
217
+ - Chat/document streaming now preserves terminal SSE events and history/trace
218
+ persistence when generation fails mid-stream.
219
+ - Runtime audit appends use JSONL while legacy JSON reads remain compatible.
220
+ - Shared JSON/ISO/hash, setup detection, and frontend helper utilities replace
221
+ duplicated implementations across runtime, Brain Core, setup, and React code.
222
+ - The legacy `server_app` namespace is allowlist-based instead of exposing all
223
+ app-factory locals.
224
+ - The main `/chat` handler now routes through focused intent handlers and shared
225
+ response epilogues for lower future regression risk.
226
+
227
+ Expected artifacts for 9.0.0 release must use exact filenames:
228
+
229
+ - `dist/ltcai-9.0.0-py3-none-any.whl`
230
+ - `dist/ltcai-9.0.0.tar.gz`
231
+ - `ltcai-9.0.0.tgz`
232
+ - `dist/ltcai-9.0.0.vsix`
233
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_9.0.0_aarch64.dmg`
231
234
 
232
235
  Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
233
236
 
234
237
  See [docs/ROADMAP_RECOMMENDATIONS.md](docs/ROADMAP_RECOMMENDATIONS.md) for the
235
- strategic roadmap slices applied through 8.8.0 and the follow-up tracks.
238
+ strategic roadmap slices applied through 9.0.0 and the follow-up tracks.
236
239
 
237
240
  ## Known Limitations
238
241
 
239
242
  - External package registries are owner-published and can lag behind GitHub.
240
- - PostgreSQL/pgvector is optional scale mode. SQLite is the default local Brain.
243
+ - PostgreSQL/pgvector is optional scale/migration tooling. SQLite remains the
244
+ live local Brain store in 9.0.0.
241
245
  - Docker, model downloads, cloud model calls, Telegram, Brain Network, and update
242
246
  checks require explicit user action.
243
247
  - Conversation does not fabricate answers when no model is loaded.
@@ -249,6 +253,8 @@ strategic roadmap slices applied through 8.8.0 and the follow-up tracks.
249
253
 
250
254
  | Version | Theme |
251
255
  | --- | --- |
256
+ | 9.0.0 | Code Review Closure & Runtime Cleanup: July 8 code-review follow-ups fixed, chat/runtime reliability improved, duplicated utility surfaces consolidated, runtime audit append paths moved to JSONL, and release metadata/artifacts synchronized |
257
+ | 8.9.0 | Scoped Memory & Tool Policy Hardening: authenticated history/KG reads are workspace-scoped, direct Tool API paths enforce registry policy, local approvals hash tokens at rest, AgentRuntime approval semantics are explicit, and frontend/runtime seams are split |
252
258
  | 8.8.0 | Brain Core Extraction & Recall Proof Hardening: internal-only Brain shim layers are removed, AgentRuntime run contracts/retry budgets are tighter, Brain Chat gains conversation controls, and citation recall exposes matched evidence |
253
259
  | 8.7.0 | Runtime State Hygiene & Release Evidence Refresh: model-runtime internals prefer typed state over legacy globals, compatibility sync is deprecated, 8.7.0 visual evidence is refreshed, and all release metadata/docs are synchronized |
254
260
  | 8.6.0 | Desktop Capture & Navigation Reliability: native folder selection works from the Tauri localhost app, picker failures surface in Capture, web saving remains one-action, and the Brain shell sidebar/admin flow is CI-covered |
@@ -258,16 +264,6 @@ strategic roadmap slices applied through 8.8.0 and the follow-up tracks.
258
264
  | 8.2.0 | Brain Brief: evidence-backed home briefing, honest empty-state guidance, recall/graph/model-proof next actions, and continued model/workspace runtime extraction |
259
265
  | 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 |
260
266
  | 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 |
261
- | 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 |
262
- | 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 |
263
- | 7.7.0 | Complete Product Polish: command-center Brain Home, repaired Review Center evidence, exact 7.7 docs/artifacts, product readiness gate, and stronger CI/release checks |
264
- | 7.6.0 | Brain-Centered UX & Architecture Closure: Wake Brain first-run surface, concentric memory rings with direct depth controls, and machine-checkable closure of the two architecture/UX review files |
265
- | 7.5.0 | Runtime Debt Burn-down & Release Risk Cleanup: API consumers get normalized contract views, retrieval quality uses a 250+ record corpus fixture, stale artifact risk is removed, npm audit is clean, and Tauri is updated past the old block warning |
266
- | 7.4.0 | Runtime Contract Convergence & Corpus Retrieval: agent/workflow/audit/realtime records share the agent-run-contract/v1 family, and retrieval quality gates run against a real corpus-scale fixture |
267
- | 7.3.0 | Runtime Contract & Retrieval Quality: shared agent-run contract across runtimes plus deterministic hybrid recall/ranking regression gates |
268
- | 7.2.0 | Runtime Trust Baseline: agent run preview/readiness, simulation-mode guardrails, live ToolRegistry manifest/diagnostics, and tests for dispatch/governance/catalog drift |
269
- | 7.1.0 | Brain Usability Completion: clearer first-run onboarding, ingestion progress/emergence, richer graph controls, inline answer proof, workspace/profile/admin discovery, empty/error/consent feedback, and VS Code sync status |
270
- | 7.0.0 | Brain Productization Loop: first-screen ingestion for files/folders/notes/web, answer-level memory proof and source citations, model-continuity demo flow, five-minute first-run loop, and recall/KG quality eval in CI |
271
267
 
272
268
  ## Documentation
273
269
 
package/auto_setup.py CHANGED
@@ -25,7 +25,7 @@ Lattice AI — Zero-Config Auto Setup
25
25
  python3 auto_setup.py probe # 1단계만
26
26
  python3 auto_setup.py recommend # 1+2단계
27
27
  python3 auto_setup.py plan # 1+2+3 (설치 계획 출력)
28
- python3 auto_setup.py plan --apply # 실제 설치 실행 (위험)
28
+ python3 auto_setup.py plan --apply --confirm-token <token>
29
29
  python3 auto_setup.py verify # 4단계 단독
30
30
  python3 auto_setup.py preset # 5단계
31
31
  python3 auto_setup.py all # 전체 흐름
@@ -47,6 +47,20 @@ from dataclasses import asdict, dataclass, field
47
47
  from pathlib import Path
48
48
  from typing import Any, Dict, List, Optional, Tuple
49
49
 
50
+ from latticeai.services.process_audit import (
51
+ CommandConfirmationError,
52
+ append_process_audit_event,
53
+ command_plan,
54
+ command_plan_for_commands,
55
+ require_command_confirmation,
56
+ )
57
+ from latticeai.services.setup_detection import (
58
+ detect_cuda,
59
+ detect_tools,
60
+ detect_wsl_from_text,
61
+ parse_windows_video_controllers as _parse_windows_video_controllers,
62
+ )
63
+
50
64
  __all__ = [
51
65
  "SystemProfile", "Recommendation", "InstallPlan",
52
66
  "probe", "recommend", "plan", "verify", "preset", "run_all",
@@ -145,44 +159,6 @@ def _which(binary: str) -> Optional[str]:
145
159
  return None
146
160
 
147
161
 
148
- def _parse_windows_video_controllers(raw: str) -> List[Dict[str, Any]]:
149
- controllers: List[Dict[str, Any]] = []
150
- if not raw:
151
- return controllers
152
- try:
153
- data = json.loads(raw)
154
- if isinstance(data, dict):
155
- data = [data]
156
- if isinstance(data, list):
157
- for item in data:
158
- name = str(item.get("Name") or "").strip()
159
- if not name:
160
- continue
161
- try:
162
- ram_mb = int(item.get("AdapterRAM") or 0) // (1024 * 1024)
163
- except Exception:
164
- ram_mb = 0
165
- controllers.append({"name": name, "vram_mb": ram_mb})
166
- if controllers:
167
- return controllers
168
- except Exception:
169
- pass
170
- current: Dict[str, Any] = {}
171
- for line in raw.splitlines():
172
- if line.startswith("Name="):
173
- if current:
174
- controllers.append(current)
175
- current = {"name": line.split("=", 1)[-1].strip(), "vram_mb": 0}
176
- elif line.startswith("AdapterRAM=") and current:
177
- try:
178
- current["vram_mb"] = int(line.split("=", 1)[-1].strip()) // (1024 * 1024)
179
- except ValueError:
180
- current["vram_mb"] = 0
181
- if current:
182
- controllers.append(current)
183
- return controllers
184
-
185
-
186
162
  def _detect_gpu(prof_os: str, arch: str) -> GPUInfo:
187
163
  """OS별 휴리스틱으로 GPU 감지. 외부 라이브러리 없이 가능한 만큼만."""
188
164
  gpu = GPUInfo()
@@ -268,36 +244,17 @@ def _detect_package_manager(prof_os: str) -> Optional[str]:
268
244
 
269
245
 
270
246
  def _detect_tools() -> Dict[str, str]:
271
- tools: Dict[str, str] = {}
272
- for binary in ("ollama", "lms", "nvidia-smi", "nvcc", "winget", "brew", "apt", "git", "node", "python", "python3"):
273
- found = _which(binary)
274
- if found:
275
- tools[binary] = found
276
- return tools
247
+ detected = detect_tools(_which, ("ollama", "lms", "nvidia-smi", "nvcc", "winget", "brew", "apt", "git", "node", "python", "python3"))
248
+ return {binary: path for binary, path in detected.items() if path}
277
249
 
278
250
 
279
251
  def _detect_wsl(prof_os: str) -> Tuple[bool, str]:
280
- if prof_os != "linux":
281
- return False, ""
282
- raw = _read_text("/proc/version")
283
- is_wsl = "microsoft" in raw.lower() or "wsl" in raw.lower()
284
- version = "2" if "microsoft-standard" in raw.lower() or "wsl2" in raw.lower() else ("1" if is_wsl else "")
285
- return is_wsl, version
252
+ return detect_wsl_from_text(prof_os, _read_text("/proc/version"))
286
253
 
287
254
 
288
255
  def _detect_cuda() -> Tuple[bool, str]:
289
- nvidia_smi = _which("nvidia-smi")
290
- nvcc = _which("nvcc")
291
- version = ""
292
- if nvidia_smi:
293
- raw = _run([nvidia_smi, "--query-gpu=driver_version", "--format=csv,noheader"], timeout=4.0)
294
- version = raw.splitlines()[0].strip() if raw.splitlines() else ""
295
- if nvcc:
296
- raw = _run([nvcc, "--version"], timeout=4.0)
297
- m = re.search(r"release\s+([\d.]+)", raw)
298
- if m:
299
- version = m.group(1)
300
- return bool(nvidia_smi or nvcc), version
256
+ available, version, _nvidia_smi, _nvcc = detect_cuda(_which, lambda args: _run(args, timeout=4.0))
257
+ return available, version
301
258
 
302
259
 
303
260
  def _detect_cpu_details(prof_os: str) -> Tuple[str, int, int, List[str]]:
@@ -547,6 +504,21 @@ class InstallStep:
547
504
  command: List[str]
548
505
  requires_admin: bool = False
549
506
 
507
+ def to_json(self) -> Dict[str, Any]:
508
+ return {
509
+ "name": self.name,
510
+ "why": self.why,
511
+ "command": self.command,
512
+ "requires_admin": self.requires_admin,
513
+ "command_plan": command_plan(
514
+ self.command,
515
+ name=self.name,
516
+ purpose="auto_setup_install",
517
+ requires_admin=self.requires_admin,
518
+ metadata={"why": self.why},
519
+ ),
520
+ }
521
+
550
522
 
551
523
  @dataclass
552
524
  class InstallPlan:
@@ -555,10 +527,18 @@ class InstallPlan:
555
527
  notes: List[str] = field(default_factory=list)
556
528
 
557
529
  def to_json(self) -> Dict[str, Any]:
530
+ plan_summary = command_plan_for_commands(
531
+ [step.command for step in self.steps],
532
+ name="auto_setup_plan",
533
+ purpose="auto_setup_install",
534
+ metadata={"package_manager": self.package_manager},
535
+ )
558
536
  return {
559
537
  "package_manager": self.package_manager,
560
- "steps": [asdict(s) for s in self.steps],
538
+ "steps": [s.to_json() for s in self.steps],
561
539
  "notes": self.notes,
540
+ "command_plan": plan_summary,
541
+ "confirmation_token": plan_summary["confirmation_token"],
562
542
  }
563
543
 
564
544
 
@@ -661,23 +641,55 @@ def plan(profile: SystemProfile, rec: Recommendation) -> InstallPlan:
661
641
  return InstallPlan(package_manager=pm, steps=steps, notes=notes)
662
642
 
663
643
 
664
- def apply_plan(plan_obj: InstallPlan, *, confirm: bool = False) -> List[Dict[str, Any]]:
665
- """위험: 실제로 설치 명령을 실행한다. ``confirm=True`` 필수."""
644
+ def apply_plan(
645
+ plan_obj: InstallPlan,
646
+ *,
647
+ confirm: bool = False,
648
+ confirmation_token: Optional[str] = None,
649
+ ) -> List[Dict[str, Any]]:
650
+ """위험: 실제로 설치 명령을 실행한다. ``confirm`` and token are required."""
666
651
  if not confirm:
667
652
  raise RuntimeError("refuse to apply: pass confirm=True")
653
+ expected = plan_obj.to_json().get("confirmation_token")
654
+ if str(confirmation_token or "").strip() != expected:
655
+ raise CommandConfirmationError("refuse to apply: confirmation token mismatch")
668
656
  results: List[Dict[str, Any]] = []
669
657
  for step in plan_obj.steps:
658
+ step_plan = command_plan(
659
+ step.command,
660
+ name=step.name,
661
+ purpose="auto_setup_install",
662
+ requires_admin=step.requires_admin,
663
+ metadata={"why": step.why},
664
+ )
670
665
  try:
666
+ require_command_confirmation(
667
+ step.command,
668
+ step_plan["confirmation_token"],
669
+ purpose="auto_setup_install",
670
+ )
671
+ append_process_audit_event("installer_command", plan=step_plan, status="started")
671
672
  r = subprocess.run(step.command, capture_output=True, text=True,
672
673
  timeout=300, check=False)
674
+ append_process_audit_event(
675
+ "installer_command",
676
+ plan=step_plan,
677
+ status="finished",
678
+ returncode=r.returncode,
679
+ stdout=r.stdout,
680
+ stderr=r.stderr,
681
+ )
673
682
  results.append({
674
683
  "name": step.name,
684
+ "command_hash": step_plan["command_hash"],
685
+ "command_preview": step_plan["command_preview"],
675
686
  "returncode": r.returncode,
676
687
  "stdout_tail": (r.stdout or "")[-2000:],
677
688
  "stderr_tail": (r.stderr or "")[-2000:],
678
689
  })
679
690
  except Exception as exc:
680
- results.append({"name": step.name, "error": str(exc)})
691
+ append_process_audit_event("installer_command", plan=step_plan, status="error", error=str(exc))
692
+ results.append({"name": step.name, "command_hash": step_plan["command_hash"], "error": str(exc)})
681
693
  return results
682
694
 
683
695
 
@@ -780,13 +792,13 @@ def preset(profile: SystemProfile, rec: Recommendation) -> Dict[str, Any]:
780
792
 
781
793
 
782
794
  # ── orchestrator ────────────────────────────────────────────────────────────
783
- def run_all(*, apply_install: bool = False) -> Dict[str, Any]:
795
+ def run_all(*, apply_install: bool = False, confirmation_token: Optional[str] = None) -> Dict[str, Any]:
784
796
  p = probe()
785
797
  r = recommend(p)
786
798
  pl = plan(p, r)
787
799
  install_results = None
788
800
  if apply_install:
789
- install_results = apply_plan(pl, confirm=True)
801
+ install_results = apply_plan(pl, confirm=True, confirmation_token=confirmation_token)
790
802
  v = verify(p, r)
791
803
  ps = preset(p, r)
792
804
  return {
@@ -809,6 +821,8 @@ def _main() -> int:
809
821
  sp_plan = sub.add_parser("plan")
810
822
  sp_plan.add_argument("--apply", action="store_true",
811
823
  help="actually run the install commands (DANGER)")
824
+ sp_plan.add_argument("--confirm-token", default=None,
825
+ help="confirmation token from the dry-run plan output")
812
826
  sub.add_parser("verify")
813
827
  sub.add_parser("preset")
814
828
  sub.add_parser("all")
@@ -825,7 +839,7 @@ def _main() -> int:
825
839
  p = probe(); r = recommend(p); pl = plan(p, r)
826
840
  out: Dict[str, Any] = {"plan": pl.to_json()}
827
841
  if args.apply:
828
- out["install"] = apply_plan(pl, confirm=True)
842
+ out["install"] = apply_plan(pl, confirm=True, confirmation_token=args.confirm_token)
829
843
  print(json.dumps(out, indent=2, ensure_ascii=False)); return 0
830
844
  if args.cmd == "verify":
831
845
  p = probe(); r = recommend(p)