ltcai 4.1.0 → 4.3.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 +33 -24
- package/docs/CHANGELOG.md +84 -0
- package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +97 -0
- package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +91 -0
- package/docs/V4_2_VALIDATION_REPORT.md +89 -0
- package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +69 -0
- package/docs/V4_3_PRIVACY_AUDIT.md +60 -0
- package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +53 -0
- package/docs/V4_3_VALIDATION_REPORT.md +58 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +31 -33
- package/frontend/openapi.json +449 -1
- package/frontend/src/api/client.ts +10 -0
- package/frontend/src/api/openapi.ts +542 -0
- package/frontend/src/pages/System.tsx +92 -0
- package/kg_schema.py +1 -1
- package/knowledge_graph.py +4 -4
- package/lattice_brain/__init__.py +70 -0
- package/lattice_brain/_kg_common.py +1 -0
- package/lattice_brain/archive.py +446 -0
- package/lattice_brain/context.py +3 -0
- package/lattice_brain/conversations.py +3 -0
- package/lattice_brain/core.py +82 -0
- package/lattice_brain/discovery.py +1 -0
- package/lattice_brain/documents.py +1 -0
- package/lattice_brain/embeddings.py +82 -0
- package/lattice_brain/identity.py +13 -0
- package/lattice_brain/ingest.py +1 -0
- package/lattice_brain/memory.py +3 -0
- package/lattice_brain/network.py +1 -0
- package/lattice_brain/projection.py +1 -0
- package/lattice_brain/provenance.py +1 -0
- package/lattice_brain/retrieval.py +1 -0
- package/lattice_brain/schema.py +1 -0
- package/lattice_brain/storage/__init__.py +22 -0
- package/lattice_brain/storage/base.py +72 -0
- package/lattice_brain/storage/docker.py +105 -0
- package/lattice_brain/storage/factory.py +31 -0
- package/lattice_brain/storage/migration.py +190 -0
- package/lattice_brain/storage/postgres.py +123 -0
- package/lattice_brain/storage/sqlite.py +128 -0
- package/lattice_brain/store.py +3 -0
- package/lattice_brain/write_master.py +1 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +11 -0
- package/latticeai/api/portability.py +127 -1
- package/latticeai/app_factory.py +26 -10
- package/latticeai/brain/__init__.py +6 -6
- package/latticeai/brain/_kg_common.py +1 -1
- package/latticeai/brain/network.py +1 -1
- package/latticeai/brain/retrieval.py +15 -0
- package/latticeai/brain/store.py +22 -6
- package/latticeai/core/config.py +9 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/product_hardening.py +217 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/kg_portability.py +227 -3
- package/ltcai_cli.py +2 -1
- package/package.json +4 -3
- package/scripts/bump_version.py +3 -0
- package/scripts/clean_release_artifacts.mjs +27 -0
- package/scripts/lint_frontend.mjs +10 -0
- package/scripts/migrate_brain_storage.py +53 -0
- package/scripts/validate_release_artifacts.py +10 -0
- package/scripts/wheel_smoke.py +3 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +113 -13
- package/src-tauri/tauri.conf.json +5 -2
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/{index-CJRAzNnf.js → index-RiJTJliG.js} +3 -3
- package/static/app/assets/index-RiJTJliG.js.map +1 -0
- package/static/app/assets/index-yZswHE3d.css +2 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-CJRAzNnf.js.map +0 -1
- package/static/app/assets/index-CSwBBgf4.css +0 -2
|
@@ -14,7 +14,7 @@ from datetime import datetime
|
|
|
14
14
|
from typing import Any, Callable, Dict, List, Optional
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
MULTI_AGENT_VERSION = "4.
|
|
17
|
+
MULTI_AGENT_VERSION = "4.3.0"
|
|
18
18
|
|
|
19
19
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
20
20
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Product hardening and privacy status helpers.
|
|
2
|
+
|
|
3
|
+
These helpers are read-only and must not perform network probes. They describe
|
|
4
|
+
the local-first startup posture and distinguish available credentials from
|
|
5
|
+
enabled outbound communication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Mapping, Optional
|
|
14
|
+
|
|
15
|
+
from latticeai.core.config import Config
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _bool(env: Mapping[str, str], key: str, default: bool = False) -> bool:
|
|
19
|
+
raw = env.get(key)
|
|
20
|
+
if raw is None:
|
|
21
|
+
return default
|
|
22
|
+
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _present(env: Mapping[str, str], *keys: str) -> bool:
|
|
26
|
+
return any(bool(str(env.get(key) or "").strip()) for key in keys)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def external_integration_status(
|
|
30
|
+
config: Config,
|
|
31
|
+
*,
|
|
32
|
+
env: Optional[Mapping[str, str]] = None,
|
|
33
|
+
) -> Dict[str, Any]:
|
|
34
|
+
if env is None:
|
|
35
|
+
env = os.environ
|
|
36
|
+
telegram_credentials = _present(env, "LATTICEAI_TELEGRAM_BOT_TOKEN", "TELEGRAM_BOT_TOKEN")
|
|
37
|
+
brain_network_auto_push = _bool(env, "LATTICEAI_BRAIN_NETWORK_AUTO_PUSH", default=False)
|
|
38
|
+
updater_enabled = _bool(env, "LATTICEAI_ENABLE_UPDATES", default=False)
|
|
39
|
+
model_downloads_enabled = _bool(env, "LATTICEAI_ALLOW_MODEL_DOWNLOADS", default=False) or bool(config.autoload_models)
|
|
40
|
+
docker_auto_start = _bool(env, "LATTICEAI_DOCKER_AUTO_START", default=False)
|
|
41
|
+
external_connectors_enabled = _bool(env, "LATTICEAI_ENABLE_EXTERNAL_CONNECTORS", default=False)
|
|
42
|
+
postgres_enabled = config.storage_engine == "postgres" and bool(config.postgres_dsn)
|
|
43
|
+
return {
|
|
44
|
+
"local_only_default": default_startup_local_only(config, env=env),
|
|
45
|
+
"integrations": {
|
|
46
|
+
"telegram": {
|
|
47
|
+
"enabled": bool(config.enable_telegram),
|
|
48
|
+
"credential_present": telegram_credentials,
|
|
49
|
+
"opt_in_required": True,
|
|
50
|
+
"automatic_egress": bool(config.enable_telegram),
|
|
51
|
+
"detail": (
|
|
52
|
+
"enabled by LATTICEAI_ENABLE_TELEGRAM"
|
|
53
|
+
if config.enable_telegram
|
|
54
|
+
else "disabled; token presence alone does not start Telegram"
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
"brain_network": {
|
|
58
|
+
"enabled": brain_network_auto_push,
|
|
59
|
+
"credential_present": False,
|
|
60
|
+
"opt_in_required": True,
|
|
61
|
+
"automatic_egress": brain_network_auto_push,
|
|
62
|
+
"detail": "peer pushes are user/admin initiated; no automatic peer sync by default",
|
|
63
|
+
},
|
|
64
|
+
"updates": {
|
|
65
|
+
"enabled": updater_enabled,
|
|
66
|
+
"credential_present": False,
|
|
67
|
+
"opt_in_required": True,
|
|
68
|
+
"automatic_egress": updater_enabled,
|
|
69
|
+
"detail": "desktop updater checks are disabled unless LATTICEAI_ENABLE_UPDATES is true",
|
|
70
|
+
},
|
|
71
|
+
"model_downloads": {
|
|
72
|
+
"enabled": model_downloads_enabled,
|
|
73
|
+
"credential_present": _present(env, "HF_TOKEN", "HUGGINGFACEHUB_API_TOKEN"),
|
|
74
|
+
"opt_in_required": True,
|
|
75
|
+
"automatic_egress": bool(config.autoload_models),
|
|
76
|
+
"detail": "model downloads require an explicit load/autoload setting",
|
|
77
|
+
},
|
|
78
|
+
"docker": {
|
|
79
|
+
"enabled": docker_auto_start,
|
|
80
|
+
"credential_present": False,
|
|
81
|
+
"opt_in_required": True,
|
|
82
|
+
"automatic_egress": docker_auto_start,
|
|
83
|
+
"detail": "Docker setup requires explicit runtime consent; auto-start is disabled by default",
|
|
84
|
+
},
|
|
85
|
+
"postgres": {
|
|
86
|
+
"enabled": postgres_enabled,
|
|
87
|
+
"credential_present": bool(config.postgres_dsn),
|
|
88
|
+
"opt_in_required": True,
|
|
89
|
+
"automatic_egress": postgres_enabled,
|
|
90
|
+
"detail": "Postgres scale mode is used only when storage engine and DSN are explicitly configured",
|
|
91
|
+
},
|
|
92
|
+
"external_connectors": {
|
|
93
|
+
"enabled": external_connectors_enabled,
|
|
94
|
+
"credential_present": _present(
|
|
95
|
+
env,
|
|
96
|
+
"OPENAI_API_KEY",
|
|
97
|
+
"ANTHROPIC_API_KEY",
|
|
98
|
+
"GITHUB_TOKEN",
|
|
99
|
+
"SLACK_BOT_TOKEN",
|
|
100
|
+
"DISCORD_BOT_TOKEN",
|
|
101
|
+
),
|
|
102
|
+
"opt_in_required": True,
|
|
103
|
+
"automatic_egress": external_connectors_enabled,
|
|
104
|
+
"detail": "connector credentials are inert until the connector is explicitly enabled and invoked",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def default_startup_local_only(
|
|
111
|
+
config: Config,
|
|
112
|
+
*,
|
|
113
|
+
env: Optional[Mapping[str, str]] = None,
|
|
114
|
+
) -> bool:
|
|
115
|
+
if env is None:
|
|
116
|
+
env = os.environ
|
|
117
|
+
local_embedding = config.embedding_provider in {"", "hash", "local", "fallback", "sqlite"}
|
|
118
|
+
external = external_integration_status_no_recurse(config, env=env)
|
|
119
|
+
return (
|
|
120
|
+
not config.network_exposed
|
|
121
|
+
and not config.cors_allow_network
|
|
122
|
+
and not config.enable_telegram
|
|
123
|
+
and not config.autoload_models
|
|
124
|
+
and config.storage_engine == "sqlite"
|
|
125
|
+
and local_embedding
|
|
126
|
+
and not any(item["automatic_egress"] for item in external.values())
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def external_integration_status_no_recurse(
|
|
131
|
+
config: Config,
|
|
132
|
+
*,
|
|
133
|
+
env: Mapping[str, str],
|
|
134
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
135
|
+
return {
|
|
136
|
+
"brain_network": {"automatic_egress": _bool(env, "LATTICEAI_BRAIN_NETWORK_AUTO_PUSH", default=False)},
|
|
137
|
+
"updates": {"automatic_egress": _bool(env, "LATTICEAI_ENABLE_UPDATES", default=False)},
|
|
138
|
+
"docker": {"automatic_egress": _bool(env, "LATTICEAI_DOCKER_AUTO_START", default=False)},
|
|
139
|
+
"postgres": {"automatic_egress": config.storage_engine == "postgres" and bool(config.postgres_dsn)},
|
|
140
|
+
"external_connectors": {"automatic_egress": _bool(env, "LATTICEAI_ENABLE_EXTERNAL_CONNECTORS", default=False)},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build_product_hardening_status(
|
|
145
|
+
*,
|
|
146
|
+
config: Config,
|
|
147
|
+
portability: Any = None,
|
|
148
|
+
device_identity: Any = None,
|
|
149
|
+
env: Optional[Mapping[str, str]] = None,
|
|
150
|
+
) -> Dict[str, Any]:
|
|
151
|
+
if env is None:
|
|
152
|
+
env = os.environ
|
|
153
|
+
storage = {"available": False}
|
|
154
|
+
backup = {"available": False}
|
|
155
|
+
if portability is not None and getattr(portability, "available", lambda: False)():
|
|
156
|
+
storage = portability.storage_status()
|
|
157
|
+
backup = portability.backup_health()
|
|
158
|
+
identity = {}
|
|
159
|
+
if device_identity is not None:
|
|
160
|
+
identity = device_identity.describe()
|
|
161
|
+
data_dir = Path(config.data_dir)
|
|
162
|
+
return {
|
|
163
|
+
"version": "4.3.0",
|
|
164
|
+
"startup": {
|
|
165
|
+
"local_only_default": default_startup_local_only(config, env=env),
|
|
166
|
+
"host": config.host,
|
|
167
|
+
"port": config.port,
|
|
168
|
+
"network_exposed": config.network_exposed,
|
|
169
|
+
"auth_required": config.require_auth,
|
|
170
|
+
"cors_network_allowed": config.cors_allow_network,
|
|
171
|
+
},
|
|
172
|
+
"desktop": {
|
|
173
|
+
"sidecar_lifecycle": "managed",
|
|
174
|
+
"restart_supported": True,
|
|
175
|
+
"shutdown_supported": True,
|
|
176
|
+
"updater": {
|
|
177
|
+
"enabled": _bool(env, "LATTICEAI_ENABLE_UPDATES", default=False),
|
|
178
|
+
"limitation": "No external update checks run unless explicitly enabled by policy.",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
"first_run": {
|
|
182
|
+
"data_dir": str(data_dir),
|
|
183
|
+
"data_dir_exists": data_dir.exists(),
|
|
184
|
+
"python_available": shutil.which("python3") is not None or shutil.which("python") is not None,
|
|
185
|
+
"docker_available": shutil.which("docker") is not None,
|
|
186
|
+
"docker_required": False,
|
|
187
|
+
"postgres_required": False,
|
|
188
|
+
},
|
|
189
|
+
"privacy": external_integration_status(config, env=env),
|
|
190
|
+
"storage": storage,
|
|
191
|
+
"backup": backup,
|
|
192
|
+
"device_identity": identity,
|
|
193
|
+
"permissions": {
|
|
194
|
+
"export_requires_admin": True,
|
|
195
|
+
"import_requires_admin": True,
|
|
196
|
+
"restore_requires_admin": True,
|
|
197
|
+
"destructive_restore_requires_confirmation": True,
|
|
198
|
+
"workspace_isolation_enforced": True,
|
|
199
|
+
"audit_log_visible_to_admin": True,
|
|
200
|
+
},
|
|
201
|
+
"failure_policy": {
|
|
202
|
+
"archive_corruption": "fail_closed",
|
|
203
|
+
"partial_archive": "fail_closed",
|
|
204
|
+
"signature_mismatch": "fail_closed",
|
|
205
|
+
"unsupported_version": "fail_closed",
|
|
206
|
+
"missing_docker": "honest_unavailable",
|
|
207
|
+
"missing_postgres": "honest_unavailable",
|
|
208
|
+
"permission_denied": "honest_error",
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
__all__ = [
|
|
214
|
+
"build_product_hardening_status",
|
|
215
|
+
"default_startup_local_only",
|
|
216
|
+
"external_integration_status",
|
|
217
|
+
]
|
|
@@ -19,7 +19,7 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
WORKSPACE_OS_VERSION = "4.
|
|
22
|
+
WORKSPACE_OS_VERSION = "4.3.0"
|
|
23
23
|
|
|
24
24
|
# Workspace types separate single-user Personal workspaces from shared
|
|
25
25
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
|
@@ -19,9 +19,16 @@ import shutil
|
|
|
19
19
|
import tempfile
|
|
20
20
|
import zipfile
|
|
21
21
|
from datetime import datetime, timezone
|
|
22
|
-
from pathlib import Path
|
|
22
|
+
from pathlib import Path, PurePosixPath
|
|
23
23
|
from typing import Any, Dict, Optional
|
|
24
24
|
|
|
25
|
+
from lattice_brain.archive import BrainArchivePaths, EncryptedBrainArchive
|
|
26
|
+
from lattice_brain.storage import (
|
|
27
|
+
DockerPostgresWizard,
|
|
28
|
+
PostgresEngine,
|
|
29
|
+
SQLiteToPostgresMigrator,
|
|
30
|
+
)
|
|
31
|
+
|
|
25
32
|
FORMAT = "latticeai.kg.export"
|
|
26
33
|
FORMAT_VERSION = 1
|
|
27
34
|
BACKUP_FORMAT = "latticeai.kg.backup"
|
|
@@ -43,6 +50,13 @@ def _sha256_file(path: Path) -> str:
|
|
|
43
50
|
return h.hexdigest()
|
|
44
51
|
|
|
45
52
|
|
|
53
|
+
def _safe_zip_names(names) -> None:
|
|
54
|
+
for name in names:
|
|
55
|
+
path = PurePosixPath(name)
|
|
56
|
+
if path.is_absolute() or ".." in path.parts:
|
|
57
|
+
raise ValueError(f"Backup archive contains unsafe path: {name}")
|
|
58
|
+
|
|
59
|
+
|
|
46
60
|
class KGPortabilityService:
|
|
47
61
|
def __init__(self, *, knowledge_graph: Any, data_dir, enable_graph: bool = True, device_identity: Any = None) -> None:
|
|
48
62
|
self._kg = knowledge_graph
|
|
@@ -95,7 +109,7 @@ class KGPortabilityService:
|
|
|
95
109
|
origin = "unsigned-legacy"
|
|
96
110
|
signature = artifact.get("signature")
|
|
97
111
|
if signature:
|
|
98
|
-
from
|
|
112
|
+
from lattice_brain.identity import verify_manifest
|
|
99
113
|
|
|
100
114
|
if not verify_manifest(artifact.get("header") or {}, signature):
|
|
101
115
|
raise ValueError("Bundle signature verification failed — refusing to import.")
|
|
@@ -149,13 +163,23 @@ class KGPortabilityService:
|
|
|
149
163
|
zf.write(f, f"blobs/{f.relative_to(blob_dir)}")
|
|
150
164
|
return {"path": str(dest), "bytes": dest.stat().st_size, "manifest": manifest}
|
|
151
165
|
|
|
152
|
-
def restore(
|
|
166
|
+
def restore(
|
|
167
|
+
self,
|
|
168
|
+
archive_path,
|
|
169
|
+
*,
|
|
170
|
+
verify: bool = True,
|
|
171
|
+
dry_run: bool = False,
|
|
172
|
+
confirm: bool = False,
|
|
173
|
+
) -> Dict[str, Any]:
|
|
153
174
|
self._require()
|
|
154
175
|
archive = Path(archive_path)
|
|
155
176
|
if not archive.exists():
|
|
156
177
|
raise FileNotFoundError(f"Backup archive not found: {archive}")
|
|
178
|
+
if not dry_run and not confirm:
|
|
179
|
+
raise ValueError("Explicit confirmation is required before restoring a Knowledge Graph backup.")
|
|
157
180
|
with zipfile.ZipFile(archive) as zf:
|
|
158
181
|
names = zf.namelist()
|
|
182
|
+
_safe_zip_names(names)
|
|
159
183
|
if "knowledge_graph.sqlite" not in names:
|
|
160
184
|
raise ValueError("Archive is missing knowledge_graph.sqlite.")
|
|
161
185
|
manifest = json.loads(zf.read("manifest.json")) if "manifest.json" in names else {}
|
|
@@ -166,6 +190,18 @@ class KGPortabilityService:
|
|
|
166
190
|
if verify and manifest.get("db_sha256"):
|
|
167
191
|
if _sha256_file(db_src) != manifest["db_sha256"]:
|
|
168
192
|
raise ValueError("Backup integrity check failed (db sha256 mismatch).")
|
|
193
|
+
if dry_run:
|
|
194
|
+
return {
|
|
195
|
+
"restored": False,
|
|
196
|
+
"dry_run": True,
|
|
197
|
+
"verified": True,
|
|
198
|
+
"manifest": manifest,
|
|
199
|
+
"planned": {
|
|
200
|
+
"database": str(self._kg.db_path),
|
|
201
|
+
"blobs": str(self._kg.blob_dir),
|
|
202
|
+
"archive": str(archive),
|
|
203
|
+
},
|
|
204
|
+
}
|
|
169
205
|
db_dest = Path(self._kg.db_path)
|
|
170
206
|
blob_dest = Path(self._kg.blob_dir)
|
|
171
207
|
db_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -189,6 +225,113 @@ class KGPortabilityService:
|
|
|
189
225
|
"nodes": sum(stats.get("nodes", {}).values()),
|
|
190
226
|
}
|
|
191
227
|
|
|
228
|
+
def verify_backup(self, archive_path) -> Dict[str, Any]:
|
|
229
|
+
archive = Path(archive_path)
|
|
230
|
+
if not archive.exists():
|
|
231
|
+
return {"ok": False, "path": str(archive), "errors": [f"Backup archive not found: {archive}"]}
|
|
232
|
+
try:
|
|
233
|
+
with zipfile.ZipFile(archive) as zf:
|
|
234
|
+
names = zf.namelist()
|
|
235
|
+
_safe_zip_names(names)
|
|
236
|
+
if "knowledge_graph.sqlite" not in names:
|
|
237
|
+
raise ValueError("Archive is missing knowledge_graph.sqlite.")
|
|
238
|
+
manifest = json.loads(zf.read("manifest.json")) if "manifest.json" in names else {}
|
|
239
|
+
with tempfile.TemporaryDirectory() as tmp_s:
|
|
240
|
+
tmp = Path(tmp_s)
|
|
241
|
+
zf.extract("knowledge_graph.sqlite", tmp)
|
|
242
|
+
db_src = tmp / "knowledge_graph.sqlite"
|
|
243
|
+
if manifest.get("db_sha256") and _sha256_file(db_src) != manifest["db_sha256"]:
|
|
244
|
+
raise ValueError("Backup integrity check failed (db sha256 mismatch).")
|
|
245
|
+
return {"ok": True, "path": str(archive), "manifest": manifest, "errors": []}
|
|
246
|
+
except (ValueError, zipfile.BadZipFile, OSError, json.JSONDecodeError) as exc:
|
|
247
|
+
return {"ok": False, "path": str(archive), "errors": [str(exc)]}
|
|
248
|
+
|
|
249
|
+
# ── encrypted .latticebrain archive ───────────────────────────────────
|
|
250
|
+
def encrypted_archive(self, dest_path=None, *, passphrase: str) -> Dict[str, Any]:
|
|
251
|
+
self._require()
|
|
252
|
+
self._exports_dir.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
dest = Path(dest_path) if dest_path else self._exports_dir / f"brain-{_stamp()}.latticebrain"
|
|
254
|
+
metadata = {
|
|
255
|
+
"storage": self.storage_status().get("active", {}),
|
|
256
|
+
"snapshot": self.snapshot_metadata(),
|
|
257
|
+
"device_identity": self._identity.describe() if self._identity is not None else {},
|
|
258
|
+
"provenance": {"exported_at": _now_iso(), "source": "kg-portability"},
|
|
259
|
+
}
|
|
260
|
+
archive = EncryptedBrainArchive(
|
|
261
|
+
BrainArchivePaths(
|
|
262
|
+
db_path=Path(self._kg.db_path),
|
|
263
|
+
blob_dir=Path(self._kg.blob_dir),
|
|
264
|
+
data_dir=self._data_dir,
|
|
265
|
+
metadata=metadata,
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
return archive.create(dest, passphrase=passphrase)
|
|
269
|
+
|
|
270
|
+
def inspect_encrypted_archive(self, archive_path, *, passphrase: Optional[str] = None) -> Dict[str, Any]:
|
|
271
|
+
archive = EncryptedBrainArchive(
|
|
272
|
+
BrainArchivePaths(
|
|
273
|
+
db_path=Path(self._kg.db_path),
|
|
274
|
+
blob_dir=Path(self._kg.blob_dir),
|
|
275
|
+
data_dir=self._data_dir,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
return archive.inspect(Path(archive_path), passphrase=passphrase)
|
|
279
|
+
|
|
280
|
+
def verify_encrypted_archive(self, archive_path, *, passphrase: str) -> Dict[str, Any]:
|
|
281
|
+
archive = EncryptedBrainArchive(
|
|
282
|
+
BrainArchivePaths(
|
|
283
|
+
db_path=Path(self._kg.db_path),
|
|
284
|
+
blob_dir=Path(self._kg.blob_dir),
|
|
285
|
+
data_dir=self._data_dir,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
return archive.verify(Path(archive_path), passphrase=passphrase)
|
|
289
|
+
|
|
290
|
+
def restore_encrypted_archive(
|
|
291
|
+
self,
|
|
292
|
+
archive_path,
|
|
293
|
+
*,
|
|
294
|
+
passphrase: str,
|
|
295
|
+
dry_run: bool = False,
|
|
296
|
+
confirm: bool = False,
|
|
297
|
+
) -> Dict[str, Any]:
|
|
298
|
+
self._require()
|
|
299
|
+
archive = EncryptedBrainArchive(
|
|
300
|
+
BrainArchivePaths(
|
|
301
|
+
db_path=Path(self._kg.db_path),
|
|
302
|
+
blob_dir=Path(self._kg.blob_dir),
|
|
303
|
+
data_dir=self._data_dir,
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
return archive.restore(
|
|
307
|
+
Path(archive_path),
|
|
308
|
+
passphrase=passphrase,
|
|
309
|
+
target=BrainArchivePaths(
|
|
310
|
+
db_path=Path(self._kg.db_path),
|
|
311
|
+
blob_dir=Path(self._kg.blob_dir),
|
|
312
|
+
data_dir=self._data_dir,
|
|
313
|
+
),
|
|
314
|
+
dry_run=dry_run,
|
|
315
|
+
confirm=confirm,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def import_encrypted_archive(
|
|
319
|
+
self,
|
|
320
|
+
archive_path,
|
|
321
|
+
*,
|
|
322
|
+
passphrase: str,
|
|
323
|
+
dry_run: bool = False,
|
|
324
|
+
confirm: bool = False,
|
|
325
|
+
) -> Dict[str, Any]:
|
|
326
|
+
result = self.restore_encrypted_archive(
|
|
327
|
+
archive_path,
|
|
328
|
+
passphrase=passphrase,
|
|
329
|
+
dry_run=dry_run,
|
|
330
|
+
confirm=confirm,
|
|
331
|
+
)
|
|
332
|
+
result["operation"] = "import"
|
|
333
|
+
return result
|
|
334
|
+
|
|
192
335
|
# ── status surface ───────────────────────────────────────────────────────
|
|
193
336
|
def snapshot_metadata(self) -> Dict[str, Any]:
|
|
194
337
|
if not self.available():
|
|
@@ -198,8 +341,89 @@ class KGPortabilityService:
|
|
|
198
341
|
**self._kg.schema_versions(),
|
|
199
342
|
"stats": self._kg.stats(),
|
|
200
343
|
"provenance": self._kg.provenance_stats(),
|
|
344
|
+
"storage": (
|
|
345
|
+
self._kg.storage_engine.capabilities().as_dict()
|
|
346
|
+
if getattr(self._kg, "storage_engine", None) is not None
|
|
347
|
+
else {"engine": "sqlite", "available": True}
|
|
348
|
+
),
|
|
201
349
|
}
|
|
202
350
|
|
|
351
|
+
def storage_status(self) -> Dict[str, Any]:
|
|
352
|
+
if not self.available():
|
|
353
|
+
return {"available": False}
|
|
354
|
+
return {
|
|
355
|
+
"available": True,
|
|
356
|
+
"active": (
|
|
357
|
+
self._kg.storage_engine.capabilities().as_dict()
|
|
358
|
+
if getattr(self._kg, "storage_engine", None) is not None
|
|
359
|
+
else {"engine": "sqlite", "available": True}
|
|
360
|
+
),
|
|
361
|
+
"postgres": PostgresEngine("", schema="lattice_brain").capabilities().as_dict(),
|
|
362
|
+
"backup_health": self.backup_health(),
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
def backup_health(self) -> Dict[str, Any]:
|
|
366
|
+
self._exports_dir.mkdir(parents=True, exist_ok=True)
|
|
367
|
+
backups = sorted(
|
|
368
|
+
[
|
|
369
|
+
p for p in self._exports_dir.glob("*")
|
|
370
|
+
if p.is_file() and (p.suffix == ".zip" or p.suffix == ".latticebrain")
|
|
371
|
+
],
|
|
372
|
+
key=lambda p: p.stat().st_mtime,
|
|
373
|
+
reverse=True,
|
|
374
|
+
)
|
|
375
|
+
latest = backups[0] if backups else None
|
|
376
|
+
return {
|
|
377
|
+
"available": True,
|
|
378
|
+
"directory": str(self._exports_dir),
|
|
379
|
+
"count": len(backups),
|
|
380
|
+
"latest": str(latest) if latest else None,
|
|
381
|
+
"latest_bytes": latest.stat().st_size if latest else 0,
|
|
382
|
+
"encrypted_archives": sum(1 for p in backups if p.suffix == ".latticebrain"),
|
|
383
|
+
"zip_backups": sum(1 for p in backups if p.suffix == ".zip"),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
def postgres_docker_setup(
|
|
387
|
+
self,
|
|
388
|
+
*,
|
|
389
|
+
consent: bool,
|
|
390
|
+
dry_run: bool = False,
|
|
391
|
+
port: int = 5432,
|
|
392
|
+
) -> Dict[str, Any]:
|
|
393
|
+
wizard = DockerPostgresWizard(self._data_dir / "postgres", port=port)
|
|
394
|
+
return wizard.start(consent=consent, dry_run=dry_run)
|
|
395
|
+
|
|
396
|
+
def migrate_sqlite_to_postgres(
|
|
397
|
+
self,
|
|
398
|
+
*,
|
|
399
|
+
dsn: str,
|
|
400
|
+
schema: str = "lattice_brain",
|
|
401
|
+
dry_run: bool = True,
|
|
402
|
+
) -> Dict[str, Any]:
|
|
403
|
+
self._require()
|
|
404
|
+
if not dsn:
|
|
405
|
+
raise ValueError("Postgres DSN is required for SQLite to Postgres migration.")
|
|
406
|
+
migrator = SQLiteToPostgresMigrator(
|
|
407
|
+
Path(self._kg.db_path),
|
|
408
|
+
PostgresEngine(dsn, schema=schema),
|
|
409
|
+
)
|
|
410
|
+
if dry_run:
|
|
411
|
+
return migrator.migrate(dry_run=True)
|
|
412
|
+
backup = self.backup()
|
|
413
|
+
verification = self.verify_backup(backup["path"])
|
|
414
|
+
if not verification.get("ok"):
|
|
415
|
+
raise RuntimeError(
|
|
416
|
+
"Pre-migration backup verification failed; Postgres migration was not started: "
|
|
417
|
+
+ "; ".join(verification.get("errors") or [])
|
|
418
|
+
)
|
|
419
|
+
result = migrator.migrate(dry_run=False)
|
|
420
|
+
result["pre_migration_backup"] = {
|
|
421
|
+
"path": backup["path"],
|
|
422
|
+
"verified": True,
|
|
423
|
+
"manifest": backup.get("manifest"),
|
|
424
|
+
}
|
|
425
|
+
return result
|
|
426
|
+
|
|
203
427
|
def recent_ingestions(self, *, limit: int = 50, source_type: Optional[str] = None) -> Dict[str, Any]:
|
|
204
428
|
"""Recent provenance records (newest first) for the ingestion-sources UI."""
|
|
205
429
|
if not self.available():
|
package/ltcai_cli.py
CHANGED
|
@@ -269,9 +269,10 @@ def main() -> None:
|
|
|
269
269
|
|
|
270
270
|
# Telegram startup notification (local start, tunnel handled separately inside _start_tunnel)
|
|
271
271
|
if not args.tunnel:
|
|
272
|
+
_tg_enabled = os.getenv("LATTICEAI_ENABLE_TELEGRAM", "").strip().lower() in ("1", "true", "yes", "on")
|
|
272
273
|
_tg_token = os.getenv("LATTICEAI_TELEGRAM_BOT_TOKEN", "")
|
|
273
274
|
_tg_chat = os.getenv("LATTICEAI_TELEGRAM_CHAT_ID", "")
|
|
274
|
-
if _tg_token and _tg_chat:
|
|
275
|
+
if _tg_enabled and _tg_token and _tg_chat:
|
|
275
276
|
_local_msg = (
|
|
276
277
|
f"✅ Lattice AI 시작됨\n\n"
|
|
277
278
|
f"🏠 로컬: http://localhost:{args.port}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, signed brain exchange)",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"desktop:tauri:check": "cd src-tauri && cargo check",
|
|
42
42
|
"desktop:electron": "electron desktop/electron/main.cjs",
|
|
43
43
|
"package:vsix": "node scripts/build_vsix.mjs",
|
|
44
|
-
"release:artifacts": "npm run build:assets && npm run build:python && npm pack && npm run package:vsix",
|
|
45
|
-
"release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz",
|
|
44
|
+
"release:artifacts": "node scripts/clean_release_artifacts.mjs $npm_package_version && npm run build:assets && npm run build:python && npm pack && npm run package:vsix && npm run desktop:tauri:build",
|
|
45
|
+
"release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz --require-dmg",
|
|
46
46
|
"publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
|
|
47
47
|
"publish:pypi": "npm run build:python && node scripts/run_python.mjs -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
|
|
48
48
|
"publish:vscode": "cd vscode-extension && npm run package:vsix && npm run publish:vscode",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"tools/",
|
|
81
81
|
"mcp_registry.py",
|
|
82
82
|
"latticeai/**/*.py",
|
|
83
|
+
"lattice_brain/**/*.py",
|
|
83
84
|
"skills/",
|
|
84
85
|
"static/favicon.ico",
|
|
85
86
|
"static/manifest.json",
|
package/scripts/bump_version.py
CHANGED
|
@@ -23,6 +23,7 @@ REPO = Path(__file__).resolve().parents[1]
|
|
|
23
23
|
# (path, kind, pattern) — pattern groups: (prefix, version)
|
|
24
24
|
TARGETS = [
|
|
25
25
|
("latticeai/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
|
|
26
|
+
("lattice_brain/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
|
|
26
27
|
("latticeai/core/workspace_os.py", "regex", r'(WORKSPACE_OS_VERSION = ")([^"]+)(")'),
|
|
27
28
|
("latticeai/core/marketplace.py", "regex", r'(MARKETPLACE_VERSION = ")([^"]+)(")'),
|
|
28
29
|
("latticeai/core/multi_agent.py", "regex", r'(MULTI_AGENT_VERSION = ")([^"]+)(")'),
|
|
@@ -31,6 +32,8 @@ TARGETS = [
|
|
|
31
32
|
("package-lock.json", "package-lock", None),
|
|
32
33
|
("vscode-extension/package.json", "json", "version"),
|
|
33
34
|
("vscode-extension/package-lock.json", "package-lock", None),
|
|
35
|
+
("src-tauri/Cargo.toml", "regex", r'(^version = ")([^"]+)(")'),
|
|
36
|
+
("src-tauri/tauri.conf.json", "json", "version"),
|
|
34
37
|
("static/app/asset-manifest.json", "json", "version"),
|
|
35
38
|
]
|
|
36
39
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const repo = join(import.meta.dirname, "..");
|
|
6
|
+
const version = process.argv[2] || process.env.npm_package_version;
|
|
7
|
+
|
|
8
|
+
if (!version || !/^\d+\.\d+\.\d+([.-][0-9A-Za-z.]+)?$/.test(version)) {
|
|
9
|
+
console.error("usage: node scripts/clean_release_artifacts.mjs <version>");
|
|
10
|
+
process.exit(2);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const targets = [
|
|
14
|
+
join(repo, "dist", `ltcai-${version}-py3-none-any.whl`),
|
|
15
|
+
join(repo, "dist", `ltcai-${version}.tar.gz`),
|
|
16
|
+
join(repo, "dist", `ltcai-${version}.vsix`),
|
|
17
|
+
join(repo, `ltcai-${version}.tgz`),
|
|
18
|
+
join(repo, "src-tauri", "target", "release", "bundle", "dmg", `Lattice AI_${version}_aarch64.dmg`),
|
|
19
|
+
join(repo, "src-tauri", "target", "release", "bundle", "macos", "Lattice AI.app"),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const target of targets) {
|
|
23
|
+
if (existsSync(target)) {
|
|
24
|
+
rmSync(target, { recursive: true, force: true });
|
|
25
|
+
console.log(`removed ${target}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -72,6 +72,16 @@ const requiredPaths = [
|
|
|
72
72
|
"/workflows/api/definitions",
|
|
73
73
|
"/workspace/os",
|
|
74
74
|
"/models",
|
|
75
|
+
"/api/brain/storage",
|
|
76
|
+
"/api/brain/storage/postgres/docker",
|
|
77
|
+
"/api/brain/storage/migrate-postgres",
|
|
78
|
+
"/api/knowledge-graph/archive",
|
|
79
|
+
"/api/knowledge-graph/archive/inspect",
|
|
80
|
+
"/api/knowledge-graph/archive/verify",
|
|
81
|
+
"/api/knowledge-graph/archive/import",
|
|
82
|
+
"/api/knowledge-graph/archive/restore",
|
|
83
|
+
"/api/knowledge-graph/backup-health",
|
|
84
|
+
"/admin/product-hardening",
|
|
75
85
|
];
|
|
76
86
|
if (openapiPaths.length < 300) fail(`OpenAPI path count too low: ${openapiPaths.length}`);
|
|
77
87
|
for (const path of requiredPaths) {
|