ltcai 4.2.0 → 4.3.1
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 +28 -21
- package/bin/ltcai.js +6 -2
- package/docs/CHANGELOG.md +72 -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 +19 -25
- package/frontend/openapi.json +213 -1
- package/frontend/src/App.tsx +15 -1
- package/frontend/src/api/client.ts +26 -1
- package/frontend/src/api/openapi.ts +268 -0
- package/frontend/src/pages/Act.tsx +63 -2
- package/frontend/src/pages/Library.tsx +9 -3
- package/frontend/src/pages/System.tsx +58 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +360 -47
- package/lattice_brain/storage/sqlite.py +15 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +11 -0
- package/latticeai/api/agents.py +3 -1
- package/latticeai/api/models.py +66 -18
- package/latticeai/api/portability.py +59 -2
- package/latticeai/app_factory.py +9 -0
- package/latticeai/brain/projection.py +12 -2
- package/latticeai/brain/retrieval.py +10 -0
- package/latticeai/brain/store.py +6 -1
- package/latticeai/core/config.py +4 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/product_hardening.py +218 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/agent_runtime.py +52 -12
- package/latticeai/services/kg_portability.py +147 -4
- package/latticeai/services/model_runtime.py +83 -2
- package/ltcai_cli.py +16 -4
- package/package.json +5 -4
- package/requirements.txt +17 -0
- package/scripts/clean_release_artifacts.mjs +27 -0
- package/scripts/lint_frontend.mjs +5 -0
- package/scripts/validate_release_artifacts.py +10 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +356 -24
- package/src-tauri/tauri.conf.json +20 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/{index-C_HAkbAg.js → index-BhPuj8rT.js} +45 -45
- package/static/app/assets/index-BhPuj8rT.js.map +1 -0
- package/static/app/assets/{index-CDjiH_se.css → index-yZswHE3d.css} +1 -1
- package/static/app/index.html +2 -2
- package/static/app/assets/index-C_HAkbAg.js.map +0 -1
package/latticeai/api/models.py
CHANGED
|
@@ -62,6 +62,7 @@ class LoadModelRequest(BaseModel):
|
|
|
62
62
|
user_email: Optional[str] = None
|
|
63
63
|
adapter_path: Optional[str] = None
|
|
64
64
|
draft_model_id: Optional[str] = None
|
|
65
|
+
allow_download: bool = False
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
class InstallEngineRequest(BaseModel):
|
|
@@ -82,6 +83,7 @@ class PrepareModelRequest(BaseModel):
|
|
|
82
83
|
model: str
|
|
83
84
|
engine: Optional[str] = None
|
|
84
85
|
user_email: Optional[str] = None
|
|
86
|
+
allow_download: bool = False
|
|
85
87
|
|
|
86
88
|
|
|
87
89
|
class VerifyCloudRequest(BaseModel):
|
|
@@ -127,9 +129,57 @@ def create_models_router(
|
|
|
127
129
|
REQUIRE_AUTH = require_auth
|
|
128
130
|
_list_compat_profiles = list_compat_profiles
|
|
129
131
|
|
|
130
|
-
def _recommended_with_engine_options(
|
|
132
|
+
def _recommended_with_engine_options(
|
|
133
|
+
items: List[Dict[str, object]],
|
|
134
|
+
engines: Optional[List[Dict[str, object]]] = None,
|
|
135
|
+
loaded_ids: Optional[List[str]] = None,
|
|
136
|
+
current_id: Optional[str] = None,
|
|
137
|
+
) -> List[Dict[str, object]]:
|
|
138
|
+
engine_lookup = {str(engine.get("id") or ""): engine for engine in engines or []}
|
|
139
|
+
model_lookup: Dict[str, Dict[str, object]] = {}
|
|
140
|
+
for engine in engines or []:
|
|
141
|
+
engine_id = str(engine.get("id") or "")
|
|
142
|
+
for model in engine.get("models") or []:
|
|
143
|
+
if isinstance(model, dict):
|
|
144
|
+
model_lookup[str(model.get("id") or "")] = {**model, "_engine": engine_id}
|
|
145
|
+
loaded = set(loaded_ids or [])
|
|
131
146
|
out: List[Dict[str, object]] = []
|
|
132
147
|
for item in items:
|
|
148
|
+
short_id = str(item["id"]).lower()
|
|
149
|
+
aliases = MODEL_ENGINE_ALIASES.get(short_id) or {}
|
|
150
|
+
options: List[Dict[str, str]] = []
|
|
151
|
+
for engine_name in ("local_mlx", "ollama", "lmstudio", "llamacpp", "vllm"):
|
|
152
|
+
real = aliases.get(engine_name)
|
|
153
|
+
if not real:
|
|
154
|
+
continue
|
|
155
|
+
options.append({
|
|
156
|
+
"engine": engine_name,
|
|
157
|
+
"model_id": real,
|
|
158
|
+
"load_id": real if engine_name == "local_mlx" else f"{engine_name}:{real}",
|
|
159
|
+
})
|
|
160
|
+
if not options:
|
|
161
|
+
options.append({"engine": "local_mlx", "model_id": item["id"], "load_id": item["id"]})
|
|
162
|
+
recommended_engine = options[0]["engine"]
|
|
163
|
+
load_id = options[0]["load_id"]
|
|
164
|
+
engine_info = engine_lookup.get(recommended_engine) or {}
|
|
165
|
+
model_info = model_lookup.get(load_id) or model_lookup.get(str(item["id"])) or {}
|
|
166
|
+
pulled = bool(model_info.get("pulled"))
|
|
167
|
+
is_loaded = load_id in loaded or str(item["id"]) in loaded or current_id in {load_id, str(item["id"])}
|
|
168
|
+
engine_installed = bool(engine_info.get("installed"))
|
|
169
|
+
pullable = bool(item.get("pullable", True))
|
|
170
|
+
download_required = bool(pullable and not pulled and not is_loaded)
|
|
171
|
+
if is_loaded:
|
|
172
|
+
load_status = "loaded"
|
|
173
|
+
unavailable_reason = None
|
|
174
|
+
elif not engine_installed:
|
|
175
|
+
load_status = "unavailable"
|
|
176
|
+
unavailable_reason = f"{engine_info.get('name') or recommended_engine} runtime is not installed."
|
|
177
|
+
elif download_required:
|
|
178
|
+
load_status = "download_required"
|
|
179
|
+
unavailable_reason = "Model files are not present locally. Downloads are opt-in and never start from token/model presence alone."
|
|
180
|
+
else:
|
|
181
|
+
load_status = "ready"
|
|
182
|
+
unavailable_reason = None
|
|
133
183
|
base = {
|
|
134
184
|
"id": item["id"],
|
|
135
185
|
"name": item["name"],
|
|
@@ -144,23 +194,15 @@ def create_models_router(
|
|
|
144
194
|
"run_location": item.get("run_location"),
|
|
145
195
|
"internet_requirement": item.get("internet_requirement"),
|
|
146
196
|
"source_display_order": item.get("source_display_order"),
|
|
197
|
+
"pulled": pulled,
|
|
198
|
+
"download_required": download_required,
|
|
199
|
+
"load_available": is_loaded or (engine_installed and not download_required),
|
|
200
|
+
"load_status": load_status,
|
|
201
|
+
"unavailable_reason": unavailable_reason,
|
|
147
202
|
}
|
|
148
|
-
short_id = str(item["id"]).lower()
|
|
149
|
-
aliases = MODEL_ENGINE_ALIASES.get(short_id) or {}
|
|
150
|
-
options: List[Dict[str, str]] = []
|
|
151
|
-
for engine_name in ("local_mlx", "ollama", "lmstudio", "llamacpp", "vllm"):
|
|
152
|
-
real = aliases.get(engine_name)
|
|
153
|
-
if not real:
|
|
154
|
-
continue
|
|
155
|
-
options.append({
|
|
156
|
-
"engine": engine_name,
|
|
157
|
-
"model_id": real,
|
|
158
|
-
"load_id": real if engine_name == "local_mlx" else f"{engine_name}:{real}",
|
|
159
|
-
})
|
|
160
|
-
if not options:
|
|
161
|
-
options.append({"engine": "local_mlx", "model_id": item["id"], "load_id": item["id"]})
|
|
162
203
|
base["engine_options"] = options
|
|
163
|
-
base["recommended_engine"] =
|
|
204
|
+
base["recommended_engine"] = recommended_engine
|
|
205
|
+
base["recommended_load_id"] = load_id
|
|
164
206
|
out.append(base)
|
|
165
207
|
return out
|
|
166
208
|
|
|
@@ -232,6 +274,7 @@ def create_models_router(
|
|
|
232
274
|
require_user(request)
|
|
233
275
|
return await prepare_and_load_model(
|
|
234
276
|
req.model, request, engine=req.engine, user_email=req.user_email,
|
|
277
|
+
allow_download=req.allow_download,
|
|
235
278
|
)
|
|
236
279
|
|
|
237
280
|
@router.post("/engines/prepare-model/stream")
|
|
@@ -242,6 +285,7 @@ def create_models_router(
|
|
|
242
285
|
try:
|
|
243
286
|
async for chunk in prepare_and_load_model_stream(
|
|
244
287
|
req.model, request, engine=req.engine, user_email=req.user_email,
|
|
288
|
+
allow_download=req.allow_download,
|
|
245
289
|
):
|
|
246
290
|
yield chunk
|
|
247
291
|
except HTTPException as exc:
|
|
@@ -287,10 +331,13 @@ def create_models_router(
|
|
|
287
331
|
|
|
288
332
|
@router.get("/models")
|
|
289
333
|
async def list_models():
|
|
334
|
+
engines = await asyncio.to_thread(engine_status)
|
|
290
335
|
recommended = _recommended_with_engine_options(
|
|
291
|
-
list(filter_lower_family_versions(ENGINE_MODEL_CATALOG.get("local_mlx", [])))
|
|
336
|
+
list(filter_lower_family_versions(ENGINE_MODEL_CATALOG.get("local_mlx", []))),
|
|
337
|
+
engines=engines,
|
|
338
|
+
loaded_ids=_router.loaded_model_ids,
|
|
339
|
+
current_id=_router.current_model_id,
|
|
292
340
|
)
|
|
293
|
-
engines = await asyncio.to_thread(engine_status)
|
|
294
341
|
return {
|
|
295
342
|
"recommended": recommended,
|
|
296
343
|
"cloud": _router.detected_cloud_models(),
|
|
@@ -319,6 +366,7 @@ def create_models_router(
|
|
|
319
366
|
return await prepare_and_load_model(
|
|
320
367
|
model_id, request, engine=req.engine, user_email=req.user_email,
|
|
321
368
|
adapter_path=req.adapter_path, draft_model_id=req.draft_model_id,
|
|
369
|
+
allow_download=req.allow_download,
|
|
322
370
|
)
|
|
323
371
|
except HTTPException:
|
|
324
372
|
raise
|
|
@@ -26,6 +26,8 @@ class BackupRequest(BaseModel):
|
|
|
26
26
|
class RestoreRequest(BaseModel):
|
|
27
27
|
path: str
|
|
28
28
|
verify: bool = True
|
|
29
|
+
dry_run: bool = False
|
|
30
|
+
confirm: bool = False
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class EncryptedArchiveRequest(BaseModel):
|
|
@@ -36,6 +38,18 @@ class EncryptedArchiveRequest(BaseModel):
|
|
|
36
38
|
class EncryptedRestoreRequest(BaseModel):
|
|
37
39
|
path: str
|
|
38
40
|
passphrase: str
|
|
41
|
+
dry_run: bool = False
|
|
42
|
+
confirm: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EncryptedInspectRequest(BaseModel):
|
|
46
|
+
path: str
|
|
47
|
+
passphrase: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EncryptedVerifyRequest(BaseModel):
|
|
51
|
+
path: str
|
|
52
|
+
passphrase: str
|
|
39
53
|
|
|
40
54
|
|
|
41
55
|
class DockerPostgresRequest(BaseModel):
|
|
@@ -74,6 +88,12 @@ def create_portability_router(
|
|
|
74
88
|
_require_service()
|
|
75
89
|
return service.storage_status()
|
|
76
90
|
|
|
91
|
+
@router.get("/api/knowledge-graph/backup-health")
|
|
92
|
+
async def backup_health(request: Request):
|
|
93
|
+
require_user(request)
|
|
94
|
+
_require_service()
|
|
95
|
+
return service.backup_health()
|
|
96
|
+
|
|
77
97
|
@router.get("/api/knowledge-graph/provenance")
|
|
78
98
|
async def recent_provenance(request: Request, limit: int = 50, source_type: Optional[str] = None):
|
|
79
99
|
"""Recent ingestions (provenance trail) for the ingestion-sources UI."""
|
|
@@ -114,7 +134,7 @@ def create_portability_router(
|
|
|
114
134
|
require_admin(request)
|
|
115
135
|
_require_service()
|
|
116
136
|
try:
|
|
117
|
-
return service.restore(req.path, verify=req.verify)
|
|
137
|
+
return service.restore(req.path, verify=req.verify, dry_run=req.dry_run, confirm=req.confirm)
|
|
118
138
|
except (ValueError, FileNotFoundError) as exc:
|
|
119
139
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
120
140
|
|
|
@@ -127,12 +147,49 @@ def create_portability_router(
|
|
|
127
147
|
except (ValueError, FileNotFoundError) as exc:
|
|
128
148
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
129
149
|
|
|
150
|
+
@router.post("/api/knowledge-graph/archive/inspect")
|
|
151
|
+
async def inspect_encrypted_archive(req: EncryptedInspectRequest, request: Request):
|
|
152
|
+
require_admin(request)
|
|
153
|
+
_require_service()
|
|
154
|
+
try:
|
|
155
|
+
return service.inspect_encrypted_archive(req.path, passphrase=req.passphrase)
|
|
156
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
157
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
158
|
+
|
|
159
|
+
@router.post("/api/knowledge-graph/archive/verify")
|
|
160
|
+
async def verify_encrypted_archive(req: EncryptedVerifyRequest, request: Request):
|
|
161
|
+
require_admin(request)
|
|
162
|
+
_require_service()
|
|
163
|
+
result = service.verify_encrypted_archive(req.path, passphrase=req.passphrase)
|
|
164
|
+
if not result.get("ok"):
|
|
165
|
+
raise HTTPException(status_code=400, detail="; ".join(result.get("errors") or ["Archive verification failed."]))
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
@router.post("/api/knowledge-graph/archive/import")
|
|
169
|
+
async def import_encrypted_archive(req: EncryptedRestoreRequest, request: Request):
|
|
170
|
+
require_admin(request)
|
|
171
|
+
_require_service()
|
|
172
|
+
try:
|
|
173
|
+
return service.import_encrypted_archive(
|
|
174
|
+
req.path,
|
|
175
|
+
passphrase=req.passphrase,
|
|
176
|
+
dry_run=req.dry_run,
|
|
177
|
+
confirm=req.confirm,
|
|
178
|
+
)
|
|
179
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
180
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
181
|
+
|
|
130
182
|
@router.post("/api/knowledge-graph/archive/restore")
|
|
131
183
|
async def restore_encrypted_archive(req: EncryptedRestoreRequest, request: Request):
|
|
132
184
|
require_admin(request)
|
|
133
185
|
_require_service()
|
|
134
186
|
try:
|
|
135
|
-
return service.restore_encrypted_archive(
|
|
187
|
+
return service.restore_encrypted_archive(
|
|
188
|
+
req.path,
|
|
189
|
+
passphrase=req.passphrase,
|
|
190
|
+
dry_run=req.dry_run,
|
|
191
|
+
confirm=req.confirm,
|
|
192
|
+
)
|
|
136
193
|
except (ValueError, FileNotFoundError) as exc:
|
|
137
194
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
138
195
|
|
package/latticeai/app_factory.py
CHANGED
|
@@ -154,6 +154,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
154
154
|
from latticeai.api.hooks import create_hooks_router
|
|
155
155
|
from latticeai.core.hooks import HooksRegistry
|
|
156
156
|
from latticeai.core.builtin_hooks import register_builtin_hook_runners
|
|
157
|
+
from latticeai.core.product_hardening import build_product_hardening_status
|
|
157
158
|
from latticeai.api.agent_registry import create_agent_registry_router
|
|
158
159
|
from latticeai.core.agent_registry import AgentRegistry
|
|
159
160
|
from latticeai.api.memory import create_memory_router
|
|
@@ -1256,6 +1257,13 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1256
1257
|
except Exception as e:
|
|
1257
1258
|
return {"error": str(e)}
|
|
1258
1259
|
|
|
1260
|
+
def _product_hardening_status():
|
|
1261
|
+
return build_product_hardening_status(
|
|
1262
|
+
config=CONFIG,
|
|
1263
|
+
portability=KG_PORTABILITY,
|
|
1264
|
+
device_identity=DEVICE_IDENTITY,
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1259
1267
|
app.include_router(create_admin_router(
|
|
1260
1268
|
require_admin=require_admin, require_user=require_user,
|
|
1261
1269
|
load_users=load_users, save_users=save_users,
|
|
@@ -1270,6 +1278,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1270
1278
|
invite_code=INVITE_CODE, invite_gate_enabled=INVITE_GATE_ENABLED,
|
|
1271
1279
|
default_port=DEFAULT_PORT,
|
|
1272
1280
|
policy_matrix=policy_matrix,
|
|
1281
|
+
product_hardening_status=_product_hardening_status,
|
|
1273
1282
|
))
|
|
1274
1283
|
|
|
1275
1284
|
app.include_router(create_invitations_router(
|
|
@@ -94,17 +94,24 @@ class KnowledgeGraphProjectionMixin:
|
|
|
94
94
|
"""
|
|
95
95
|
if KGStoreV2 is None or _exec_script is None:
|
|
96
96
|
return
|
|
97
|
+
self._v2_projection_available = False
|
|
97
98
|
try:
|
|
98
99
|
self._backup_before_v2_flip()
|
|
99
100
|
with self._connect() as conn:
|
|
100
101
|
conn.execute("BEGIN")
|
|
101
102
|
stale = self._projection_version(conn) != _PROJECTION_VERSION
|
|
103
|
+
# Reconstruction views are non-authoritative. Recreate them on
|
|
104
|
+
# every startup so older SQLite rename migrations cannot strand
|
|
105
|
+
# a view against a temporary table such as edges_v2_old.
|
|
106
|
+
for stmt in (
|
|
107
|
+
"DROP VIEW IF EXISTS kgv2_edges",
|
|
108
|
+
"DROP VIEW IF EXISTS kgv2_nodes",
|
|
109
|
+
):
|
|
110
|
+
conn.execute(stmt)
|
|
102
111
|
if stale:
|
|
103
112
|
# The projection is non-authoritative; drop it so init_schema
|
|
104
113
|
# recreates the tables with the current normalized columns.
|
|
105
114
|
for stmt in (
|
|
106
|
-
"DROP VIEW IF EXISTS kgv2_edges",
|
|
107
|
-
"DROP VIEW IF EXISTS kgv2_nodes",
|
|
108
115
|
"DROP TABLE IF EXISTS edges_v2",
|
|
109
116
|
"DROP TABLE IF EXISTS nodes_v2",
|
|
110
117
|
):
|
|
@@ -128,6 +135,9 @@ class KnowledgeGraphProjectionMixin:
|
|
|
128
135
|
(_V2_WRITE_MASTER_KEY, _V2_WRITE_MASTER_KEY, mastered_at),
|
|
129
136
|
)
|
|
130
137
|
conn.execute(f"PRAGMA user_version={_KG_DB_FORMAT_VERSION}")
|
|
138
|
+
conn.execute("SELECT 1 FROM kgv2_nodes LIMIT 1").fetchone()
|
|
139
|
+
conn.execute("SELECT 1 FROM kgv2_edges LIMIT 1").fetchone()
|
|
140
|
+
self._v2_projection_available = True
|
|
131
141
|
except Exception as e:
|
|
132
142
|
logging.warning("knowledge_graph: v2 schema init/backfill skipped: %s", e)
|
|
133
143
|
|
|
@@ -879,6 +879,16 @@ class KnowledgeGraphRetrievalMixin:
|
|
|
879
879
|
if isinstance(storage_capabilities, dict)
|
|
880
880
|
else "bruteforce-cosine"
|
|
881
881
|
),
|
|
882
|
+
"vector_search_mode": (
|
|
883
|
+
(storage_capabilities.get("metadata") or {}).get("vector_mode")
|
|
884
|
+
if isinstance(storage_capabilities, dict)
|
|
885
|
+
else "fallback"
|
|
886
|
+
),
|
|
887
|
+
"sqlite_vec_ann_available": (
|
|
888
|
+
bool((storage_capabilities.get("metadata") or {}).get("sqlite_vec_ann_available"))
|
|
889
|
+
if isinstance(storage_capabilities, dict)
|
|
890
|
+
else False
|
|
891
|
+
),
|
|
882
892
|
},
|
|
883
893
|
"source_items": len(source_items),
|
|
884
894
|
"indexed_items": sum(vector_counts.values()),
|
package/latticeai/brain/store.py
CHANGED
|
@@ -53,10 +53,15 @@ class KnowledgeGraphStore(
|
|
|
53
53
|
self._embedding_model = (
|
|
54
54
|
embedder if embedder is not None else LocalEmbeddingModel()
|
|
55
55
|
)
|
|
56
|
+
self._v2_projection_available = False
|
|
56
57
|
self._init_db()
|
|
57
58
|
# Read graph queries from the v2 projection (kgv2_* views) when available.
|
|
58
59
|
# Toggle off (e.g. in tests) to compare against the legacy tables.
|
|
59
|
-
self._read_from_v2 =
|
|
60
|
+
self._read_from_v2 = (
|
|
61
|
+
KGStoreV2 is not None
|
|
62
|
+
and _READ_FROM_V2_DEFAULT
|
|
63
|
+
and self._v2_projection_available
|
|
64
|
+
)
|
|
60
65
|
|
|
61
66
|
def _read_tables(self) -> tuple:
|
|
62
67
|
"""Return (nodes_table, edges_table) for read queries.
|
package/latticeai/core/config.py
CHANGED
|
@@ -157,13 +157,15 @@ class Config:
|
|
|
157
157
|
if packaged_static.exists():
|
|
158
158
|
static_dir = packaged_static
|
|
159
159
|
|
|
160
|
+
default_sso_redirect = f"http://localhost:{port}/auth/sso/callback"
|
|
161
|
+
|
|
160
162
|
return cls(
|
|
161
163
|
app_mode=app_mode,
|
|
162
164
|
is_public=is_public,
|
|
163
165
|
host=host,
|
|
164
166
|
port=port,
|
|
165
167
|
network_exposed=network_exposed,
|
|
166
|
-
enable_telegram=_bool(env, "LATTICEAI_ENABLE_TELEGRAM", default=
|
|
168
|
+
enable_telegram=_bool(env, "LATTICEAI_ENABLE_TELEGRAM", default=False),
|
|
167
169
|
enable_graph=_bool(env, "LATTICEAI_ENABLE_GRAPH", default=True),
|
|
168
170
|
autoload_models=_bool(env, "LATTICEAI_AUTOLOAD_MODELS", default=is_public),
|
|
169
171
|
model_idle_unload_seconds=_int(env, "LATTICEAI_MODEL_IDLE_UNLOAD_SECONDS", 0),
|
|
@@ -196,7 +198,7 @@ class Config:
|
|
|
196
198
|
sso_discovery_url=_value(env, "OIDC_DISCOVERY_URL", ""),
|
|
197
199
|
sso_client_id=_value(env, "OIDC_CLIENT_ID", ""),
|
|
198
200
|
sso_client_secret=_value(env, "OIDC_CLIENT_SECRET", ""),
|
|
199
|
-
sso_redirect_uri=_value(env, "OIDC_REDIRECT_URI",
|
|
201
|
+
sso_redirect_uri=_value(env, "OIDC_REDIRECT_URI", default_sso_redirect),
|
|
200
202
|
sso_provider_name=_value(env, "OIDC_PROVIDER_NAME", "SSO"),
|
|
201
203
|
discord_permission_webhook=_value(env, "LATTICEAI_DISCORD_PERMISSION_WEBHOOK", ""),
|
|
202
204
|
discord_bot_token=_value(env, "LATTICEAI_DISCORD_BOT_TOKEN", ""),
|
|
@@ -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.1"
|
|
18
18
|
|
|
19
19
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
20
20
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
@@ -0,0 +1,218 @@
|
|
|
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 import __version__
|
|
16
|
+
from latticeai.core.config import Config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _bool(env: Mapping[str, str], key: str, default: bool = False) -> bool:
|
|
20
|
+
raw = env.get(key)
|
|
21
|
+
if raw is None:
|
|
22
|
+
return default
|
|
23
|
+
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _present(env: Mapping[str, str], *keys: str) -> bool:
|
|
27
|
+
return any(bool(str(env.get(key) or "").strip()) for key in keys)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def external_integration_status(
|
|
31
|
+
config: Config,
|
|
32
|
+
*,
|
|
33
|
+
env: Optional[Mapping[str, str]] = None,
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
if env is None:
|
|
36
|
+
env = os.environ
|
|
37
|
+
telegram_credentials = _present(env, "LATTICEAI_TELEGRAM_BOT_TOKEN", "TELEGRAM_BOT_TOKEN")
|
|
38
|
+
brain_network_auto_push = _bool(env, "LATTICEAI_BRAIN_NETWORK_AUTO_PUSH", default=False)
|
|
39
|
+
updater_enabled = _bool(env, "LATTICEAI_ENABLE_UPDATES", default=False)
|
|
40
|
+
model_downloads_enabled = _bool(env, "LATTICEAI_ALLOW_MODEL_DOWNLOADS", default=False) or bool(config.autoload_models)
|
|
41
|
+
docker_auto_start = _bool(env, "LATTICEAI_DOCKER_AUTO_START", default=False)
|
|
42
|
+
external_connectors_enabled = _bool(env, "LATTICEAI_ENABLE_EXTERNAL_CONNECTORS", default=False)
|
|
43
|
+
postgres_enabled = config.storage_engine == "postgres" and bool(config.postgres_dsn)
|
|
44
|
+
return {
|
|
45
|
+
"local_only_default": default_startup_local_only(config, env=env),
|
|
46
|
+
"integrations": {
|
|
47
|
+
"telegram": {
|
|
48
|
+
"enabled": bool(config.enable_telegram),
|
|
49
|
+
"credential_present": telegram_credentials,
|
|
50
|
+
"opt_in_required": True,
|
|
51
|
+
"automatic_egress": bool(config.enable_telegram),
|
|
52
|
+
"detail": (
|
|
53
|
+
"enabled by LATTICEAI_ENABLE_TELEGRAM"
|
|
54
|
+
if config.enable_telegram
|
|
55
|
+
else "disabled; token presence alone does not start Telegram"
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
"brain_network": {
|
|
59
|
+
"enabled": brain_network_auto_push,
|
|
60
|
+
"credential_present": False,
|
|
61
|
+
"opt_in_required": True,
|
|
62
|
+
"automatic_egress": brain_network_auto_push,
|
|
63
|
+
"detail": "peer pushes are user/admin initiated; no automatic peer sync by default",
|
|
64
|
+
},
|
|
65
|
+
"updates": {
|
|
66
|
+
"enabled": updater_enabled,
|
|
67
|
+
"credential_present": False,
|
|
68
|
+
"opt_in_required": True,
|
|
69
|
+
"automatic_egress": updater_enabled,
|
|
70
|
+
"detail": "desktop updater checks are disabled unless LATTICEAI_ENABLE_UPDATES is true",
|
|
71
|
+
},
|
|
72
|
+
"model_downloads": {
|
|
73
|
+
"enabled": model_downloads_enabled,
|
|
74
|
+
"credential_present": _present(env, "HF_TOKEN", "HUGGINGFACEHUB_API_TOKEN"),
|
|
75
|
+
"opt_in_required": True,
|
|
76
|
+
"automatic_egress": bool(config.autoload_models),
|
|
77
|
+
"detail": "model downloads require an explicit load/autoload setting",
|
|
78
|
+
},
|
|
79
|
+
"docker": {
|
|
80
|
+
"enabled": docker_auto_start,
|
|
81
|
+
"credential_present": False,
|
|
82
|
+
"opt_in_required": True,
|
|
83
|
+
"automatic_egress": docker_auto_start,
|
|
84
|
+
"detail": "Docker setup requires explicit runtime consent; auto-start is disabled by default",
|
|
85
|
+
},
|
|
86
|
+
"postgres": {
|
|
87
|
+
"enabled": postgres_enabled,
|
|
88
|
+
"credential_present": bool(config.postgres_dsn),
|
|
89
|
+
"opt_in_required": True,
|
|
90
|
+
"automatic_egress": postgres_enabled,
|
|
91
|
+
"detail": "Postgres scale mode is used only when storage engine and DSN are explicitly configured",
|
|
92
|
+
},
|
|
93
|
+
"external_connectors": {
|
|
94
|
+
"enabled": external_connectors_enabled,
|
|
95
|
+
"credential_present": _present(
|
|
96
|
+
env,
|
|
97
|
+
"OPENAI_API_KEY",
|
|
98
|
+
"ANTHROPIC_API_KEY",
|
|
99
|
+
"GITHUB_TOKEN",
|
|
100
|
+
"SLACK_BOT_TOKEN",
|
|
101
|
+
"DISCORD_BOT_TOKEN",
|
|
102
|
+
),
|
|
103
|
+
"opt_in_required": True,
|
|
104
|
+
"automatic_egress": external_connectors_enabled,
|
|
105
|
+
"detail": "connector credentials are inert until the connector is explicitly enabled and invoked",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def default_startup_local_only(
|
|
112
|
+
config: Config,
|
|
113
|
+
*,
|
|
114
|
+
env: Optional[Mapping[str, str]] = None,
|
|
115
|
+
) -> bool:
|
|
116
|
+
if env is None:
|
|
117
|
+
env = os.environ
|
|
118
|
+
local_embedding = config.embedding_provider in {"", "hash", "local", "fallback", "sqlite"}
|
|
119
|
+
external = external_integration_status_no_recurse(config, env=env)
|
|
120
|
+
return (
|
|
121
|
+
not config.network_exposed
|
|
122
|
+
and not config.cors_allow_network
|
|
123
|
+
and not config.enable_telegram
|
|
124
|
+
and not config.autoload_models
|
|
125
|
+
and config.storage_engine == "sqlite"
|
|
126
|
+
and local_embedding
|
|
127
|
+
and not any(item["automatic_egress"] for item in external.values())
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def external_integration_status_no_recurse(
|
|
132
|
+
config: Config,
|
|
133
|
+
*,
|
|
134
|
+
env: Mapping[str, str],
|
|
135
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
136
|
+
return {
|
|
137
|
+
"brain_network": {"automatic_egress": _bool(env, "LATTICEAI_BRAIN_NETWORK_AUTO_PUSH", default=False)},
|
|
138
|
+
"updates": {"automatic_egress": _bool(env, "LATTICEAI_ENABLE_UPDATES", default=False)},
|
|
139
|
+
"docker": {"automatic_egress": _bool(env, "LATTICEAI_DOCKER_AUTO_START", default=False)},
|
|
140
|
+
"postgres": {"automatic_egress": config.storage_engine == "postgres" and bool(config.postgres_dsn)},
|
|
141
|
+
"external_connectors": {"automatic_egress": _bool(env, "LATTICEAI_ENABLE_EXTERNAL_CONNECTORS", default=False)},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_product_hardening_status(
|
|
146
|
+
*,
|
|
147
|
+
config: Config,
|
|
148
|
+
portability: Any = None,
|
|
149
|
+
device_identity: Any = None,
|
|
150
|
+
env: Optional[Mapping[str, str]] = None,
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
if env is None:
|
|
153
|
+
env = os.environ
|
|
154
|
+
storage = {"available": False}
|
|
155
|
+
backup = {"available": False}
|
|
156
|
+
if portability is not None and getattr(portability, "available", lambda: False)():
|
|
157
|
+
storage = portability.storage_status()
|
|
158
|
+
backup = portability.backup_health()
|
|
159
|
+
identity = {}
|
|
160
|
+
if device_identity is not None:
|
|
161
|
+
identity = device_identity.describe()
|
|
162
|
+
data_dir = Path(config.data_dir)
|
|
163
|
+
return {
|
|
164
|
+
"version": __version__,
|
|
165
|
+
"startup": {
|
|
166
|
+
"local_only_default": default_startup_local_only(config, env=env),
|
|
167
|
+
"host": config.host,
|
|
168
|
+
"port": config.port,
|
|
169
|
+
"network_exposed": config.network_exposed,
|
|
170
|
+
"auth_required": config.require_auth,
|
|
171
|
+
"cors_network_allowed": config.cors_allow_network,
|
|
172
|
+
},
|
|
173
|
+
"desktop": {
|
|
174
|
+
"sidecar_lifecycle": "managed",
|
|
175
|
+
"restart_supported": True,
|
|
176
|
+
"shutdown_supported": True,
|
|
177
|
+
"updater": {
|
|
178
|
+
"enabled": _bool(env, "LATTICEAI_ENABLE_UPDATES", default=False),
|
|
179
|
+
"limitation": "No external update checks run unless explicitly enabled by policy.",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
"first_run": {
|
|
183
|
+
"data_dir": str(data_dir),
|
|
184
|
+
"data_dir_exists": data_dir.exists(),
|
|
185
|
+
"python_available": shutil.which("python3") is not None or shutil.which("python") is not None,
|
|
186
|
+
"docker_available": shutil.which("docker") is not None,
|
|
187
|
+
"docker_required": False,
|
|
188
|
+
"postgres_required": False,
|
|
189
|
+
},
|
|
190
|
+
"privacy": external_integration_status(config, env=env),
|
|
191
|
+
"storage": storage,
|
|
192
|
+
"backup": backup,
|
|
193
|
+
"device_identity": identity,
|
|
194
|
+
"permissions": {
|
|
195
|
+
"export_requires_admin": True,
|
|
196
|
+
"import_requires_admin": True,
|
|
197
|
+
"restore_requires_admin": True,
|
|
198
|
+
"destructive_restore_requires_confirmation": True,
|
|
199
|
+
"workspace_isolation_enforced": True,
|
|
200
|
+
"audit_log_visible_to_admin": True,
|
|
201
|
+
},
|
|
202
|
+
"failure_policy": {
|
|
203
|
+
"archive_corruption": "fail_closed",
|
|
204
|
+
"partial_archive": "fail_closed",
|
|
205
|
+
"signature_mismatch": "fail_closed",
|
|
206
|
+
"unsupported_version": "fail_closed",
|
|
207
|
+
"missing_docker": "honest_unavailable",
|
|
208
|
+
"missing_postgres": "honest_unavailable",
|
|
209
|
+
"permission_denied": "honest_error",
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
__all__ = [
|
|
215
|
+
"build_product_hardening_status",
|
|
216
|
+
"default_startup_local_only",
|
|
217
|
+
"external_integration_status",
|
|
218
|
+
]
|
|
@@ -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.1"
|
|
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
|