ltcai 8.7.0 → 8.9.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 +38 -26
- package/auto_setup.py +73 -8
- package/docs/CHANGELOG.md +63 -0
- package/docs/CODE_REVIEW_2026-07-06.md +764 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +4 -3
- package/docs/DEVELOPMENT.md +10 -10
- package/docs/LEGACY_COMPATIBILITY.md +33 -14
- package/docs/ONBOARDING.md +2 -2
- package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
- package/docs/TRUST_MODEL.md +5 -1
- package/docs/WHY_LATTICE.md +4 -3
- package/docs/architecture.md +4 -0
- package/docs/kg-schema.md +1 -1
- package/kg_schema.py +1 -1
- package/knowledge_graph.py +2 -2
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/conversations.py +156 -21
- package/lattice_brain/core.py +1 -1
- package/lattice_brain/graph/_kg_common.py +12 -34
- package/lattice_brain/graph/json_utils.py +25 -0
- package/lattice_brain/graph/retrieval.py +66 -27
- package/lattice_brain/graph/runtime.py +16 -0
- package/lattice_brain/ingestion.py +35 -20
- package/lattice_brain/runtime/agent_runtime.py +37 -3
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +31 -14
- package/latticeai/api/mcp.py +3 -2
- package/latticeai/api/models.py +4 -1
- package/latticeai/api/permissions.py +69 -30
- package/latticeai/api/setup.py +17 -2
- package/latticeai/api/tools.py +104 -62
- package/latticeai/app_factory.py +93 -10
- package/latticeai/core/agent.py +25 -7
- package/latticeai/core/legacy_compatibility.py +36 -25
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/sessions.py +11 -3
- package/latticeai/core/tool_registry.py +15 -4
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/bootstrap.py +1 -1
- package/latticeai/runtime/persistence_runtime.py +1 -1
- package/latticeai/runtime/platform_services_runtime.py +1 -1
- package/latticeai/services/app_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +2 -2
- package/latticeai/services/memory_service.py +38 -7
- package/latticeai/services/model_engines.py +79 -12
- package/latticeai/services/model_runtime.py +24 -4
- package/latticeai/services/process_audit.py +208 -0
- package/latticeai/services/product_readiness.py +11 -11
- package/latticeai/services/search_service.py +106 -30
- package/latticeai/services/tool_dispatch.py +66 -0
- package/latticeai/services/workspace_service.py +15 -0
- package/package.json +1 -1
- package/scripts/check_i18n_literals.mjs +20 -8
- package/scripts/i18n_literal_allowlist.json +34 -0
- package/scripts/lint_frontend.mjs +6 -2
- package/setup_wizard.py +185 -19
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/{Act-_U7mhXir.js → Act-fZokUnC0.js} +1 -1
- package/static/app/assets/{Brain-BxyTHZ21.js → Brain-DtyuWubr.js} +2 -2
- package/static/app/assets/{Capture-DyDKWNh9.js → Capture-D5KV3Cu7.js} +1 -1
- package/static/app/assets/{Library-DJ8KioFM.js → Library-C9kyFkSt.js} +1 -1
- package/static/app/assets/{System-C0FIb3OO.js → System-VbChmX7r.js} +1 -1
- package/static/app/assets/index-DCh5AoXt.css +2 -0
- package/static/app/assets/index-DPdcPoF0.js +17 -0
- package/static/app/assets/{primitives-BywkNS3f.js → primitives-DFeanEV6.js} +1 -1
- package/static/app/assets/{textarea-CQ61Rycp.js → textarea-CD8UNKIy.js} +1 -1
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/lattice_brain/_kg_common.py +0 -11
- package/lattice_brain/discovery.py +0 -11
- package/lattice_brain/documents.py +0 -11
- package/lattice_brain/identity.py +0 -11
- package/lattice_brain/ingest.py +0 -11
- package/lattice_brain/network.py +0 -11
- package/lattice_brain/projection.py +0 -11
- package/lattice_brain/provenance.py +0 -11
- package/lattice_brain/retrieval.py +0 -11
- package/lattice_brain/schema.py +0 -11
- package/lattice_brain/store.py +0 -11
- package/lattice_brain/write_master.py +0 -11
- package/latticeai/brain/__init__.py +0 -36
- package/latticeai/brain/_kg_common.py +0 -17
- package/latticeai/brain/context.py +0 -17
- package/latticeai/brain/conversations.py +0 -17
- package/latticeai/brain/discovery.py +0 -17
- package/latticeai/brain/documents.py +0 -17
- package/latticeai/brain/identity.py +0 -17
- package/latticeai/brain/ingest.py +0 -17
- package/latticeai/brain/memory.py +0 -17
- package/latticeai/brain/network.py +0 -17
- package/latticeai/brain/projection.py +0 -17
- package/latticeai/brain/provenance.py +0 -17
- package/latticeai/brain/retrieval.py +0 -17
- package/latticeai/brain/schema.py +0 -17
- package/latticeai/brain/store.py +0 -17
- package/latticeai/brain/write_master.py +0 -17
- package/latticeai/services/agent_runtime.py +0 -11
- package/static/app/assets/index-Bh7IIlyY.js +0 -16
- package/static/app/assets/index-_M5aCv21.css +0 -2
|
@@ -23,6 +23,13 @@ from typing import Any, Dict, List, Optional
|
|
|
23
23
|
|
|
24
24
|
from fastapi import HTTPException
|
|
25
25
|
|
|
26
|
+
from latticeai.services.process_audit import (
|
|
27
|
+
CommandConfirmationError,
|
|
28
|
+
append_process_audit_event,
|
|
29
|
+
command_plan,
|
|
30
|
+
require_command_confirmation,
|
|
31
|
+
)
|
|
32
|
+
|
|
26
33
|
|
|
27
34
|
def _progress_payload(*args, **kwargs) -> Dict[str, object]:
|
|
28
35
|
try:
|
|
@@ -440,8 +447,8 @@ def engine_support_status(engine: str) -> Dict[str, object]:
|
|
|
440
447
|
return {"supported": True, "reason": None}
|
|
441
448
|
|
|
442
449
|
|
|
443
|
-
def
|
|
444
|
-
from latticeai.services.model_runtime import BASE_DIR, ENGINE_INSTALLERS
|
|
450
|
+
def _engine_install_command(engine: str) -> tuple[list[str], str, bool]:
|
|
451
|
+
from latticeai.services.model_runtime import BASE_DIR, ENGINE_INSTALLERS
|
|
445
452
|
|
|
446
453
|
if engine not in ENGINE_INSTALLERS:
|
|
447
454
|
raise HTTPException(status_code=400, detail="지원하지 않는 엔진입니다.")
|
|
@@ -449,14 +456,7 @@ def install_engine(engine: str) -> Dict[str, Any]:
|
|
|
449
456
|
required_binary = installer.get("requires_binary")
|
|
450
457
|
if required_binary and shutil.which(required_binary) is None:
|
|
451
458
|
raise HTTPException(status_code=400, detail=f"{required_binary}가 설치되어 있지 않아 자동 설치할 수 없습니다.")
|
|
452
|
-
command = installer["command"]
|
|
453
|
-
run_kwargs = {
|
|
454
|
-
"cwd": str(BASE_DIR),
|
|
455
|
-
"capture_output": True,
|
|
456
|
-
"text": True,
|
|
457
|
-
"timeout": 900,
|
|
458
|
-
"check": False,
|
|
459
|
-
}
|
|
459
|
+
command = list(installer["command"])
|
|
460
460
|
|
|
461
461
|
if engine == "vllm" and sys.platform == "darwin" and platform.machine() == "arm64":
|
|
462
462
|
command = [
|
|
@@ -468,13 +468,69 @@ def install_engine(engine: str) -> Dict[str, Any]:
|
|
|
468
468
|
"~/.venv-vllm-metal/bin/pip install -U pip setuptools wheel; "
|
|
469
469
|
"~/.venv-vllm-metal/bin/pip install vllm-metal",
|
|
470
470
|
]
|
|
471
|
+
requires_admin = bool(command and command[0] in {"apt", "apt-get", "dnf", "pacman"})
|
|
472
|
+
return command, str(BASE_DIR), requires_admin
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def engine_install_plan(engine: str) -> Dict[str, Any]:
|
|
476
|
+
command, cwd, requires_admin = _engine_install_command(engine)
|
|
477
|
+
return command_plan(
|
|
478
|
+
command,
|
|
479
|
+
name=f"engine:{engine}",
|
|
480
|
+
purpose="engine_install",
|
|
481
|
+
cwd=cwd,
|
|
482
|
+
requires_admin=requires_admin,
|
|
483
|
+
metadata={"engine": engine},
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def install_engine(engine: str, confirmation_token: Optional[str] = None) -> Dict[str, Any]:
|
|
488
|
+
from latticeai.services.model_runtime import engine_installed
|
|
489
|
+
|
|
490
|
+
command, cwd, _requires_admin = _engine_install_command(engine)
|
|
491
|
+
plan = engine_install_plan(engine)
|
|
492
|
+
try:
|
|
493
|
+
require_command_confirmation(command, confirmation_token, cwd=cwd, purpose="engine_install")
|
|
494
|
+
except CommandConfirmationError as exc:
|
|
495
|
+
append_process_audit_event("engine_install", plan=plan, status="denied", error=str(exc))
|
|
496
|
+
raise HTTPException(
|
|
497
|
+
status_code=403,
|
|
498
|
+
detail={
|
|
499
|
+
"status": "confirmation_required",
|
|
500
|
+
"reason": str(exc),
|
|
501
|
+
"install_plan": plan,
|
|
502
|
+
},
|
|
503
|
+
) from exc
|
|
504
|
+
|
|
505
|
+
run_kwargs = {
|
|
506
|
+
"cwd": cwd,
|
|
507
|
+
"capture_output": True,
|
|
508
|
+
"text": True,
|
|
509
|
+
"timeout": 900,
|
|
510
|
+
"check": False,
|
|
511
|
+
}
|
|
471
512
|
try:
|
|
513
|
+
append_process_audit_event("engine_install", plan=plan, status="started")
|
|
472
514
|
completed = subprocess.run(command, **run_kwargs)
|
|
473
515
|
except subprocess.TimeoutExpired:
|
|
516
|
+
append_process_audit_event("engine_install", plan=plan, status="timeout")
|
|
474
517
|
raise HTTPException(status_code=408, detail="엔진 설치 시간이 초과되었습니다.")
|
|
518
|
+
except Exception as exc:
|
|
519
|
+
append_process_audit_event("engine_install", plan=plan, status="error", error=str(exc))
|
|
520
|
+
raise
|
|
521
|
+
append_process_audit_event(
|
|
522
|
+
"engine_install",
|
|
523
|
+
plan=plan,
|
|
524
|
+
status="finished",
|
|
525
|
+
returncode=completed.returncode,
|
|
526
|
+
stdout=completed.stdout,
|
|
527
|
+
stderr=completed.stderr,
|
|
528
|
+
)
|
|
475
529
|
result = {
|
|
476
530
|
"engine": engine,
|
|
477
|
-
"
|
|
531
|
+
"command_hash": plan["command_hash"],
|
|
532
|
+
"command_preview": plan["command_preview"],
|
|
533
|
+
"install_plan": plan,
|
|
478
534
|
"returncode": completed.returncode,
|
|
479
535
|
"stdout": completed.stdout[-12000:],
|
|
480
536
|
"stderr": completed.stderr[-12000:],
|
|
@@ -491,15 +547,25 @@ def install_engine(engine: str) -> Dict[str, Any]:
|
|
|
491
547
|
if already_up:
|
|
492
548
|
result["daemon_started"] = "already_running"
|
|
493
549
|
else:
|
|
550
|
+
daemon_command = [ollama, "serve"]
|
|
551
|
+
daemon_plan = command_plan(
|
|
552
|
+
daemon_command,
|
|
553
|
+
name="engine:ollama:serve",
|
|
554
|
+
purpose="engine_daemon_start",
|
|
555
|
+
metadata={"engine": "ollama"},
|
|
556
|
+
)
|
|
494
557
|
try:
|
|
558
|
+
append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="started")
|
|
495
559
|
subprocess.Popen(
|
|
496
|
-
|
|
560
|
+
daemon_command,
|
|
497
561
|
stdout=subprocess.DEVNULL,
|
|
498
562
|
stderr=subprocess.DEVNULL,
|
|
499
563
|
start_new_session=True,
|
|
500
564
|
)
|
|
565
|
+
append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="spawned")
|
|
501
566
|
result["daemon_started"] = True
|
|
502
567
|
except Exception as exc:
|
|
568
|
+
append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="error", error=str(exc))
|
|
503
569
|
logging.warning("ollama serve spawn failed: %s", exc)
|
|
504
570
|
result["daemon_started"] = False
|
|
505
571
|
return result
|
|
@@ -589,6 +655,7 @@ __all__ = [
|
|
|
589
655
|
"pull_ollama_model_with_progress",
|
|
590
656
|
"get_ollama_pulled_models",
|
|
591
657
|
"engine_support_status",
|
|
658
|
+
"engine_install_plan",
|
|
592
659
|
"install_engine",
|
|
593
660
|
"_smoke_test_loaded_model",
|
|
594
661
|
]
|
|
@@ -47,6 +47,7 @@ from .model_engines import (
|
|
|
47
47
|
pull_ollama_model_with_progress as _pull_ollama_model_with_progress,
|
|
48
48
|
get_ollama_pulled_models as _get_ollama_pulled_models,
|
|
49
49
|
engine_support_status as _engine_support_status,
|
|
50
|
+
engine_install_plan as _engine_install_plan,
|
|
50
51
|
install_engine as _install_engine,
|
|
51
52
|
local_binary as _local_binary,
|
|
52
53
|
vllm_executable as _vllm_executable,
|
|
@@ -88,6 +89,8 @@ class ModelRuntimeState:
|
|
|
88
89
|
self.ENABLE_GRAPH = True
|
|
89
90
|
self.AUTOLOAD_MODELS = False
|
|
90
91
|
self.MODEL_IDLE_UNLOAD_SECONDS = 0
|
|
92
|
+
self.ALLOW_MODEL_DOWNLOADS = False
|
|
93
|
+
self.MODEL_DOWNLOAD_TIMEOUT = 300
|
|
91
94
|
self.ALLOW_LOCAL_MODELS = True
|
|
92
95
|
self.REQUIRE_AUTH = False
|
|
93
96
|
self.INVITE_GATE_ENABLED = False
|
|
@@ -118,7 +121,8 @@ class ModelRuntimeState:
|
|
|
118
121
|
"""Internal no-warning sync used at init."""
|
|
119
122
|
global router, APP_MODE, DEFAULT_HOST, DEFAULT_PORT, DATA_DIR, BASE_DIR
|
|
120
123
|
global ENABLE_TELEGRAM, ENABLE_GRAPH, AUTOLOAD_MODELS, MODEL_IDLE_UNLOAD_SECONDS
|
|
121
|
-
global
|
|
124
|
+
global ALLOW_MODEL_DOWNLOADS, MODEL_DOWNLOAD_TIMEOUT, ALLOW_LOCAL_MODELS, REQUIRE_AUTH
|
|
125
|
+
global INVITE_GATE_ENABLED, ALLOW_PLAINTEXT_API_KEYS
|
|
122
126
|
global CORS_ALLOW_NETWORK, PUBLIC_MODEL, LOCAL_MODEL, IS_PUBLIC_MODE
|
|
123
127
|
global keyring, get_current_user, get_user_api_key
|
|
124
128
|
router = self.router
|
|
@@ -131,6 +135,8 @@ class ModelRuntimeState:
|
|
|
131
135
|
ENABLE_GRAPH = self.ENABLE_GRAPH
|
|
132
136
|
AUTOLOAD_MODELS = self.AUTOLOAD_MODELS
|
|
133
137
|
MODEL_IDLE_UNLOAD_SECONDS = self.MODEL_IDLE_UNLOAD_SECONDS
|
|
138
|
+
ALLOW_MODEL_DOWNLOADS = self.ALLOW_MODEL_DOWNLOADS
|
|
139
|
+
MODEL_DOWNLOAD_TIMEOUT = self.MODEL_DOWNLOAD_TIMEOUT
|
|
134
140
|
ALLOW_LOCAL_MODELS = self.ALLOW_LOCAL_MODELS
|
|
135
141
|
REQUIRE_AUTH = self.REQUIRE_AUTH
|
|
136
142
|
INVITE_GATE_ENABLED = self.INVITE_GATE_ENABLED
|
|
@@ -159,7 +165,8 @@ def _env_bool(key: str, default: bool = False) -> bool:
|
|
|
159
165
|
def _download_allowed(allow_download: bool = False) -> bool:
|
|
160
166
|
# Prefer STATE (the source of truth) over bare module global for internal logic.
|
|
161
167
|
autoload = getattr(STATE, "AUTOLOAD_MODELS", AUTOLOAD_MODELS)
|
|
162
|
-
|
|
168
|
+
configured = getattr(STATE, "ALLOW_MODEL_DOWNLOADS", ALLOW_MODEL_DOWNLOADS)
|
|
169
|
+
return bool(allow_download) or bool(configured) or bool(autoload)
|
|
163
170
|
|
|
164
171
|
|
|
165
172
|
def _download_block(provider: str, model_name: str) -> None:
|
|
@@ -726,6 +733,13 @@ def ensure_llamacpp_server(model_name: str) -> None:
|
|
|
726
733
|
return _ensure_llamacpp_server(model_name)
|
|
727
734
|
|
|
728
735
|
|
|
736
|
+
def _safe_engine_install_plan(engine: str) -> Optional[Dict[str, object]]:
|
|
737
|
+
try:
|
|
738
|
+
return _engine_install_plan(engine)
|
|
739
|
+
except Exception:
|
|
740
|
+
return None
|
|
741
|
+
|
|
742
|
+
|
|
729
743
|
def engine_installed(engine: str) -> bool:
|
|
730
744
|
if engine == "local_mlx":
|
|
731
745
|
return bool(
|
|
@@ -847,6 +861,7 @@ def engine_status() -> List[Dict]:
|
|
|
847
861
|
"installed": engine_installed("local_mlx"),
|
|
848
862
|
"installable": True,
|
|
849
863
|
"install_label": ENGINE_INSTALLERS["local_mlx"]["label"],
|
|
864
|
+
"install_plan": _safe_engine_install_plan("local_mlx"),
|
|
850
865
|
"models": mlx_models,
|
|
851
866
|
},
|
|
852
867
|
{
|
|
@@ -857,6 +872,7 @@ def engine_status() -> List[Dict]:
|
|
|
857
872
|
"installed": ollama_installed,
|
|
858
873
|
"installable": True,
|
|
859
874
|
"install_label": ENGINE_INSTALLERS["ollama"]["label"],
|
|
875
|
+
"install_plan": _safe_engine_install_plan("ollama"),
|
|
860
876
|
"models": ollama_models,
|
|
861
877
|
},
|
|
862
878
|
]
|
|
@@ -872,6 +888,7 @@ def engine_status() -> List[Dict]:
|
|
|
872
888
|
"support_reason": support["reason"],
|
|
873
889
|
"installable": support["supported"] and spec["id"] in ENGINE_INSTALLERS,
|
|
874
890
|
"install_label": ENGINE_INSTALLERS.get(spec["id"], {}).get("label"),
|
|
891
|
+
"install_plan": _safe_engine_install_plan(spec["id"]) if spec["id"] in ENGINE_INSTALLERS else None,
|
|
875
892
|
"requires": spec["requires"],
|
|
876
893
|
"models": (
|
|
877
894
|
vllm_models if spec["id"] == "vllm"
|
|
@@ -900,6 +917,7 @@ def engine_status() -> List[Dict]:
|
|
|
900
917
|
"installed": engine_installed(provider),
|
|
901
918
|
"installable": True,
|
|
902
919
|
"install_label": ENGINE_INSTALLERS[provider]["label"],
|
|
920
|
+
"install_plan": _safe_engine_install_plan(provider),
|
|
903
921
|
"requires": env_key,
|
|
904
922
|
"models": provider_models,
|
|
905
923
|
})
|
|
@@ -920,6 +938,8 @@ def runtime_features() -> Dict:
|
|
|
920
938
|
"graph_enabled": s.ENABLE_GRAPH,
|
|
921
939
|
"autoload_models": s.AUTOLOAD_MODELS,
|
|
922
940
|
"model_idle_unload_seconds": s.MODEL_IDLE_UNLOAD_SECONDS,
|
|
941
|
+
"allow_model_downloads": s.ALLOW_MODEL_DOWNLOADS,
|
|
942
|
+
"model_download_timeout": s.MODEL_DOWNLOAD_TIMEOUT,
|
|
923
943
|
"model_memory_policy": r.model_memory_policy() if r else None,
|
|
924
944
|
"allow_local_models": s.ALLOW_LOCAL_MODELS,
|
|
925
945
|
"security": {
|
|
@@ -944,8 +964,8 @@ def runtime_features() -> Dict:
|
|
|
944
964
|
},
|
|
945
965
|
}
|
|
946
966
|
|
|
947
|
-
def install_engine(engine: str) -> Dict:
|
|
948
|
-
return _install_engine(engine)
|
|
967
|
+
def install_engine(engine: str, confirmation_token: Optional[str] = None) -> Dict:
|
|
968
|
+
return _install_engine(engine, confirmation_token=confirmation_token)
|
|
949
969
|
|
|
950
970
|
|
|
951
971
|
def _resolve_model_alias(model_id: str, engine: Optional[str] = None) -> str:
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Auditable command plans for installer and local process execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Iterable, Mapping, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_SECRET_RE = re.compile(
|
|
15
|
+
r"(api[_-]?key|access[_-]?token|auth[_-]?token|bearer|client[_-]?secret|password|secret|token)",
|
|
16
|
+
re.IGNORECASE,
|
|
17
|
+
)
|
|
18
|
+
_SENSITIVE_FLAGS = {
|
|
19
|
+
"--api-key",
|
|
20
|
+
"--access-token",
|
|
21
|
+
"--auth-token",
|
|
22
|
+
"--client-secret",
|
|
23
|
+
"--password",
|
|
24
|
+
"--secret",
|
|
25
|
+
"--token",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommandConfirmationError(ValueError):
|
|
30
|
+
"""Raised when a process command lacks the expected execution token."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _default_audit_file() -> Path:
|
|
34
|
+
base = Path(os.environ.get("LATTICEAI_DATA_DIR") or Path.home() / ".ltcai")
|
|
35
|
+
return base / "process_audit.jsonl"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_command(command: Sequence[Any]) -> list[str]:
|
|
39
|
+
return [str(part) for part in command]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def redact_command(command: Sequence[Any]) -> list[str]:
|
|
43
|
+
redacted: list[str] = []
|
|
44
|
+
redact_next = False
|
|
45
|
+
for raw in _normalize_command(command):
|
|
46
|
+
lower = raw.lower()
|
|
47
|
+
if redact_next:
|
|
48
|
+
redacted.append("[REDACTED]")
|
|
49
|
+
redact_next = False
|
|
50
|
+
continue
|
|
51
|
+
if lower in _SENSITIVE_FLAGS:
|
|
52
|
+
redacted.append(raw)
|
|
53
|
+
redact_next = True
|
|
54
|
+
continue
|
|
55
|
+
if "=" in raw:
|
|
56
|
+
key, _value = raw.split("=", 1)
|
|
57
|
+
if _SECRET_RE.search(key):
|
|
58
|
+
redacted.append(f"{key}=[REDACTED]")
|
|
59
|
+
continue
|
|
60
|
+
if _SECRET_RE.search(raw) and len(raw) > 32:
|
|
61
|
+
redacted.append("[REDACTED]")
|
|
62
|
+
continue
|
|
63
|
+
redacted.append(raw)
|
|
64
|
+
return redacted
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _digest_payload(payload: Mapping[str, Any]) -> str:
|
|
68
|
+
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
|
|
69
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def command_hash(command: Sequence[Any], *, cwd: Optional[str] = None) -> str:
|
|
73
|
+
return _digest_payload({"command": _normalize_command(command), "cwd": cwd or ""})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def confirmation_token(command: Sequence[Any], *, cwd: Optional[str] = None, purpose: str = "execute") -> str:
|
|
77
|
+
digest = _digest_payload(
|
|
78
|
+
{
|
|
79
|
+
"purpose": purpose,
|
|
80
|
+
"command": _normalize_command(command),
|
|
81
|
+
"cwd": cwd or "",
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
return digest[:16]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def command_plan(
|
|
88
|
+
command: Sequence[Any],
|
|
89
|
+
*,
|
|
90
|
+
name: str,
|
|
91
|
+
purpose: str = "installer",
|
|
92
|
+
cwd: Optional[str] = None,
|
|
93
|
+
requires_admin: bool = False,
|
|
94
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
95
|
+
) -> dict[str, Any]:
|
|
96
|
+
return {
|
|
97
|
+
"name": name,
|
|
98
|
+
"purpose": purpose,
|
|
99
|
+
"command_preview": redact_command(command),
|
|
100
|
+
"command_hash": command_hash(command, cwd=cwd),
|
|
101
|
+
"confirmation_token": confirmation_token(command, cwd=cwd, purpose=purpose),
|
|
102
|
+
"requires_admin": bool(requires_admin),
|
|
103
|
+
"cwd": cwd,
|
|
104
|
+
"metadata": dict(metadata or {}),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def command_plan_for_commands(
|
|
109
|
+
commands: Iterable[Sequence[Any]],
|
|
110
|
+
*,
|
|
111
|
+
name: str,
|
|
112
|
+
purpose: str = "installer",
|
|
113
|
+
cwd: Optional[str] = None,
|
|
114
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
normalized = [_normalize_command(command) for command in commands]
|
|
117
|
+
digest = _digest_payload({"purpose": purpose, "commands": normalized, "cwd": cwd or ""})
|
|
118
|
+
return {
|
|
119
|
+
"name": name,
|
|
120
|
+
"purpose": purpose,
|
|
121
|
+
"command_count": len(normalized),
|
|
122
|
+
"command_hash": digest,
|
|
123
|
+
"confirmation_token": digest[:16],
|
|
124
|
+
"cwd": cwd,
|
|
125
|
+
"metadata": dict(metadata or {}),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def verify_command_confirmation(
|
|
130
|
+
command: Sequence[Any],
|
|
131
|
+
provided_token: Optional[str],
|
|
132
|
+
*,
|
|
133
|
+
cwd: Optional[str] = None,
|
|
134
|
+
purpose: str = "installer",
|
|
135
|
+
) -> bool:
|
|
136
|
+
if not provided_token:
|
|
137
|
+
return False
|
|
138
|
+
return str(provided_token).strip() == confirmation_token(command, cwd=cwd, purpose=purpose)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def require_command_confirmation(
|
|
142
|
+
command: Sequence[Any],
|
|
143
|
+
provided_token: Optional[str],
|
|
144
|
+
*,
|
|
145
|
+
cwd: Optional[str] = None,
|
|
146
|
+
purpose: str = "installer",
|
|
147
|
+
) -> None:
|
|
148
|
+
if not verify_command_confirmation(command, provided_token, cwd=cwd, purpose=purpose):
|
|
149
|
+
raise CommandConfirmationError("installer command requires a matching confirmation token")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _preview_text(text: Optional[str], *, limit: int = 500) -> Optional[str]:
|
|
153
|
+
if text is None:
|
|
154
|
+
return None
|
|
155
|
+
value = str(text)[-limit:]
|
|
156
|
+
value = re.sub(r"(?i)(api[_-]?key|access[_-]?token|auth[_-]?token|password|secret|token)=\S+", r"\1=[REDACTED]", value)
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _text_hash(text: Optional[str]) -> Optional[str]:
|
|
161
|
+
if text is None:
|
|
162
|
+
return None
|
|
163
|
+
return hashlib.sha256(str(text).encode("utf-8", errors="replace")).hexdigest()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def append_process_audit_event(
|
|
167
|
+
event_type: str,
|
|
168
|
+
*,
|
|
169
|
+
plan: Mapping[str, Any],
|
|
170
|
+
status: str,
|
|
171
|
+
user_email: Optional[str] = None,
|
|
172
|
+
returncode: Optional[int] = None,
|
|
173
|
+
stdout: Optional[str] = None,
|
|
174
|
+
stderr: Optional[str] = None,
|
|
175
|
+
error: Optional[str] = None,
|
|
176
|
+
audit_file: Optional[Path] = None,
|
|
177
|
+
extra: Optional[Mapping[str, Any]] = None,
|
|
178
|
+
) -> None:
|
|
179
|
+
path = audit_file or _default_audit_file()
|
|
180
|
+
entry = {
|
|
181
|
+
"ts": time.time(),
|
|
182
|
+
"event_type": event_type,
|
|
183
|
+
"status": status,
|
|
184
|
+
"user_email": user_email,
|
|
185
|
+
"plan": {
|
|
186
|
+
"name": plan.get("name"),
|
|
187
|
+
"purpose": plan.get("purpose"),
|
|
188
|
+
"command_preview": plan.get("command_preview"),
|
|
189
|
+
"command_hash": plan.get("command_hash"),
|
|
190
|
+
"requires_admin": plan.get("requires_admin"),
|
|
191
|
+
"cwd": plan.get("cwd"),
|
|
192
|
+
"metadata": plan.get("metadata") or {},
|
|
193
|
+
},
|
|
194
|
+
"returncode": returncode,
|
|
195
|
+
"stdout_hash": _text_hash(stdout),
|
|
196
|
+
"stderr_hash": _text_hash(stderr),
|
|
197
|
+
"stdout_preview": _preview_text(stdout),
|
|
198
|
+
"stderr_preview": _preview_text(stderr),
|
|
199
|
+
"error": _preview_text(error),
|
|
200
|
+
"extra": dict(extra or {}),
|
|
201
|
+
}
|
|
202
|
+
try:
|
|
203
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
205
|
+
fh.write(json.dumps(entry, ensure_ascii=False, sort_keys=True) + "\n")
|
|
206
|
+
except Exception:
|
|
207
|
+
# Auditing must never make setup recovery paths unusable.
|
|
208
|
+
return
|
|
@@ -18,7 +18,7 @@ from typing import Any, Dict, List
|
|
|
18
18
|
|
|
19
19
|
from latticeai.services.architecture_readiness import architecture_readiness
|
|
20
20
|
|
|
21
|
-
PRODUCT_VERSION_TARGET = "8.
|
|
21
|
+
PRODUCT_VERSION_TARGET = "8.9.0"
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@dataclass(frozen=True)
|
|
@@ -76,10 +76,10 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
76
76
|
evidence=[
|
|
77
77
|
"package.json::release:artifacts",
|
|
78
78
|
"package.json::release:validate",
|
|
79
|
-
"README.md::dist/ltcai-8.
|
|
80
|
-
"README.md::dist/ltcai-8.
|
|
81
|
-
"README.md::dist/ltcai-8.
|
|
82
|
-
"README.md::ltcai-8.
|
|
79
|
+
"README.md::dist/ltcai-8.9.0-py3-none-any.whl",
|
|
80
|
+
"README.md::dist/ltcai-8.9.0.tar.gz",
|
|
81
|
+
"README.md::dist/ltcai-8.9.0.vsix",
|
|
82
|
+
"README.md::ltcai-8.9.0.tgz",
|
|
83
83
|
"scripts/validate_release_artifacts.py",
|
|
84
84
|
"scripts/release_smoke.py",
|
|
85
85
|
"Dockerfile",
|
|
@@ -95,12 +95,12 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
95
95
|
title="Release story is documented and honest",
|
|
96
96
|
evidence=[
|
|
97
97
|
"README.md",
|
|
98
|
-
"README.md::The current release is **8.
|
|
99
|
-
"SECURITY.md::8.
|
|
100
|
-
"vscode-extension/README.md::**8.
|
|
101
|
-
"docs/CHANGELOG.md::## [8.
|
|
98
|
+
"README.md::The current release is **8.9.0",
|
|
99
|
+
"SECURITY.md::8.9.x (latest)",
|
|
100
|
+
"vscode-extension/README.md::**8.9.0",
|
|
101
|
+
"docs/CHANGELOG.md::## [8.9.0]",
|
|
102
102
|
"FEATURE_STATUS.md",
|
|
103
|
-
"RELEASE_NOTES_v8.
|
|
103
|
+
"RELEASE_NOTES_v8.9.0.md",
|
|
104
104
|
"latticeai/core/agent.py::SingleAgentRuntime",
|
|
105
105
|
"latticeai/core/agent.py::AgentRuntime = SingleAgentRuntime",
|
|
106
106
|
"lattice_brain/runtime/contracts.py::runtime-boundary/v1",
|
|
@@ -115,7 +115,7 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
115
115
|
id="ecosystem-path",
|
|
116
116
|
title="Community and plugin growth path is explicit",
|
|
117
117
|
evidence=[
|
|
118
|
-
"docs/COMMUNITY_AND_PLUGINS.md::8.
|
|
118
|
+
"docs/COMMUNITY_AND_PLUGINS.md::8.9.0",
|
|
119
119
|
"docs/PLUGIN_SDK.md",
|
|
120
120
|
"plugins/README.md",
|
|
121
121
|
"plugins/hello-world/plugin.json",
|