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 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()