ltcai 10.0.0 → 10.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 +43 -32
- package/docs/CHANGELOG.md +204 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
- package/docs/MYPY_BACKLOG.md +98 -0
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +5 -4
- package/lattice_brain/conversations.py +14 -3
- package/lattice_brain/embeddings.py +12 -2
- package/lattice_brain/graph/_kg_common.py +5 -5
- package/lattice_brain/graph/_kg_fsutil.py +5 -4
- package/lattice_brain/graph/discovery.py +4 -3
- package/lattice_brain/graph/discovery_index.py +0 -1
- package/lattice_brain/graph/documents.py +3 -2
- package/lattice_brain/graph/fusion.py +4 -0
- package/lattice_brain/graph/ingest.py +12 -2
- package/lattice_brain/graph/projection.py +3 -2
- package/lattice_brain/graph/provenance.py +5 -3
- package/lattice_brain/graph/rerank.py +4 -1
- package/lattice_brain/graph/retrieval.py +0 -1
- package/lattice_brain/graph/retrieval_docgen.py +0 -1
- package/lattice_brain/graph/retrieval_reads.py +0 -1
- package/lattice_brain/graph/retrieval_vector.py +0 -1
- package/lattice_brain/graph/schema.py +4 -3
- package/lattice_brain/graph/store.py +18 -4
- package/lattice_brain/graph/write_master.py +46 -1
- package/lattice_brain/ingestion.py +4 -1
- package/lattice_brain/portability.py +5 -2
- package/lattice_brain/quality.py +12 -5
- package/lattice_brain/quiet.py +43 -0
- package/lattice_brain/runtime/agent_runtime.py +19 -10
- package/lattice_brain/runtime/hooks.py +3 -2
- package/lattice_brain/runtime/multi_agent.py +2 -3
- package/lattice_brain/sensitivity.py +94 -0
- package/lattice_brain/storage/base.py +30 -2
- package/lattice_brain/storage/migration.py +3 -2
- package/lattice_brain/storage/postgres.py +2 -2
- package/lattice_brain/storage/sqlite.py +6 -4
- package/lattice_brain/workflow.py +4 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +4 -2
- package/latticeai/api/auth.py +5 -1
- package/latticeai/api/automation_intelligence.py +2 -1
- package/latticeai/api/browser.py +3 -2
- package/latticeai/api/chat.py +28 -17
- package/latticeai/api/chat_agent_http.py +22 -8
- package/latticeai/api/chat_contracts.py +4 -0
- package/latticeai/api/chat_documents.py +6 -2
- package/latticeai/api/chat_hybrid.py +82 -0
- package/latticeai/api/computer_use.py +8 -3
- package/latticeai/api/knowledge_graph.py +1 -1
- package/latticeai/api/mcp.py +4 -4
- package/latticeai/api/models.py +5 -2
- package/latticeai/api/network_boundary.py +220 -0
- package/latticeai/api/permissions.py +0 -1
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/security_dashboard.py +1 -1
- package/latticeai/api/setup.py +16 -3
- package/latticeai/api/static_routes.py +2 -1
- package/latticeai/api/tools.py +12 -8
- package/latticeai/api/voice_capture.py +3 -1
- package/latticeai/api/workflow_designer.py +1 -1
- package/latticeai/api/workspace.py +1 -2
- package/latticeai/app_factory.py +174 -145
- package/latticeai/cli/entrypoint.py +6 -4
- package/latticeai/core/agent.py +55 -495
- package/latticeai/core/agent_eval.py +2 -2
- package/latticeai/core/agent_helpers.py +493 -0
- package/latticeai/core/agent_prompts.py +0 -1
- package/latticeai/core/agent_registry.py +3 -1
- package/latticeai/core/agent_state.py +41 -0
- package/latticeai/core/audit.py +1 -1
- package/latticeai/core/builtin_hooks.py +2 -1
- package/latticeai/core/config.py +0 -1
- package/latticeai/core/embedding_providers.py +12 -1
- package/latticeai/core/file_generation.py +3 -0
- package/latticeai/core/invitations.py +4 -1
- package/latticeai/core/io_utils.py +3 -1
- package/latticeai/core/legacy_compatibility.py +1 -2
- package/latticeai/core/local_embeddings.py +12 -1
- package/latticeai/core/marketplace.py +1 -2
- package/latticeai/core/mcp_registry.py +0 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/network_boundary.py +168 -0
- package/latticeai/core/oidc.py +3 -0
- package/latticeai/core/permission_mode.py +6 -6
- package/latticeai/core/plugins.py +3 -2
- package/latticeai/core/policy.py +0 -1
- package/latticeai/core/quiet.py +84 -0
- package/latticeai/core/realtime.py +3 -2
- package/latticeai/core/run_store.py +4 -2
- package/latticeai/core/security.py +4 -0
- package/latticeai/core/users.py +5 -3
- package/latticeai/core/workspace_graph_trace.py +3 -0
- package/latticeai/core/workspace_os.py +65 -273
- package/latticeai/core/workspace_os_constants.py +126 -0
- package/latticeai/core/workspace_os_state.py +180 -0
- package/latticeai/core/workspace_os_utils.py +3 -1
- package/latticeai/core/workspace_permissions.py +1 -0
- package/latticeai/core/workspace_snapshots.py +3 -1
- package/latticeai/core/workspace_timeline.py +3 -1
- package/latticeai/integrations/telegram_bot.py +25 -16
- package/latticeai/models/router.py +6 -3
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/audit_runtime.py +3 -2
- package/latticeai/runtime/chat_wiring.py +4 -1
- package/latticeai/runtime/history_runtime.py +1 -1
- package/latticeai/runtime/history_writer.py +138 -0
- package/latticeai/runtime/lifespan_runtime.py +3 -1
- package/latticeai/runtime/network_boundary_wiring.py +124 -0
- package/latticeai/runtime/persistence_runtime.py +3 -1
- package/latticeai/runtime/router_registration.py +11 -1
- package/latticeai/services/architecture_readiness.py +1 -2
- package/latticeai/services/change_proposals.py +2 -1
- package/latticeai/services/cloud_egress_audit.py +85 -0
- package/latticeai/services/cloud_extraction.py +129 -0
- package/latticeai/services/cloud_streaming.py +266 -0
- package/latticeai/services/cloud_token_guard.py +84 -0
- package/latticeai/services/command_center.py +2 -1
- package/latticeai/services/folder_watch.py +3 -0
- package/latticeai/services/funnel_metrics.py +3 -1
- package/latticeai/services/hybrid_chat.py +265 -0
- package/latticeai/services/hybrid_context.py +228 -0
- package/latticeai/services/hybrid_policy.py +178 -0
- package/latticeai/services/memory_service.py +1 -1
- package/latticeai/services/model_catalog.py +10 -1
- package/latticeai/services/model_engines.py +35 -14
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime.py +68 -17
- package/latticeai/services/multimodal_streaming.py +123 -0
- package/latticeai/services/network_boundary_service.py +154 -0
- package/latticeai/services/openai_compatible_adapter.py +100 -0
- package/latticeai/services/p_reinforce.py +4 -0
- package/latticeai/services/platform_runtime.py +9 -4
- package/latticeai/services/process_audit.py +0 -1
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +3 -2
- package/latticeai/services/search_service.py +1 -4
- package/latticeai/services/setup_detection.py +2 -1
- package/latticeai/services/tool_dispatch.py +7 -2
- package/latticeai/services/upload_service.py +2 -1
- package/latticeai/setup/auto_setup.py +10 -7
- package/latticeai/setup/wizard.py +15 -11
- package/latticeai/tools/__init__.py +3 -3
- package/latticeai/tools/commands.py +7 -7
- package/latticeai/tools/computer.py +3 -2
- package/latticeai/tools/documents.py +10 -9
- package/latticeai/tools/filesystem.py +7 -4
- package/latticeai/tools/knowledge.py +2 -0
- package/latticeai/tools/local_files.py +1 -1
- package/latticeai/tools/network.py +2 -1
- package/package.json +7 -3
- package/scripts/bench_agent_smoke.py +0 -1
- package/scripts/bench_models.py +0 -1
- package/scripts/brain_quality_eval.py +7 -2
- package/scripts/bump_version.py +2 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/migrate_brain_storage.py +5 -1
- package/scripts/profile_kg.py +2 -7
- package/scripts/verify_hf_model_registry.py +2 -2
- 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 +37 -37
- package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
- package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
- package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
- package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
- package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
- package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
- package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
- package/static/app/assets/Library-CXnbNiQW.js +1 -0
- package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
- package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
- package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
- package/static/app/assets/System-Bp9ZY4ER.js +1 -0
- package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
- package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
- package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
- package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
- package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
- package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
- package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
- package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
- package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
- package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
- package/static/app/assets/index-CEkL4_M-.css +2 -0
- package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
- package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
- package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
- package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
- package/static/app/assets/search-CwEJWBEA.js +1 -0
- package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
- package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
- package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
- package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
- package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
- package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
- package/static/app/assets/workspace-BMUEiXUm.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Library-DhvoPvC7.js +0 -1
- package/static/app/assets/System-CSMdYLMy.js +0 -1
- package/static/app/assets/index-FxDusbr0.css +0 -2
- package/static/app/assets/search-DhbSgW6m.js +0 -1
- package/static/app/assets/workspace-DBPB0jkX.js +0 -1
|
@@ -77,7 +77,18 @@ class LocalEmbeddingModel:
|
|
|
77
77
|
return [value / norm for value in vector]
|
|
78
78
|
|
|
79
79
|
def similarity(self, left: Iterable[float], right: Iterable[float]) -> float:
|
|
80
|
-
|
|
80
|
+
# strict=True: a dimension mismatch means the two vectors came from
|
|
81
|
+
# different embedding models. Truncating to the shorter one produces a
|
|
82
|
+
# plausible-looking similarity that is meaningless — exactly the silent
|
|
83
|
+
# wrongness this codebase keeps finding. Callers that can hit a model
|
|
84
|
+
# swap already handle failure and fall back to lexical search.
|
|
85
|
+
left_v, right_v = list(left), list(right)
|
|
86
|
+
if len(left_v) != len(right_v):
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"embedding dimension mismatch: {len(left_v)} vs {len(right_v)}; "
|
|
89
|
+
"the vector index was built with a different model"
|
|
90
|
+
)
|
|
91
|
+
return float(sum(a * b for a, b in zip(left_v, right_v, strict=True)))
|
|
81
92
|
|
|
82
93
|
def encode(self, vector: Iterable[float]) -> bytes:
|
|
83
94
|
values = list(vector)
|
|
@@ -10,8 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from copy import deepcopy
|
|
11
11
|
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
MARKETPLACE_VERSION = "10.0.0"
|
|
13
|
+
MARKETPLACE_VERSION = "10.3.0"
|
|
15
14
|
TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
|
|
16
15
|
|
|
17
16
|
|
|
@@ -17,7 +17,6 @@ import httpx
|
|
|
17
17
|
from fastapi import HTTPException
|
|
18
18
|
|
|
19
19
|
# ── MCP Registry (built-in tool definitions) ─────────────────────────────────
|
|
20
|
-
|
|
21
20
|
# Built-in MCP server catalog data → .mcp_catalog (re-exported).
|
|
22
21
|
from .mcp_catalog import MCP_REGISTRY # noqa: F401
|
|
23
22
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Network boundary modes for hybrid local + cloud LLM usage.
|
|
2
|
+
|
|
3
|
+
Local-first principle: the Knowledge Graph and all durable memory stay on the
|
|
4
|
+
machine. Cloud LLMs are an explicit, opt-in worker. This module defines the
|
|
5
|
+
dial that controls whether any knowledge is allowed to leave the host.
|
|
6
|
+
|
|
7
|
+
Modes
|
|
8
|
+
-----
|
|
9
|
+
* ``local_only`` — default. No chat context is ever sent to a cloud provider.
|
|
10
|
+
* ``cloud_allowed`` — user has explicitly opted in. Only the *minimal* related
|
|
11
|
+
nodes selected by the hybrid context extractor may be sent. Sensitive types
|
|
12
|
+
and hard filters still apply (mode-invariant).
|
|
13
|
+
|
|
14
|
+
The mode is orthogonal to PermissionMode (agent autonomy). A session can be
|
|
15
|
+
``cloud_allowed`` + ``strict`` at the same time.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Any, Dict, FrozenSet, Mapping, Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NetworkBoundaryMode(str, Enum):
|
|
25
|
+
LOCAL_ONLY = "local_only"
|
|
26
|
+
CLOUD_ALLOWED = "cloud_allowed"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
DEFAULT_NETWORK_MODE = NetworkBoundaryMode.LOCAL_ONLY
|
|
30
|
+
|
|
31
|
+
# Node types that must never be included in a cloud payload, regardless of mode.
|
|
32
|
+
# Extend carefully; this is a hard circuit breaker.
|
|
33
|
+
#
|
|
34
|
+
# This was empty through 10.1.x ("keep empty for v1 scaffolding"), which meant
|
|
35
|
+
# the type arm of the filter could never fire. Combined with nothing being able
|
|
36
|
+
# to *set* the metadata flags below, the whole guard was unreachable — correct
|
|
37
|
+
# code protecting nothing.
|
|
38
|
+
HARD_BLOCK_NODE_TYPES: FrozenSet[str] = frozenset({
|
|
39
|
+
"Credential",
|
|
40
|
+
"Secret",
|
|
41
|
+
"ApiKey",
|
|
42
|
+
"Token",
|
|
43
|
+
"Password",
|
|
44
|
+
"PrivateKey",
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
# Metadata keys that, when present and truthy, block a node from leaving.
|
|
48
|
+
HARD_BLOCK_METADATA_FLAGS: FrozenSet[str] = frozenset({
|
|
49
|
+
"sensitive",
|
|
50
|
+
"private",
|
|
51
|
+
"do_not_share",
|
|
52
|
+
"local_only",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
# Path-based never-leaves rules live in Brain Core: they describe the data, not
|
|
56
|
+
# the transport, and Brain Core cannot import from this package. Re-exported
|
|
57
|
+
# here so the boundary module stays the single place the app layer reads.
|
|
58
|
+
from lattice_brain.sensitivity import ( # noqa: E402
|
|
59
|
+
SENSITIVE_FILENAMES,
|
|
60
|
+
SENSITIVE_PATH_FRAGMENTS,
|
|
61
|
+
sensitive_reason_for_path,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_network_mode(value: Any) -> NetworkBoundaryMode:
|
|
66
|
+
"""Parse user/API/env input into a NetworkBoundaryMode; unknown → local_only."""
|
|
67
|
+
if isinstance(value, NetworkBoundaryMode):
|
|
68
|
+
return value
|
|
69
|
+
text = str(value or "").strip().lower()
|
|
70
|
+
aliases = {
|
|
71
|
+
"local_only": NetworkBoundaryMode.LOCAL_ONLY,
|
|
72
|
+
"local": NetworkBoundaryMode.LOCAL_ONLY,
|
|
73
|
+
"local-only": NetworkBoundaryMode.LOCAL_ONLY,
|
|
74
|
+
"offline": NetworkBoundaryMode.LOCAL_ONLY,
|
|
75
|
+
"cloud_allowed": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
76
|
+
"cloud": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
77
|
+
"cloud-allowed": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
78
|
+
"hybrid": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
79
|
+
"online": NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
80
|
+
}
|
|
81
|
+
return aliases.get(text, DEFAULT_NETWORK_MODE)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def network_mode_catalog() -> list[Dict[str, Any]]:
|
|
85
|
+
"""UI/API catalog for the network boundary selector."""
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
"id": NetworkBoundaryMode.LOCAL_ONLY.value,
|
|
89
|
+
"label": "Local only",
|
|
90
|
+
"label_ko": "로컬만",
|
|
91
|
+
"summary": "Nothing leaves this machine. Answers use local models and the local Brain only.",
|
|
92
|
+
"summary_ko": "이 컴퓨터를 벗어나지 않습니다. 로컬 모델과 로컬 Brain만 사용합니다.",
|
|
93
|
+
"risk": "low",
|
|
94
|
+
"requires_ack": False,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": NetworkBoundaryMode.CLOUD_ALLOWED.value,
|
|
98
|
+
"label": "Cloud streaming allowed",
|
|
99
|
+
"label_ko": "클라우드 스트리밍 허용",
|
|
100
|
+
"summary": (
|
|
101
|
+
"Minimal related Knowledge Graph nodes may be sent to a cloud LLM. "
|
|
102
|
+
"The streamed answer is written back into the local Brain with provenance."
|
|
103
|
+
),
|
|
104
|
+
"summary_ko": (
|
|
105
|
+
"관련된 최소 Knowledge Graph 노드만 클라우드 LLM으로 전송될 수 있습니다. "
|
|
106
|
+
"스트리밍 답변은 provenance와 함께 로컬 Brain에 다시 기록됩니다."
|
|
107
|
+
),
|
|
108
|
+
"risk": "medium",
|
|
109
|
+
"requires_ack": True,
|
|
110
|
+
"warning": (
|
|
111
|
+
"Cloud mode sends a compact summary of selected local nodes to an external provider. "
|
|
112
|
+
"Sensitive nodes remain blocked. You can switch back to Local only at any time."
|
|
113
|
+
),
|
|
114
|
+
"warning_ko": (
|
|
115
|
+
"클라우드 모드는 선택된 로컬 노드의 압축 요약을 외부 제공자에게 전송합니다. "
|
|
116
|
+
"민감 노드는 계속 차단됩니다. 언제든지 로컬만으로 되돌릴 수 있습니다."
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_node_blocked_for_cloud(node: Mapping[str, Any]) -> Optional[str]:
|
|
123
|
+
"""Return a reason string when this node must never leave the host."""
|
|
124
|
+
node_type = str(node.get("type") or "")
|
|
125
|
+
if node_type in HARD_BLOCK_NODE_TYPES:
|
|
126
|
+
return f"node type {node_type!r} is blocked from cloud payloads"
|
|
127
|
+
|
|
128
|
+
meta = node.get("metadata") or {}
|
|
129
|
+
if not isinstance(meta, Mapping):
|
|
130
|
+
return None
|
|
131
|
+
for flag in HARD_BLOCK_METADATA_FLAGS:
|
|
132
|
+
if meta.get(flag):
|
|
133
|
+
return f"node flagged {flag!r} is blocked from cloud payloads"
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def network_mode_contract(mode: NetworkBoundaryMode | str) -> Dict[str, Any]:
|
|
138
|
+
"""Serializable contract for API / agent / UI."""
|
|
139
|
+
mode = normalize_network_mode(mode)
|
|
140
|
+
entry = next(
|
|
141
|
+
(m for m in network_mode_catalog() if m["id"] == mode.value),
|
|
142
|
+
network_mode_catalog()[0],
|
|
143
|
+
)
|
|
144
|
+
return {
|
|
145
|
+
"mode": mode.value,
|
|
146
|
+
"label": entry["label"],
|
|
147
|
+
"label_ko": entry["label_ko"],
|
|
148
|
+
"risk": entry["risk"],
|
|
149
|
+
"requires_ack": entry["requires_ack"],
|
|
150
|
+
"allows_cloud": mode == NetworkBoundaryMode.CLOUD_ALLOWED,
|
|
151
|
+
"hard_block_node_types": sorted(HARD_BLOCK_NODE_TYPES),
|
|
152
|
+
"hard_block_metadata_flags": sorted(HARD_BLOCK_METADATA_FLAGS),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"NetworkBoundaryMode",
|
|
158
|
+
"DEFAULT_NETWORK_MODE",
|
|
159
|
+
"HARD_BLOCK_NODE_TYPES",
|
|
160
|
+
"HARD_BLOCK_METADATA_FLAGS",
|
|
161
|
+
"normalize_network_mode",
|
|
162
|
+
"network_mode_catalog",
|
|
163
|
+
"is_node_blocked_for_cloud",
|
|
164
|
+
"sensitive_reason_for_path",
|
|
165
|
+
"SENSITIVE_PATH_FRAGMENTS",
|
|
166
|
+
"SENSITIVE_FILENAMES",
|
|
167
|
+
"network_mode_contract",
|
|
168
|
+
]
|
package/latticeai/core/oidc.py
CHANGED
|
@@ -25,6 +25,8 @@ import json
|
|
|
25
25
|
import time
|
|
26
26
|
from typing import Any, Dict, List, Optional
|
|
27
27
|
|
|
28
|
+
from latticeai.core.quiet import quiet
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
class OIDCValidationError(Exception):
|
|
30
32
|
"""Raised when an OIDC ID token fails any validation step (fail-closed)."""
|
|
@@ -116,6 +118,7 @@ def _verify_signature(token: str, jwks: Any) -> Dict[str, Any]:
|
|
|
116
118
|
public_key = _public_key_from_jwk(jwk)
|
|
117
119
|
public_key.verify(signature, signing_input, padding.PKCS1v15(), hash_obj)
|
|
118
120
|
except (InvalidSignature, OIDCValidationError):
|
|
121
|
+
quiet()
|
|
119
122
|
continue
|
|
120
123
|
# Signature verified — now it is safe to parse the claims.
|
|
121
124
|
try:
|
|
@@ -22,7 +22,7 @@ Circuit breakers are mode-invariant: a mode never overrides a hard deny.
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
from enum import Enum
|
|
25
|
-
from typing import Any, Dict, Mapping, Optional
|
|
25
|
+
from typing import Any, Dict, FrozenSet, Mapping, Optional
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class PermissionMode(str, Enum):
|
|
@@ -34,7 +34,7 @@ class PermissionMode(str, Enum):
|
|
|
34
34
|
DEFAULT_MODE = PermissionMode.STRICT
|
|
35
35
|
|
|
36
36
|
# Computer-use split: observation is low-friction in trusted; control stays gated.
|
|
37
|
-
COMPUTER_OBSERVATION_TOOLS:
|
|
37
|
+
COMPUTER_OBSERVATION_TOOLS: FrozenSet[str] = frozenset({
|
|
38
38
|
"computer_screenshot",
|
|
39
39
|
"computer_status",
|
|
40
40
|
"computer_use_status",
|
|
@@ -42,7 +42,7 @@ COMPUTER_OBSERVATION_TOOLS: Set[str] = frozenset({
|
|
|
42
42
|
"vision_analyze",
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
COMPUTER_CONTROL_TOOLS:
|
|
45
|
+
COMPUTER_CONTROL_TOOLS: FrozenSet[str] = frozenset({
|
|
46
46
|
"computer_click",
|
|
47
47
|
"computer_type",
|
|
48
48
|
"computer_key",
|
|
@@ -53,7 +53,7 @@ COMPUTER_CONTROL_TOOLS: Set[str] = frozenset({
|
|
|
53
53
|
"computer_open_url",
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
KNOWLEDGE_READ_TOOLS:
|
|
56
|
+
KNOWLEDGE_READ_TOOLS: FrozenSet[str] = frozenset({
|
|
57
57
|
"knowledge_search",
|
|
58
58
|
"knowledge_tree",
|
|
59
59
|
"obsidian_search",
|
|
@@ -63,7 +63,7 @@ KNOWLEDGE_READ_TOOLS: Set[str] = frozenset({
|
|
|
63
63
|
"knowledge_graph_context",
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
WORKSPACE_WRITE_TOOLS:
|
|
66
|
+
WORKSPACE_WRITE_TOOLS: FrozenSet[str] = frozenset({
|
|
67
67
|
"write_file",
|
|
68
68
|
"edit_file",
|
|
69
69
|
"create_docx",
|
|
@@ -78,7 +78,7 @@ WORKSPACE_WRITE_TOOLS: Set[str] = frozenset({
|
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
# Always blocked regardless of mode (Claude-style circuit breakers).
|
|
81
|
-
HARD_BLOCK_SANDBOXES:
|
|
81
|
+
HARD_BLOCK_SANDBOXES: FrozenSet[str] = frozenset({"system"})
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def normalize_mode(value: Any) -> PermissionMode:
|
|
@@ -29,6 +29,7 @@ from dataclasses import dataclass, field
|
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
31
31
|
|
|
32
|
+
from latticeai.core.quiet import quiet
|
|
32
33
|
|
|
33
34
|
PLUGIN_SDK_VERSION = "2.2.0"
|
|
34
35
|
|
|
@@ -309,7 +310,7 @@ class PluginRegistry:
|
|
|
309
310
|
register_skill(skill_name, plugin_id)
|
|
310
311
|
registered_skills.append(skill_name)
|
|
311
312
|
except Exception: # pragma: no cover - skill registration is best-effort
|
|
312
|
-
|
|
313
|
+
quiet()
|
|
313
314
|
entry = {}
|
|
314
315
|
if self.store is not None:
|
|
315
316
|
entry = self.store.mark_plugin_installed(
|
|
@@ -366,7 +367,7 @@ class PluginRegistry:
|
|
|
366
367
|
try:
|
|
367
368
|
self.store.record_timeline_event("plugins", event_type, payload, workspace_id=workspace_id)
|
|
368
369
|
except Exception:
|
|
369
|
-
|
|
370
|
+
quiet()
|
|
370
371
|
|
|
371
372
|
emit("plugin_started", {"plugin_id": plugin_id, "action": action})
|
|
372
373
|
|
package/latticeai/core/policy.py
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Deliberately-ignored exceptions, recorded instead of erased.
|
|
2
|
+
|
|
3
|
+
A local-first app that talks to optional hardware, optional models, optional
|
|
4
|
+
network paths and user files legitimately swallows a lot. ``except Exception:
|
|
5
|
+
pass`` is often the right *behaviour* — a failed probe should not take the
|
|
6
|
+
server down. What is not right is that the failure leaves no trace at all: a
|
|
7
|
+
genuine bug in one of those paths is indistinguishable from the optional thing
|
|
8
|
+
simply being absent, and nobody ever finds out.
|
|
9
|
+
|
|
10
|
+
``quiet()`` keeps the behaviour and removes the silence. It logs the live
|
|
11
|
+
exception at DEBUG with the file, function and line that suppressed it, so the
|
|
12
|
+
default experience is unchanged and ``LATTICEAI_LOG_LEVEL=DEBUG`` turns every
|
|
13
|
+
swallowed failure into a line you can read.
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
probe_optional_thing()
|
|
17
|
+
except Exception:
|
|
18
|
+
quiet("optional GPU probe")
|
|
19
|
+
|
|
20
|
+
The label is optional; without one the call site is still identified. Pass one
|
|
21
|
+
when the location alone would not tell a reader *why* failure is acceptable.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import sys
|
|
28
|
+
import traceback
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("latticeai.suppressed")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def quiet(reason: Optional[str] = None, *, level: int = logging.DEBUG) -> None:
|
|
35
|
+
"""Record the exception currently being handled, then continue.
|
|
36
|
+
|
|
37
|
+
Safe to call outside an ``except`` block (it becomes a no-op) so a refactor
|
|
38
|
+
that moves it cannot itself raise.
|
|
39
|
+
"""
|
|
40
|
+
exc_type, exc, tb = sys.exc_info()
|
|
41
|
+
if exc is None:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
where = "<unknown>"
|
|
45
|
+
if tb is not None:
|
|
46
|
+
# The frame that raised is the useful one; walk to the deepest.
|
|
47
|
+
deepest = tb
|
|
48
|
+
while deepest.tb_next is not None:
|
|
49
|
+
deepest = deepest.tb_next
|
|
50
|
+
frame = deepest.tb_frame
|
|
51
|
+
where = f"{frame.f_code.co_filename}:{frame.f_lineno} in {frame.f_code.co_name}"
|
|
52
|
+
|
|
53
|
+
label = f" ({reason})" if reason else ""
|
|
54
|
+
if logger.isEnabledFor(level):
|
|
55
|
+
logger.log(
|
|
56
|
+
level,
|
|
57
|
+
"suppressed %s at %s%s: %s",
|
|
58
|
+
getattr(exc_type, "__name__", "Exception"),
|
|
59
|
+
where,
|
|
60
|
+
label,
|
|
61
|
+
exc,
|
|
62
|
+
exc_info=exc,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def quiet_summary(reason: Optional[str] = None) -> str:
|
|
67
|
+
"""One-line description of the live exception, for callers that report it."""
|
|
68
|
+
exc_type, exc, _ = sys.exc_info()
|
|
69
|
+
if exc is None:
|
|
70
|
+
return ""
|
|
71
|
+
name = getattr(exc_type, "__name__", "Exception")
|
|
72
|
+
text = str(exc).strip() or name
|
|
73
|
+
return f"{reason}: {text}" if reason else text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_suppressed() -> str:
|
|
77
|
+
"""Full traceback of the live exception as text (for diagnostics payloads)."""
|
|
78
|
+
exc_type, exc, tb = sys.exc_info()
|
|
79
|
+
if exc is None:
|
|
80
|
+
return ""
|
|
81
|
+
return "".join(traceback.format_exception(exc_type, exc, tb))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["quiet", "quiet_summary", "format_suppressed"]
|
|
@@ -32,8 +32,9 @@ import threading
|
|
|
32
32
|
from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Set
|
|
33
33
|
|
|
34
34
|
from lattice_brain.runtime.contracts import realtime_event_contract
|
|
35
|
-
from .
|
|
35
|
+
from latticeai.core.quiet import quiet
|
|
36
36
|
|
|
37
|
+
from .timeutil import now_iso as _now
|
|
37
38
|
|
|
38
39
|
REALTIME_VERSION = "2.2.0"
|
|
39
40
|
_FEED_LIMIT = 200
|
|
@@ -120,7 +121,7 @@ class RealtimeBus:
|
|
|
120
121
|
sub.queue.get_nowait() # drop oldest
|
|
121
122
|
sub.queue.put_nowait(event)
|
|
122
123
|
except Exception:
|
|
123
|
-
|
|
124
|
+
quiet()
|
|
124
125
|
|
|
125
126
|
# The store calls ``event_sink(event)`` positionally; expose a stable alias.
|
|
126
127
|
def __call__(self, event: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -33,6 +33,7 @@ from typing import Any, Dict, List, Optional
|
|
|
33
33
|
|
|
34
34
|
from latticeai.core.agent import AgentRunContext, AgentState
|
|
35
35
|
from latticeai.core.agent_trace import LoopTrace
|
|
36
|
+
from latticeai.core.quiet import quiet
|
|
36
37
|
|
|
37
38
|
LOGGER = logging.getLogger(__name__)
|
|
38
39
|
|
|
@@ -148,7 +149,7 @@ class AgentRunStore:
|
|
|
148
149
|
try:
|
|
149
150
|
os.unlink(tmp_name)
|
|
150
151
|
except OSError:
|
|
151
|
-
|
|
152
|
+
quiet()
|
|
152
153
|
return True
|
|
153
154
|
except Exception as exc: # noqa: BLE001 — pause must still answer without disk
|
|
154
155
|
LOGGER.warning("agent run store save failed for %s: %s", run_id, exc)
|
|
@@ -193,6 +194,7 @@ class AgentRunStore:
|
|
|
193
194
|
try:
|
|
194
195
|
record = json.loads(path.read_text(encoding="utf-8"))
|
|
195
196
|
except Exception: # noqa: BLE001
|
|
197
|
+
quiet()
|
|
196
198
|
continue
|
|
197
199
|
if not isinstance(record, dict):
|
|
198
200
|
continue
|
|
@@ -238,7 +240,7 @@ class AgentRunStore:
|
|
|
238
240
|
path.unlink(missing_ok=True)
|
|
239
241
|
removed += 1
|
|
240
242
|
except OSError:
|
|
241
|
-
|
|
243
|
+
quiet()
|
|
242
244
|
return removed
|
|
243
245
|
|
|
244
246
|
|
|
@@ -10,6 +10,8 @@ from typing import Any, Dict, List
|
|
|
10
10
|
|
|
11
11
|
from fastapi import HTTPException
|
|
12
12
|
|
|
13
|
+
from latticeai.core.quiet import quiet
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def hash_password(password: str) -> str:
|
|
15
17
|
salt = secrets.token_hex(16)
|
|
@@ -65,6 +67,7 @@ def configure_trusted_proxies(values) -> int:
|
|
|
65
67
|
try:
|
|
66
68
|
networks.append(ipaddress.ip_network(item, strict=False))
|
|
67
69
|
except ValueError:
|
|
70
|
+
quiet()
|
|
68
71
|
continue
|
|
69
72
|
_trusted_proxies = networks
|
|
70
73
|
return len(networks)
|
|
@@ -93,6 +96,7 @@ def client_ip(request) -> str:
|
|
|
93
96
|
ipaddress.ip_address(candidate)
|
|
94
97
|
return candidate
|
|
95
98
|
except ValueError:
|
|
99
|
+
quiet()
|
|
96
100
|
continue
|
|
97
101
|
return peer or "unknown"
|
|
98
102
|
|
package/latticeai/core/users.py
CHANGED
|
@@ -6,13 +6,15 @@ import json
|
|
|
6
6
|
import shutil
|
|
7
7
|
import sqlite3
|
|
8
8
|
import uuid
|
|
9
|
+
from contextlib import closing
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, Dict, Optional
|
|
11
12
|
|
|
13
|
+
from latticeai.core.quiet import quiet
|
|
14
|
+
|
|
12
15
|
from .io_utils import atomic_write_json
|
|
13
16
|
from .timeutil import now_iso as _now
|
|
14
17
|
|
|
15
|
-
|
|
16
18
|
USER_NAMESPACE = uuid.UUID("5d6d4480-cf79-49c3-a6d0-4c6eec3224d6")
|
|
17
19
|
|
|
18
20
|
|
|
@@ -76,7 +78,7 @@ def load_users_file(path: Path) -> Dict[str, Any]:
|
|
|
76
78
|
try:
|
|
77
79
|
shutil.copy2(path, backup)
|
|
78
80
|
except Exception:
|
|
79
|
-
|
|
81
|
+
quiet()
|
|
80
82
|
atomic_write_json(path, migrated)
|
|
81
83
|
return migrated
|
|
82
84
|
|
|
@@ -112,7 +114,7 @@ def migrate_knowledge_graph_identity(db_path: Path, email_to_id: Dict[str, str])
|
|
|
112
114
|
if not db_path.exists() or not email_to_id:
|
|
113
115
|
return 0
|
|
114
116
|
changed = 0
|
|
115
|
-
with sqlite3.connect(db_path) as conn:
|
|
117
|
+
with closing(sqlite3.connect(db_path)) as conn, conn:
|
|
116
118
|
tables = {
|
|
117
119
|
row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
118
120
|
}
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
from typing import Any, Dict, List, Optional
|
|
5
5
|
|
|
6
|
+
from latticeai.core.quiet import quiet
|
|
7
|
+
|
|
6
8
|
from .timeutil import now_iso as _now
|
|
7
9
|
from .workspace_os_utils import _json_hash, _listify
|
|
8
10
|
|
|
@@ -97,6 +99,7 @@ class WorkspaceGraphTrace:
|
|
|
97
99
|
if len(edges) >= 24:
|
|
98
100
|
break
|
|
99
101
|
except Exception:
|
|
102
|
+
quiet()
|
|
100
103
|
continue
|
|
101
104
|
|
|
102
105
|
if matches:
|