ltcai 4.3.3 → 4.4.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 +21 -16
- package/docs/CHANGELOG.md +37 -0
- package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
- package/lattice_brain/__init__.py +38 -23
- package/lattice_brain/_kg_common.py +11 -1
- package/lattice_brain/context.py +212 -2
- package/lattice_brain/conversations.py +234 -1
- package/lattice_brain/discovery.py +11 -1
- package/lattice_brain/documents.py +11 -1
- package/lattice_brain/graph/__init__.py +28 -0
- package/lattice_brain/graph/_kg_common.py +1123 -0
- package/lattice_brain/graph/curator.py +473 -0
- package/lattice_brain/graph/discovery.py +1455 -0
- package/lattice_brain/graph/documents.py +218 -0
- package/lattice_brain/graph/identity.py +175 -0
- package/lattice_brain/graph/ingest.py +644 -0
- package/lattice_brain/graph/network.py +205 -0
- package/lattice_brain/graph/projection.py +571 -0
- package/lattice_brain/graph/provenance.py +401 -0
- package/lattice_brain/graph/retrieval.py +1341 -0
- package/lattice_brain/graph/schema.py +640 -0
- package/lattice_brain/graph/store.py +237 -0
- package/lattice_brain/graph/write_master.py +225 -0
- package/lattice_brain/identity.py +11 -13
- package/lattice_brain/ingest.py +11 -1
- package/lattice_brain/ingestion.py +318 -0
- package/lattice_brain/memory.py +100 -1
- package/lattice_brain/network.py +11 -1
- package/lattice_brain/portability.py +431 -0
- package/lattice_brain/projection.py +11 -1
- package/lattice_brain/provenance.py +11 -1
- package/lattice_brain/retrieval.py +11 -1
- package/lattice_brain/runtime/__init__.py +32 -0
- package/lattice_brain/runtime/agent_runtime.py +569 -0
- package/lattice_brain/runtime/hooks.py +754 -0
- package/lattice_brain/runtime/multi_agent.py +795 -0
- package/lattice_brain/schema.py +11 -1
- package/lattice_brain/store.py +10 -2
- package/lattice_brain/workflow.py +461 -0
- package/lattice_brain/write_master.py +11 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +2 -2
- package/latticeai/api/browser.py +1 -1
- package/latticeai/api/chat.py +1 -1
- package/latticeai/api/computer_use.py +1 -1
- package/latticeai/api/hooks.py +2 -2
- package/latticeai/api/mcp.py +1 -1
- package/latticeai/api/tools.py +1 -1
- package/latticeai/api/workflow_designer.py +2 -2
- package/latticeai/app_factory.py +4 -4
- package/latticeai/brain/__init__.py +24 -6
- package/latticeai/brain/_kg_common.py +11 -1117
- package/latticeai/brain/context.py +12 -208
- package/latticeai/brain/conversations.py +12 -231
- package/latticeai/brain/discovery.py +13 -1451
- package/latticeai/brain/documents.py +13 -214
- package/latticeai/brain/identity.py +11 -169
- package/latticeai/brain/ingest.py +13 -640
- package/latticeai/brain/memory.py +12 -97
- package/latticeai/brain/network.py +12 -200
- package/latticeai/brain/projection.py +13 -567
- package/latticeai/brain/provenance.py +13 -397
- package/latticeai/brain/retrieval.py +13 -1337
- package/latticeai/brain/schema.py +12 -635
- package/latticeai/brain/store.py +13 -233
- package/latticeai/brain/write_master.py +13 -221
- package/latticeai/core/agent.py +1 -1
- package/latticeai/core/agent_registry.py +2 -2
- package/latticeai/core/builtin_hooks.py +2 -2
- package/latticeai/core/graph_curator.py +6 -468
- package/latticeai/core/hooks.py +6 -749
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +6 -790
- package/latticeai/core/workflow_engine.py +6 -456
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/agent_runtime.py +6 -564
- package/latticeai/services/ingestion.py +6 -313
- package/latticeai/services/kg_portability.py +6 -426
- package/latticeai/services/platform_runtime.py +3 -3
- package/latticeai/services/run_executor.py +1 -1
- package/latticeai/services/upload_service.py +1 -1
- package/p_reinforce.py +1 -1
- package/package.json +1 -1
- package/scripts/bump_version.py +1 -1
- package/scripts/wheel_smoke.py +7 -0
- 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 +1 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Brain Network v1 — knowledge exchange between paired Lattice instances.
|
|
2
|
+
|
|
3
|
+
Local-first federation: no cloud rendezvous, no relay. A peer is another
|
|
4
|
+
Lattice installation you deliberately paired with by exchanging device
|
|
5
|
+
public keys (LAN/tailnet HTTP). Exchange is per-workspace, per-request,
|
|
6
|
+
owner-initiated: a signed export bundle is pushed to (or received from) a
|
|
7
|
+
paired peer, verified against the *paired* key, imported through the normal
|
|
8
|
+
import path, and stamped with origin-device provenance.
|
|
9
|
+
|
|
10
|
+
Peer requests authenticate independently of user sessions: each carries an
|
|
11
|
+
Ed25519 signature over (body sha256 + timestamp + nonce), with a freshness
|
|
12
|
+
window and a seen-nonce set for replay protection.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import uuid
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
from .identity import DeviceIdentity, fingerprint_of, verify_signature
|
|
27
|
+
|
|
28
|
+
PEER_AUTH_WINDOW_SECONDS = 300
|
|
29
|
+
_NONCE_CACHE_MAX = 4096
|
|
30
|
+
|
|
31
|
+
HEADER_DEVICE = "x-lattice-device"
|
|
32
|
+
HEADER_TIMESTAMP = "x-lattice-timestamp"
|
|
33
|
+
HEADER_NONCE = "x-lattice-nonce"
|
|
34
|
+
HEADER_SIGNATURE = "x-lattice-signature"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _signing_payload(body: bytes, timestamp: str, nonce: str) -> bytes:
|
|
38
|
+
body_digest = hashlib.sha256(body or b"").hexdigest()
|
|
39
|
+
return f"{body_digest}|{timestamp}|{nonce}".encode("ascii")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BrainNetwork:
|
|
43
|
+
"""Peer registry + signed bundle exchange."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
identity: DeviceIdentity,
|
|
49
|
+
portability: Any,
|
|
50
|
+
data_dir: Path,
|
|
51
|
+
http_client_factory: Any = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._identity = identity
|
|
54
|
+
self._portability = portability
|
|
55
|
+
self._peers_file = Path(data_dir) / "brain_peers.json"
|
|
56
|
+
self._lock = threading.Lock()
|
|
57
|
+
self._seen_nonces: Dict[str, float] = {}
|
|
58
|
+
# injectable for tests; default builds an httpx client per call
|
|
59
|
+
self._http_client_factory = http_client_factory
|
|
60
|
+
|
|
61
|
+
# ── peer registry (deliberate pairing) ─────────────────────────────────
|
|
62
|
+
def _load_peers(self) -> List[Dict[str, Any]]:
|
|
63
|
+
if not self._peers_file.exists():
|
|
64
|
+
return []
|
|
65
|
+
try:
|
|
66
|
+
return json.loads(self._peers_file.read_text(encoding="utf-8"))
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
logging.warning("brain network: peer registry unreadable: %s", exc)
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
def _save_peers(self, peers: List[Dict[str, Any]]) -> None:
|
|
72
|
+
self._peers_file.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
tmp = self._peers_file.with_suffix(".tmp")
|
|
74
|
+
tmp.write_text(json.dumps(peers, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
75
|
+
tmp.replace(self._peers_file)
|
|
76
|
+
|
|
77
|
+
def list_peers(self) -> List[Dict[str, Any]]:
|
|
78
|
+
return self._load_peers()
|
|
79
|
+
|
|
80
|
+
def add_peer(self, *, name: str, base_url: str, public_key: str) -> Dict[str, Any]:
|
|
81
|
+
name = str(name or "").strip()
|
|
82
|
+
base_url = str(base_url or "").strip().rstrip("/")
|
|
83
|
+
public_key = str(public_key or "").strip()
|
|
84
|
+
if not name or not base_url or not public_key:
|
|
85
|
+
raise ValueError("pairing requires name, base_url, and the peer's public key")
|
|
86
|
+
if not base_url.startswith(("http://", "https://")):
|
|
87
|
+
raise ValueError("base_url must be an http(s) URL")
|
|
88
|
+
try:
|
|
89
|
+
fingerprint = fingerprint_of(public_key)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
raise ValueError(f"public_key is not a valid Ed25519 key: {exc}") from exc
|
|
92
|
+
with self._lock:
|
|
93
|
+
peers = self._load_peers()
|
|
94
|
+
if any(p.get("public_key") == public_key for p in peers):
|
|
95
|
+
raise ValueError("this device is already paired")
|
|
96
|
+
peer = {
|
|
97
|
+
"id": f"peer-{uuid.uuid4().hex[:12]}",
|
|
98
|
+
"name": name,
|
|
99
|
+
"base_url": base_url,
|
|
100
|
+
"public_key": public_key,
|
|
101
|
+
"fingerprint": fingerprint,
|
|
102
|
+
"added_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
103
|
+
}
|
|
104
|
+
peers.append(peer)
|
|
105
|
+
self._save_peers(peers)
|
|
106
|
+
return peer
|
|
107
|
+
|
|
108
|
+
def remove_peer(self, peer_id: str) -> Dict[str, Any]:
|
|
109
|
+
with self._lock:
|
|
110
|
+
peers = self._load_peers()
|
|
111
|
+
kept = [p for p in peers if p.get("id") != peer_id]
|
|
112
|
+
if len(kept) == len(peers):
|
|
113
|
+
raise FileNotFoundError(peer_id)
|
|
114
|
+
self._save_peers(kept)
|
|
115
|
+
return {"status": "removed", "peer_id": peer_id}
|
|
116
|
+
|
|
117
|
+
def _peer_by_id(self, peer_id: str) -> Dict[str, Any]:
|
|
118
|
+
peer = next((p for p in self._load_peers() if p.get("id") == peer_id), None)
|
|
119
|
+
if peer is None:
|
|
120
|
+
raise FileNotFoundError(peer_id)
|
|
121
|
+
return peer
|
|
122
|
+
|
|
123
|
+
# ── request authentication (peer → this brain) ────────────────────────
|
|
124
|
+
def auth_headers(self, body: bytes) -> Dict[str, str]:
|
|
125
|
+
"""Headers this device attaches when pushing to a peer."""
|
|
126
|
+
timestamp = str(int(time.time()))
|
|
127
|
+
nonce = uuid.uuid4().hex
|
|
128
|
+
return {
|
|
129
|
+
HEADER_DEVICE: self._identity.public_key_b64,
|
|
130
|
+
HEADER_TIMESTAMP: timestamp,
|
|
131
|
+
HEADER_NONCE: nonce,
|
|
132
|
+
HEADER_SIGNATURE: self._identity.sign(_signing_payload(body, timestamp, nonce)),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def verify_peer_request(self, headers: Dict[str, str], body: bytes) -> Dict[str, Any]:
|
|
136
|
+
"""Authenticate an inbound peer request. Raises PermissionError."""
|
|
137
|
+
lowered = {str(k).lower(): v for k, v in headers.items()}
|
|
138
|
+
device = lowered.get(HEADER_DEVICE) or ""
|
|
139
|
+
timestamp = lowered.get(HEADER_TIMESTAMP) or ""
|
|
140
|
+
nonce = lowered.get(HEADER_NONCE) or ""
|
|
141
|
+
signature = lowered.get(HEADER_SIGNATURE) or ""
|
|
142
|
+
if not device or not timestamp or not nonce or not signature:
|
|
143
|
+
raise PermissionError("missing peer authentication headers")
|
|
144
|
+
peer = next((p for p in self._load_peers() if p.get("public_key") == device), None)
|
|
145
|
+
if peer is None:
|
|
146
|
+
raise PermissionError("device is not a paired peer")
|
|
147
|
+
try:
|
|
148
|
+
age = abs(time.time() - int(timestamp))
|
|
149
|
+
except ValueError:
|
|
150
|
+
raise PermissionError("invalid timestamp")
|
|
151
|
+
if age > PEER_AUTH_WINDOW_SECONDS:
|
|
152
|
+
raise PermissionError("request outside the freshness window")
|
|
153
|
+
with self._lock:
|
|
154
|
+
if nonce in self._seen_nonces:
|
|
155
|
+
raise PermissionError("replayed nonce")
|
|
156
|
+
self._seen_nonces[nonce] = time.time()
|
|
157
|
+
if len(self._seen_nonces) > _NONCE_CACHE_MAX:
|
|
158
|
+
cutoff = time.time() - PEER_AUTH_WINDOW_SECONDS * 2
|
|
159
|
+
self._seen_nonces = {n: t for n, t in self._seen_nonces.items() if t > cutoff}
|
|
160
|
+
if not verify_signature(device, _signing_payload(body, timestamp, nonce), signature):
|
|
161
|
+
raise PermissionError("peer request signature invalid")
|
|
162
|
+
return peer
|
|
163
|
+
|
|
164
|
+
# ── exchange ────────────────────────────────────────────────────────────
|
|
165
|
+
def push_to_peer(self, peer_id: str, *, workspace_id: Optional[str] = None, timeout: float = 30.0) -> Dict[str, Any]:
|
|
166
|
+
"""Owner-initiated: export (signed) and push to one paired peer."""
|
|
167
|
+
peer = self._peer_by_id(peer_id)
|
|
168
|
+
artifact = self._portability.export(workspace_id=workspace_id)
|
|
169
|
+
body = json.dumps(artifact, ensure_ascii=False).encode("utf-8")
|
|
170
|
+
headers = {**self.auth_headers(body), "Content-Type": "application/json"}
|
|
171
|
+
url = f"{peer['base_url']}/network/receive"
|
|
172
|
+
if self._http_client_factory is not None:
|
|
173
|
+
response = self._http_client_factory().post(url, content=body, headers=headers, timeout=timeout)
|
|
174
|
+
else:
|
|
175
|
+
import httpx
|
|
176
|
+
|
|
177
|
+
with httpx.Client() as client:
|
|
178
|
+
response = client.post(url, content=body, headers=headers, timeout=timeout)
|
|
179
|
+
payload = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
|
|
180
|
+
return {
|
|
181
|
+
"status": "ok" if response.status_code == 200 else "failed",
|
|
182
|
+
"http_status": response.status_code,
|
|
183
|
+
"peer": {"id": peer["id"], "name": peer["name"], "fingerprint": peer["fingerprint"]},
|
|
184
|
+
"peer_result": payload,
|
|
185
|
+
"counts": (artifact.get("header") or {}).get("counts"),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
def receive(self, headers: Dict[str, str], body: bytes) -> Dict[str, Any]:
|
|
189
|
+
"""Inbound: authenticate the peer, verify the bundle, import."""
|
|
190
|
+
peer = self.verify_peer_request(headers, body)
|
|
191
|
+
try:
|
|
192
|
+
artifact = json.loads(body.decode("utf-8"))
|
|
193
|
+
except Exception:
|
|
194
|
+
raise ValueError("body is not a JSON bundle")
|
|
195
|
+
signature = artifact.get("signature") or {}
|
|
196
|
+
# On the network path the bundle itself MUST be signed by the paired
|
|
197
|
+
# peer too (unsigned-legacy applies to local file imports only).
|
|
198
|
+
if signature.get("public_key") != peer.get("public_key"):
|
|
199
|
+
raise PermissionError("bundle signer does not match the paired peer")
|
|
200
|
+
result = self._portability.import_data(artifact, mode="merge")
|
|
201
|
+
result["peer"] = {"id": peer["id"], "name": peer["name"], "fingerprint": peer["fingerprint"]}
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
__all__ = ["BrainNetwork", "PEER_AUTH_WINDOW_SECONDS"]
|