synclave 1.0.0__py3-none-any.whl
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.
- synclave/__init__.py +22 -0
- synclave/a2a_cli.py +164 -0
- synclave/agent_identity.py +847 -0
- synclave/agent_mesh_a2a.py +440 -0
- synclave/cli.py +17 -0
- synclave/conversation_bridge.py +223 -0
- synclave/gpu_agent.py +181 -0
- synclave/gpu_task.py +130 -0
- synclave/inbox_worker.py +105 -0
- synclave/node_agent.py +397 -0
- synclave/sync_common_knowledge.py +502 -0
- synclave/sync_coordinator.py +205 -0
- synclave/sync_memory.py +425 -0
- synclave/sync_motor.py +3190 -0
- synclave/sync_retention.py +166 -0
- synclave-1.0.0.dist-info/METADATA +221 -0
- synclave-1.0.0.dist-info/RECORD +21 -0
- synclave-1.0.0.dist-info/WHEEL +5 -0
- synclave-1.0.0.dist-info/entry_points.txt +4 -0
- synclave-1.0.0.dist-info/licenses/LICENSE +21 -0
- synclave-1.0.0.dist-info/top_level.txt +1 -0
synclave/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""synclave — encrypted multi-node backup, replication, and agent mesh (A2A).
|
|
2
|
+
|
|
3
|
+
Eski ad: hermes-sync (v2.3.1) → rebrand: synclave (v1.0.0).
|
|
4
|
+
"""
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
|
|
7
|
+
from . import (
|
|
8
|
+
sync_motor,
|
|
9
|
+
sync_common_knowledge,
|
|
10
|
+
sync_memory,
|
|
11
|
+
sync_retention,
|
|
12
|
+
node_agent,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"sync_motor",
|
|
17
|
+
"sync_common_knowledge",
|
|
18
|
+
"sync_memory",
|
|
19
|
+
"sync_retention",
|
|
20
|
+
"node_agent",
|
|
21
|
+
"__version__",
|
|
22
|
+
]
|
synclave/a2a_cli.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
a2a_cli.py — A2A mesh client (CumulusNET)
|
|
4
|
+
|
|
5
|
+
Kullanım:
|
|
6
|
+
a2a_cli.py send <host> "<görev metni>" [--token X] → task_id + durum
|
|
7
|
+
a2a_cli.py send-status <host> [--token X] → makine durumu iste
|
|
8
|
+
a2a_cli.py get <host> <task_id> [--token X] → sonuç
|
|
9
|
+
a2a_cli.py card <host> [--token X] → AgentCard
|
|
10
|
+
a2a_cli.py ping <host> [--token X] → sağlık
|
|
11
|
+
|
|
12
|
+
Host örnekleri: 100.103.44.107 (H3), 100.92.2.47 (H1), 100.76.82.46 (H2)
|
|
13
|
+
"""
|
|
14
|
+
# pyright: reportOptionalMemberAccess=false
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
import urllib.parse
|
|
20
|
+
import urllib.request
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
24
|
+
_CARD_CACHE: dict[str, tuple] = {}
|
|
25
|
+
_CARD_TTL = 300
|
|
26
|
+
try:
|
|
27
|
+
import agent_identity as AI
|
|
28
|
+
IDENTITY_OK = True
|
|
29
|
+
except Exception:
|
|
30
|
+
AI = None
|
|
31
|
+
IDENTITY_OK = False
|
|
32
|
+
|
|
33
|
+
_IDENT = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def identity():
|
|
37
|
+
"""Yerel kimlik (imza için). Yoksa imzasız gönderilir (eski davranış)."""
|
|
38
|
+
global _IDENT
|
|
39
|
+
if _IDENT is None and IDENTITY_OK:
|
|
40
|
+
try:
|
|
41
|
+
_IDENT = AI.AgentIdentity.load_or_create()
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
return _IDENT
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def peer_card(host: str, port: int = 8643):
|
|
48
|
+
"""Karşı tarafın AgentCard'ı (önbellekli). Şifreli gönderim için X25519
|
|
49
|
+
açık anahtarı ve şifreleme desteği buradan alınır."""
|
|
50
|
+
now = time.time()
|
|
51
|
+
key = f"{host}:{port}"
|
|
52
|
+
hit = _CARD_CACHE.get(key)
|
|
53
|
+
if hit and now - hit[1] < _CARD_TTL:
|
|
54
|
+
return hit[0]
|
|
55
|
+
req = urllib.request.Request(f"http://{host}:{port}/.well-known/agent.json")
|
|
56
|
+
try:
|
|
57
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
58
|
+
card = json.loads(resp.read().decode())
|
|
59
|
+
_CARD_CACHE[key] = (card, now)
|
|
60
|
+
return card
|
|
61
|
+
except Exception:
|
|
62
|
+
return hit[0] if hit else {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def rpc(host: str, method: str, params: dict, token: str, port: int = 8643,
|
|
66
|
+
sign: bool = True, conv_id: str = "", encrypt: bool = True):
|
|
67
|
+
url = f"http://{host}:{port}/"
|
|
68
|
+
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
|
|
69
|
+
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"})
|
|
70
|
+
if token:
|
|
71
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
72
|
+
ident = identity() if sign else None
|
|
73
|
+
if ident:
|
|
74
|
+
# Klon şüphesinde kendi kimliğimizle mesaj GÖNDERMEYİZ (fail-closed)
|
|
75
|
+
ident.assert_not_clone()
|
|
76
|
+
card = peer_card(host, port)
|
|
77
|
+
peer_enc = (card.get("capabilities") or {}).get("encryptedRequests")
|
|
78
|
+
peer_x = (card.get("identity") or {}).get("x25519_public", "")
|
|
79
|
+
if encrypt and peer_enc and peer_x:
|
|
80
|
+
# ŞİFRELİ gövde: X25519 ECDH + AES-GCM (PFS) + Ed25519 imza
|
|
81
|
+
peer_aid = (card.get("identity") or {}).get("agent_id", "")
|
|
82
|
+
env = ident.secure_payload(peer_x, body, to_agent=peer_aid)
|
|
83
|
+
env["runtime"] = ident.runtime
|
|
84
|
+
env["machine_label"] = ident.meta.get("machine_label", "")
|
|
85
|
+
body2 = json.dumps({"enc": env}).encode()
|
|
86
|
+
req.data = body2
|
|
87
|
+
req.add_header("X-Agent-Enc", "v1")
|
|
88
|
+
req.add_header("X-Agent-Label", ident.meta.get("machine_label", ""))
|
|
89
|
+
if conv_id:
|
|
90
|
+
req.add_header("X-Conversation-Id", conv_id)
|
|
91
|
+
else:
|
|
92
|
+
# İmzalı ama düz gövde (eski sunucu / şifreleme yok)
|
|
93
|
+
for k, v in ident.sign_request(method, body).items():
|
|
94
|
+
req.add_header(k, v)
|
|
95
|
+
req.add_header("X-Agent-Label", ident.meta.get("machine_label", ""))
|
|
96
|
+
if conv_id:
|
|
97
|
+
req.add_header("X-Conversation-Id", conv_id)
|
|
98
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
99
|
+
out = json.loads(resp.read().decode())
|
|
100
|
+
# Giden mesajı yerel sohbet defterine işle (karşı tarafın agent_id'si ile)
|
|
101
|
+
peer = ((out.get("result") or {}).get("served_by") or "") if isinstance(out, dict) else ""
|
|
102
|
+
if ident and peer.startswith(("hx-", "oc-")):
|
|
103
|
+
try:
|
|
104
|
+
cid = conv_id or AI.open_conversation("agent", peer, "a2a", identity=ident)
|
|
105
|
+
mid = AI.log_message(cid, "out", body, peer_id=peer,
|
|
106
|
+
meta={"method": method, "host": host})
|
|
107
|
+
if isinstance(out, dict):
|
|
108
|
+
out["_local_conversation_id"] = cid
|
|
109
|
+
out["_local_message_id"] = mid
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
def main():
|
|
115
|
+
ap = argparse.ArgumentParser()
|
|
116
|
+
ap.add_argument("komut", choices=["send", "send-status", "get", "card", "ping", "stream"])
|
|
117
|
+
ap.add_argument("host")
|
|
118
|
+
ap.add_argument("gorev", nargs="?", default="")
|
|
119
|
+
ap.add_argument("--task-id", default="")
|
|
120
|
+
ap.add_argument("--token", default="")
|
|
121
|
+
ap.add_argument("--port", type=int, default=8643)
|
|
122
|
+
ap.add_argument("--mode", choices=["sync", "async"], default="sync")
|
|
123
|
+
ap.add_argument("--seconds", type=int, default=8)
|
|
124
|
+
ap.add_argument("--no-sign", action="store_true", help="imzasız gönder (eski uyum)")
|
|
125
|
+
ap.add_argument("--conv", default="", help="mevcut sohbet ID'si ile devam et")
|
|
126
|
+
args = ap.parse_args()
|
|
127
|
+
sign = not args.no_sign
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
if args.komut == "send":
|
|
131
|
+
r = rpc(args.host, "task/send", {"payload": {"action": "note", "text": args.gorev},
|
|
132
|
+
"mode": args.mode}, args.token, args.port,
|
|
133
|
+
sign=sign, conv_id=args.conv)
|
|
134
|
+
print(json.dumps(r, ensure_ascii=False, indent=2))
|
|
135
|
+
elif args.komut == "send-status":
|
|
136
|
+
r = rpc(args.host, "task/send", {"payload": {"action": "status"}, "mode": args.mode},
|
|
137
|
+
args.token, args.port, sign=sign, conv_id=args.conv)
|
|
138
|
+
print(json.dumps(r, ensure_ascii=False, indent=2))
|
|
139
|
+
elif args.komut == "get":
|
|
140
|
+
r = rpc(args.host, "task/get", {"id": args.task_id}, args.token, args.port,
|
|
141
|
+
sign=sign, conv_id=args.conv)
|
|
142
|
+
print(json.dumps(r, ensure_ascii=False, indent=2))
|
|
143
|
+
elif args.komut == "card":
|
|
144
|
+
req = urllib.request.Request(f"http://{args.host}:{args.port}/.well-known/agent.json")
|
|
145
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
146
|
+
print(resp.read().decode())
|
|
147
|
+
elif args.komut == "ping":
|
|
148
|
+
req = urllib.request.Request(f"http://{args.host}:{args.port}/health")
|
|
149
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
150
|
+
print(resp.read().decode())
|
|
151
|
+
elif args.komut == "stream":
|
|
152
|
+
# Canlı SSE akışı: H1 → H3 mesaj akışını dinle
|
|
153
|
+
url = f"http://{args.host}:{args.port}/stream?message={urllib.parse.quote(args.gorev or 'selam')}&seconds={args.seconds}"
|
|
154
|
+
req = urllib.request.Request(url)
|
|
155
|
+
with urllib.request.urlopen(req, timeout=args.seconds + 10) as resp:
|
|
156
|
+
for line in resp:
|
|
157
|
+
if line.strip():
|
|
158
|
+
print(line.decode().strip())
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f"HATA: {e}", file=sys.stderr)
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|