keep-protocol 0.1.0__py3-none-any.whl → 0.3.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.
keep/__init__.py CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  from keep.client import KeepClient
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.3.0"
6
6
  __all__ = ["KeepClient"]
keep/client.py CHANGED
@@ -1,19 +1,30 @@
1
1
  """Keep protocol client -- sign and send packets over TCP."""
2
2
 
3
+ import json
3
4
  import socket
5
+ import struct
4
6
  import uuid
5
- from typing import Optional
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Callable, Optional
6
10
 
7
11
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
8
12
 
9
13
  from keep import keep_pb2
10
14
 
15
+ MAX_PACKET_SIZE = 65536
16
+
11
17
 
12
18
  class KeepClient:
13
19
  """Client for the keep-protocol server.
14
20
 
15
21
  Generates an ephemeral ed25519 keypair on init (or accepts an existing one).
16
22
  Signs every outgoing packet.
23
+
24
+ Supports two modes:
25
+ - Ephemeral (default): opens/closes a TCP connection per send() call.
26
+ - Persistent: call connect() or use as context manager to hold a connection
27
+ open for sending and receiving routed messages via listen().
17
28
  """
18
29
 
19
30
  def __init__(
@@ -22,29 +33,105 @@ class KeepClient:
22
33
  port: int = 9009,
23
34
  private_key: Optional[Ed25519PrivateKey] = None,
24
35
  timeout: float = 10.0,
36
+ src: Optional[str] = None,
25
37
  ):
26
38
  self.host = host
27
39
  self.port = port
28
40
  self.timeout = timeout
41
+ self.src = src or "bot:keep-client"
29
42
  self._private_key = private_key or Ed25519PrivateKey.generate()
30
43
  self._public_key = self._private_key.public_key()
31
44
  self._pk_bytes = self._public_key.public_bytes_raw()
45
+ self._sock: Optional[socket.socket] = None
32
46
 
33
- def send(
47
+ # -- Connection management --
48
+
49
+ def connect(self) -> None:
50
+ """Open a persistent TCP connection to the server."""
51
+ if self._sock is not None:
52
+ return
53
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
54
+ s.settimeout(self.timeout)
55
+ s.connect((self.host, self.port))
56
+ self._sock = s
57
+
58
+ def disconnect(self) -> None:
59
+ """Close the persistent connection."""
60
+ if self._sock is not None:
61
+ try:
62
+ self._sock.close()
63
+ except OSError:
64
+ pass
65
+ self._sock = None
66
+
67
+ def __enter__(self) -> "KeepClient":
68
+ self.connect()
69
+ return self
70
+
71
+ def __exit__(self, *exc) -> None:
72
+ self.disconnect()
73
+
74
+ # -- Framing helpers --
75
+
76
+ @staticmethod
77
+ def _recv_exact(sock: socket.socket, n: int) -> bytes:
78
+ """Read exactly n bytes from sock."""
79
+ chunks = []
80
+ remaining = n
81
+ while remaining > 0:
82
+ chunk = sock.recv(min(remaining, 4096))
83
+ if not chunk:
84
+ raise ConnectionError(
85
+ f"Connection closed: expected {n} bytes, got {n - remaining}"
86
+ )
87
+ chunks.append(chunk)
88
+ remaining -= len(chunk)
89
+ return b"".join(chunks)
90
+
91
+ @staticmethod
92
+ def _send_framed(sock: socket.socket, data: bytes) -> None:
93
+ """Send data with a 4-byte big-endian length prefix."""
94
+ if len(data) > MAX_PACKET_SIZE:
95
+ raise ValueError(f"Packet too large: {len(data)} > {MAX_PACKET_SIZE}")
96
+ header = struct.pack(">I", len(data))
97
+ sock.sendall(header + data)
98
+
99
+ @classmethod
100
+ def _recv_framed(cls, sock: socket.socket) -> bytes:
101
+ """Read a length-prefixed frame: 4-byte BE header + payload."""
102
+ header = cls._recv_exact(sock, 4)
103
+ (msg_len,) = struct.unpack(">I", header)
104
+ if msg_len == 0:
105
+ raise ConnectionError("Received zero-length frame")
106
+ if msg_len > MAX_PACKET_SIZE:
107
+ raise ConnectionError(f"Frame too large: {msg_len} > {MAX_PACKET_SIZE}")
108
+ return cls._recv_exact(sock, msg_len)
109
+
110
+ @classmethod
111
+ def _read_packet(cls, sock: socket.socket) -> keep_pb2.Packet:
112
+ """Read and parse one framed Packet from sock."""
113
+ data = cls._recv_framed(sock)
114
+ p = keep_pb2.Packet()
115
+ p.ParseFromString(data)
116
+ return p
117
+
118
+ # -- Signing --
119
+
120
+ def _sign_packet(
34
121
  self,
35
122
  body: str,
36
- src: str = "bot:keep-client",
123
+ src: Optional[str] = None,
37
124
  dst: str = "server",
38
125
  typ: int = 0,
39
126
  fee: int = 0,
40
127
  ttl: int = 60,
41
128
  msg_id: Optional[str] = None,
42
129
  scar: bytes = b"",
43
- ) -> keep_pb2.Packet:
44
- """Sign and send a packet, return the server's reply."""
130
+ ) -> bytes:
131
+ """Build, sign, and serialize a Packet. Returns wire bytes."""
45
132
  msg_id = msg_id or str(uuid.uuid4())
133
+ src = src or self.src
46
134
 
47
- # Build unsigned packet
48
135
  p = keep_pb2.Packet()
49
136
  p.typ = typ
50
137
  p.id = msg_id
@@ -55,25 +142,237 @@ class KeepClient:
55
142
  p.ttl = ttl
56
143
  p.scar = scar
57
144
 
58
- # Sign
59
145
  sign_payload = p.SerializeToString()
60
146
  sig_bytes = self._private_key.sign(sign_payload)
61
147
 
62
- # Set sig + pk
63
148
  p.sig = sig_bytes
64
149
  p.pk = self._pk_bytes
65
- wire_data = p.SerializeToString()
150
+ return p.SerializeToString()
66
151
 
67
- # Send over TCP
152
+ # -- Send --
153
+
154
+ def send(
155
+ self,
156
+ body: str,
157
+ src: Optional[str] = None,
158
+ dst: str = "server",
159
+ typ: int = 0,
160
+ fee: int = 0,
161
+ ttl: int = 60,
162
+ msg_id: Optional[str] = None,
163
+ scar: bytes = b"",
164
+ wait_reply: Optional[bool] = None,
165
+ ) -> Optional[keep_pb2.Packet]:
166
+ """Sign and send a packet.
167
+
168
+ In ephemeral mode (no connect() called): opens a connection, sends,
169
+ reads the reply, and closes. Always returns the server's reply.
170
+
171
+ In persistent mode: sends on the open connection.
172
+ - wait_reply=True: blocks until a reply is received and returns it.
173
+ - wait_reply=False: sends without waiting. Returns None.
174
+ - wait_reply=None (default): waits if dst is "server" or "",
175
+ does not wait otherwise.
176
+ """
177
+ wire_data = self._sign_packet(
178
+ body=body,
179
+ src=src,
180
+ dst=dst,
181
+ typ=typ,
182
+ fee=fee,
183
+ ttl=ttl,
184
+ msg_id=msg_id,
185
+ scar=scar,
186
+ )
187
+
188
+ if self._sock is not None:
189
+ # Persistent mode
190
+ self._send_framed(self._sock, wire_data)
191
+
192
+ should_wait = wait_reply
193
+ if should_wait is None:
194
+ should_wait = dst in ("server", "") or dst.startswith("discover:")
195
+
196
+ if should_wait:
197
+ return self._read_packet(self._sock)
198
+ return None
199
+
200
+ # Ephemeral mode — open/close per call
68
201
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
69
202
  s.settimeout(self.timeout)
70
203
  try:
71
204
  s.connect((self.host, self.port))
72
- s.sendall(wire_data)
73
- reply_data = s.recv(4096)
205
+ self._send_framed(s, wire_data)
206
+ reply_data = self._recv_framed(s)
74
207
  finally:
75
208
  s.close()
76
209
 
77
210
  resp = keep_pb2.Packet()
78
211
  resp.ParseFromString(reply_data)
79
212
  return resp
213
+
214
+ # -- Listen --
215
+
216
+ def listen(
217
+ self,
218
+ callback: Callable[[keep_pb2.Packet], None],
219
+ timeout: Optional[float] = None,
220
+ ) -> None:
221
+ """Block and read packets from the persistent connection.
222
+
223
+ Invokes callback(packet) for each received packet.
224
+ Heartbeat packets (typ=2) are silently filtered.
225
+
226
+ Args:
227
+ callback: Called with each received Packet.
228
+ timeout: Seconds to listen before returning. None = listen until
229
+ the connection closes or an error occurs.
230
+
231
+ Raises:
232
+ RuntimeError: If not connected (call connect() first).
233
+ """
234
+ if self._sock is None:
235
+ raise RuntimeError("Not connected. Call connect() first.")
236
+
237
+ if timeout is not None:
238
+ self._sock.settimeout(timeout)
239
+
240
+ try:
241
+ while True:
242
+ p = self._read_packet(self._sock)
243
+ # Filter heartbeat packets
244
+ if p.typ == 2:
245
+ continue
246
+ callback(p)
247
+ except socket.timeout:
248
+ return
249
+ except ConnectionError:
250
+ return
251
+ finally:
252
+ if timeout is not None:
253
+ self._sock.settimeout(self.timeout)
254
+
255
+ # -- Discovery --
256
+
257
+ def discover(self, query: str = "info") -> dict:
258
+ """Send a discovery query and return parsed JSON response.
259
+
260
+ Args:
261
+ query: Discovery type — "info", "agents", or "stats".
262
+
263
+ Returns:
264
+ Parsed JSON dict from the server's response body.
265
+ """
266
+ reply = self.send(body="", dst=f"discover:{query}")
267
+ return json.loads(reply.body)
268
+
269
+ def discover_agents(self) -> list:
270
+ """Return list of currently connected agent identities."""
271
+ info = self.discover("agents")
272
+ return info.get("agents", [])
273
+
274
+ # -- Endpoint caching --
275
+
276
+ _CACHE_DIR = Path.home() / ".keep"
277
+ _CACHE_FILE = _CACHE_DIR / "endpoints.json"
278
+
279
+ @staticmethod
280
+ def cache_endpoint(host: str, port: int, info: dict) -> None:
281
+ """Cache a discovered endpoint in ~/.keep/endpoints.json.
282
+
283
+ Args:
284
+ host: Server hostname or IP.
285
+ port: Server port.
286
+ info: Server info dict (from discover("info")).
287
+ """
288
+ cache_dir = Path.home() / ".keep"
289
+ cache_file = cache_dir / "endpoints.json"
290
+
291
+ # Load existing cache
292
+ endpoints = []
293
+ if cache_file.exists():
294
+ try:
295
+ data = json.loads(cache_file.read_text())
296
+ endpoints = data.get("endpoints", [])
297
+ except (json.JSONDecodeError, OSError):
298
+ endpoints = []
299
+
300
+ # Update or append
301
+ now = datetime.now(timezone.utc).isoformat()
302
+ entry = {
303
+ "host": host,
304
+ "port": port,
305
+ "version": info.get("version", ""),
306
+ "agents_online": info.get("agents_online", 0),
307
+ "last_seen": now,
308
+ }
309
+
310
+ updated = False
311
+ for i, ep in enumerate(endpoints):
312
+ if ep.get("host") == host and ep.get("port") == port:
313
+ endpoints[i] = entry
314
+ updated = True
315
+ break
316
+ if not updated:
317
+ endpoints.append(entry)
318
+
319
+ cache_dir.mkdir(parents=True, exist_ok=True)
320
+ cache_file.write_text(json.dumps({"endpoints": endpoints}, indent=2))
321
+
322
+ @classmethod
323
+ def from_cache(
324
+ cls,
325
+ src: str = "bot:keep-client",
326
+ private_key: Optional[Ed25519PrivateKey] = None,
327
+ timeout: float = 5.0,
328
+ ) -> "KeepClient":
329
+ """Create a client by trying cached endpoints.
330
+
331
+ Reads ~/.keep/endpoints.json and attempts to connect to each
332
+ endpoint in order, returning the first successful connection.
333
+
334
+ Args:
335
+ src: Agent identity for this client.
336
+ private_key: Optional ed25519 private key.
337
+ timeout: Connection timeout per endpoint attempt.
338
+
339
+ Returns:
340
+ A connected KeepClient instance.
341
+
342
+ Raises:
343
+ ConnectionError: If no cached endpoint is reachable.
344
+ """
345
+ cache_file = Path.home() / ".keep" / "endpoints.json"
346
+ if not cache_file.exists():
347
+ raise ConnectionError("No cached endpoints (~/.keep/endpoints.json not found)")
348
+
349
+ try:
350
+ data = json.loads(cache_file.read_text())
351
+ endpoints = data.get("endpoints", [])
352
+ except (json.JSONDecodeError, OSError) as e:
353
+ raise ConnectionError(f"Failed to read endpoint cache: {e}")
354
+
355
+ if not endpoints:
356
+ raise ConnectionError("Endpoint cache is empty")
357
+
358
+ last_error = None
359
+ for ep in endpoints:
360
+ host = ep.get("host", "localhost")
361
+ port = ep.get("port", 9009)
362
+ try:
363
+ client = cls(
364
+ host=host,
365
+ port=port,
366
+ private_key=private_key,
367
+ timeout=timeout,
368
+ src=src,
369
+ )
370
+ # Test the connection
371
+ info = client.discover("info")
372
+ client.cache_endpoint(host, port, info)
373
+ return client
374
+ except (OSError, ConnectionError, json.JSONDecodeError) as e:
375
+ last_error = e
376
+ continue
377
+
378
+ raise ConnectionError(f"No cached endpoint reachable (last error: {last_error})")
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: keep-protocol
3
+ Version: 0.3.0
4
+ Summary: Signed protobuf packets over TCP for AI agent-to-agent communication
5
+ Author: Chris Crawford
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/CLCrawford-dev/keep-protocol
8
+ Project-URL: Repository, https://github.com/CLCrawford-dev/keep-protocol
9
+ Project-URL: Issues, https://github.com/CLCrawford-dev/keep-protocol/issues
10
+ Keywords: agent-protocol,ai-agents,protobuf,ed25519,tcp,agent-communication,mcp,multi-agent
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications
21
+ Classifier: Topic :: Security :: Cryptography
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: protobuf>=4.25
26
+ Requires-Dist: cryptography>=41.0
27
+
28
+ # keep-protocol
29
+
30
+ **Signed Protobuf packets over TCP for AI agent-to-agent communication**
31
+ Claw to claw. Fast. Verifiable. No central authority.
32
+
33
+ > Now available on ClawHub: https://www.clawhub.ai/skills/keep-protocol
34
+ > (Search "keep-protocol" or tags: agent-coordination protobuf tcp ed25519 moltbot openclaw swarm intent)
35
+
36
+ Agents send lightweight `Packet`s to a TCP endpoint (default :9009).
37
+ Unsigned or invalid signatures → **silence** (dropped, no reply).
38
+ Valid ed25519 sig → parsed, logged, replied with `{"body": "done"}`.
39
+
40
+ ### Packet (keep.proto)
41
+
42
+ ```proto
43
+ message Packet {
44
+ bytes sig = 1; // ed25519 signature (64 bytes)
45
+ bytes pk = 2; // sender's public key (32 bytes)
46
+ uint32 typ = 3; // 0=ask, 1=offer, 2=heartbeat, ...
47
+ string id = 4; // unique ID
48
+ string src = 5; // "bot:my-agent" or "human:chris"
49
+ string dst = 6; // "server", "nearest:weather", "swarm:sailing"
50
+ string body = 7; // intent / payload
51
+ uint64 fee = 8; // micro-fee in satoshis (anti-spam)
52
+ uint32 ttl = 9; // time-to-live seconds
53
+ bytes scar = 10; // gitmem-style memory commit (optional)
54
+ }
55
+ ```
56
+
57
+ Signature is over serialized bytes without sig/pk (reconstruct & verify).
58
+
59
+ ## Quick Start
60
+
61
+ **Run server (Docker, one-liner):**
62
+
63
+ ```bash
64
+ docker run -d -p 9009:9009 --name keep ghcr.io/clcrawford-dev/keep-server:latest
65
+ ```
66
+
67
+ ## Wire Format (v0.2.0+)
68
+
69
+ Every message on the wire is length-prefixed:
70
+
71
+ ```
72
+ [4 bytes: uint32 big-endian payload length][N bytes: protobuf Packet]
73
+ ```
74
+
75
+ Maximum payload size: 65,536 bytes.
76
+
77
+ **Breaking change from v0.1.x:** Raw protobuf writes are no longer accepted. All clients must use length-prefixed framing.
78
+
79
+ ### Python SDK Examples
80
+
81
+ **Install SDK:**
82
+
83
+ ```bash
84
+ pip install keep-protocol
85
+ ```
86
+
87
+ **Unsigned send (will be silently dropped):**
88
+
89
+ ```python
90
+ # Raw unsigned send using generated bindings (requires keep_pb2.py from protoc)
91
+ import socket, struct
92
+ from keep.keep_pb2 import Packet
93
+
94
+ p = Packet(typ=0, id="test-001", src="human:test", dst="server", body="hello claw")
95
+ wire_data = p.SerializeToString()
96
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
97
+ s.connect(("localhost", 9009))
98
+ s.sendall(struct.pack(">I", len(wire_data)) + wire_data)
99
+ # → timeout / silence (unsigned = dropped)
100
+ s.close()
101
+ ```
102
+
103
+ **Signed send (recommended — uses KeepClient):**
104
+
105
+ ```python
106
+ from keep import KeepClient
107
+
108
+ # Auto-generates keypair on first use
109
+ client = KeepClient("localhost", 9009)
110
+
111
+ reply = client.send(
112
+ body="ping from Python",
113
+ src="bot:python-test",
114
+ dst="server",
115
+ fee=1000 # optional anti-spam fee in sats
116
+ )
117
+
118
+ print(reply.body) # → "done"
119
+ ```
120
+
121
+ ## Agent-to-Agent Routing (v0.2.0+)
122
+
123
+ Agents register their identity by sending any signed packet — the server maps `src` to the connection. Other agents can then send packets to that identity via `dst`.
124
+
125
+ ```python
126
+ import threading
127
+ from keep import KeepClient
128
+
129
+ # Agent A: listen for messages
130
+ with KeepClient(src="bot:alice") as alice:
131
+ alice.send(body="register", dst="server", wait_reply=True)
132
+ alice.listen(lambda p: print(f"Got: {p.body}"), timeout=30)
133
+
134
+ # Agent B: send to Alice (in another thread/process)
135
+ with KeepClient(src="bot:bob") as bob:
136
+ bob.send(body="register", dst="server", wait_reply=True)
137
+ bob.send(body="hello alice!", dst="bot:alice")
138
+ ```
139
+
140
+ **Routing rules:**
141
+ - `dst="server"` or `dst=""` → server replies `"done"` (backward compatible)
142
+ - `dst="bot:alice"` → forwarded to Alice's connection with original signature intact
143
+ - Destination offline → sender gets `body: "error:offline"`
144
+ - Delivery failure → sender gets `body: "error:delivery_failed"`
145
+
146
+ See `examples/routing_basic.py` for a full working demo.
147
+
148
+ ## Discovery (v0.3.0+)
149
+
150
+ Agents can query the server for metadata and discover who's connected using `dst` conventions:
151
+
152
+ ```python
153
+ from keep import KeepClient
154
+
155
+ client = KeepClient("localhost", 9009)
156
+
157
+ # Server info: version, uptime, agent count
158
+ info = client.discover("info")
159
+ # → {"version": "0.3.0", "agents_online": 3, "uptime_sec": 1234}
160
+
161
+ # List connected agents
162
+ agents = client.discover_agents()
163
+ # → ["bot:alice", "bot:weather", "bot:planner"]
164
+
165
+ # Scar exchange stats
166
+ stats = client.discover("stats")
167
+ # → {"scar_exchanges": {"bot:alice": 5}, "total_packets": 42}
168
+ ```
169
+
170
+ **Discovery conventions:**
171
+ | `dst` value | Response |
172
+ |-------------|----------|
173
+ | `"discover:info"` | Server version, agent count, uptime |
174
+ | `"discover:agents"` | List of connected agent identities |
175
+ | `"discover:stats"` | Scar exchange counts, total packets |
176
+
177
+ **Endpoint caching:** The SDK can cache discovered endpoints in `~/.keep/endpoints.json` for reconnection:
178
+
179
+ ```python
180
+ # Cache after discovery
181
+ KeepClient.cache_endpoint("localhost", 9009, info)
182
+
183
+ # Reconnect from cache (tries each cached endpoint)
184
+ client = KeepClient.from_cache(src="bot:my-agent")
185
+ ```
186
+
187
+ See `examples/discovery_basic.py` for a full working demo.
188
+
189
+ ## Why Use It?
190
+
191
+ - **Local swarm:** Zero-latency handoff between agents on same machine.
192
+ - **Relay swarm:** Semantic routing via public/private relays (fee + ttl = spam control).
193
+ - **Memory barter:** `scar` field for sharing gitmem commits.
194
+ - **Identity without accounts:** Just a keypair — no registration.
195
+ - **No bloat:** Pure TCP + Protobuf, no HTTP/JSON overhead.
196
+
197
+ ## OpenClaw / Moltbot Integration
198
+
199
+ Prompt your agent:
200
+
201
+ ```text
202
+ Use keep-protocol to coordinate: send signed Packet to localhost:9009 body 'book sailing trip' src 'bot:me' dst 'swarm:sailing-planner' fee 1000 ttl 300
203
+ ```
204
+
205
+ **Repo:** https://github.com/CLCrawford-dev/keep-protocol
206
+ **Docker:** `ghcr.io/clcrawford-dev/keep-server:latest`
207
+
208
+ ---
209
+
210
+ **Active development happens here:** https://github.com/CLCrawford-dev/keep-protocol
211
+ Please open issues, PRs, and discussions on the original personal repo.
212
+ This nTEG-dev fork is a public mirror for visibility and ClawHub integration.
213
+
214
+ ---
215
+
216
+ 🦀 Keep it signed. Keep it simple. Claw to claw.
@@ -0,0 +1,7 @@
1
+ keep/__init__.py,sha256=x1tc3cuafZf6oOMwbtIX-9_qq4O4uyY_LNKhCxvVn8w,151
2
+ keep/client.py,sha256=B1lYu59ekZVubGyjXilozqis4Y3XlfFNUzWrMR6IO8A,11902
3
+ keep/keep_pb2.py,sha256=KQPLcH56ULH6qlEvhJLwmMBraqUb8hhMTWpmm9MvSO8,1298
4
+ keep_protocol-0.3.0.dist-info/METADATA,sha256=3mmQJ4kDNeIt8jtqbLOJp0GWwjhVWNnVMrEqjf8kRy4,7109
5
+ keep_protocol-0.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ keep_protocol-0.3.0.dist-info/top_level.txt,sha256=9mCnmW3qz7x1YOQkAFSorYLrAv4lqVBkJX4HCEvKy4U,5
7
+ keep_protocol-0.3.0.dist-info/RECORD,,
@@ -1,178 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: keep-protocol
3
- Version: 0.1.0
4
- Summary: Signed protobuf packets over TCP for AI agent-to-agent communication
5
- Author: Chris Crawford
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/CLCrawford-dev/keep-protocol
8
- Project-URL: Repository, https://github.com/CLCrawford-dev/keep-protocol
9
- Project-URL: Issues, https://github.com/CLCrawford-dev/keep-protocol/issues
10
- Keywords: agent-protocol,ai-agents,protobuf,ed25519,tcp,agent-communication,mcp,multi-agent
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: MIT License
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.9
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.13
20
- Classifier: Topic :: Communications
21
- Classifier: Topic :: Security :: Cryptography
22
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
- Requires-Python: >=3.9
24
- Description-Content-Type: text/markdown
25
- Requires-Dist: protobuf>=4.25
26
- Requires-Dist: cryptography>=41.0
27
-
28
- # keep-protocol
29
-
30
- **Signed protobuf packets over TCP for AI agent-to-agent communication.**
31
-
32
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
33
- [![Build](https://github.com/CLCrawford-dev/keep-protocol/actions/workflows/ci.yml/badge.svg)](https://github.com/CLCrawford-dev/keep-protocol/actions)
34
-
35
- > Keep is the quiet pipe agents whisper through.
36
- > A single TCP connection, a tiny Protobuf envelope, an ed25519 signature —
37
- > just enough fields to say who's talking, who should listen, what they want,
38
- > how much they'll pay, and when the message expires.
39
- > Unsigned packets vanish without a trace. Signed ones get heard.
40
-
41
- ---
42
-
43
- ## Install
44
-
45
- ### Python SDK
46
-
47
- ```bash
48
- pip install keep-protocol
49
- ```
50
-
51
- ```python
52
- from keep.client import KeepClient
53
-
54
- client = KeepClient("localhost", 9009)
55
- reply = client.send(
56
- src="bot:my-agent",
57
- dst="server",
58
- body="hello from my agent",
59
- )
60
- print(reply.body) # "done"
61
- ```
62
-
63
- ### Run the Server
64
-
65
- **Docker (recommended):**
66
- ```bash
67
- docker run -d -p 9009:9009 --name keep ghcr.io/clcrawford-dev/keep-server:latest
68
- ```
69
-
70
- **From source:**
71
- ```bash
72
- git clone https://github.com/CLCrawford-dev/keep-protocol.git
73
- cd keep-protocol
74
- go build -o keep .
75
- ./keep # listens on :9009
76
- ```
77
-
78
- ### Verify It Works
79
-
80
- ```bash
81
- pip install keep-protocol
82
- python -c "
83
- from keep.client import KeepClient
84
- reply = KeepClient('localhost', 9009).send(body='ping')
85
- print('OK' if reply.body == 'done' else 'FAIL')
86
- "
87
- ```
88
-
89
- ---
90
-
91
- ## Why Keep?
92
-
93
- | Feature | Keep | HTTP/REST | gRPC | NATS |
94
- |---------|------|-----------|------|------|
95
- | Latency | Sub-ms (TCP) | 1-10ms | 1-5ms | 1-5ms |
96
- | Auth | ed25519 built-in | Bring your own | mTLS | Tokens |
97
- | Schema | 10 fields, done | Unlimited | Unlimited | None |
98
- | Setup | 1 binary, 0 config | Web server + routes | Codegen + server | Broker cluster |
99
- | Agent-native | Yes | No | No | Partial |
100
- | Spam resistance | fee + ttl fields | None | None | None |
101
-
102
- Keep is not a replacement for gRPC or NATS. It is a protocol for agents that
103
- need to find each other and exchange signed intent with minimal ceremony.
104
-
105
- ---
106
-
107
- ## Packet Schema
108
-
109
- ```protobuf
110
- message Packet {
111
- bytes sig = 1; // ed25519 signature (64 bytes)
112
- bytes pk = 2; // sender's public key (32 bytes)
113
- uint32 typ = 3; // 0=ask, 1=offer, 2=heartbeat
114
- string id = 4; // unique message ID
115
- string src = 5; // sender: "bot:my-agent" or "human:chris"
116
- string dst = 6; // destination: "server", "nearest:weather", "swarm:planner"
117
- string body = 7; // intent or payload
118
- uint64 fee = 8; // micro-fee in sats (anti-spam)
119
- uint32 ttl = 9; // time-to-live in seconds
120
- bytes scar = 10; // gitmem-style memory commit (optional)
121
- }
122
- ```
123
-
124
- ## Signing Protocol
125
-
126
- Identity is a keypair. No accounts, no registration.
127
-
128
- ### Sending a signed packet
129
-
130
- 1. Build a `Packet` with all fields — leave `sig` and `pk` empty
131
- 2. Serialize to bytes — this is the **sign payload**
132
- 3. Sign those bytes with your ed25519 private key
133
- 4. Set `sig` (64 bytes) and `pk` (32 bytes) on the Packet
134
- 5. Serialize the full Packet — send over TCP
135
-
136
- ### Server verification
137
-
138
- 1. Unmarshal the incoming bytes into a `Packet`
139
- 2. If `sig` and `pk` are empty — **DROPPED** (logged, no reply)
140
- 3. Copy all fields except `sig`/`pk` into a new Packet, serialize it
141
- 4. Verify the signature against those bytes using the sender's `pk`
142
- 5. If invalid — **DROPPED** (logged, no reply)
143
- 6. If valid — process the message, send `done` reply
144
-
145
- ---
146
-
147
- ## Examples
148
-
149
- See the [`examples/`](examples/) directory:
150
-
151
- - **[python_basic.py](examples/python_basic.py)** — Minimal SDK usage
152
- - **[python_raw.py](examples/python_raw.py)** — Raw TCP + signing without the SDK (educational)
153
- - **[mcp_tool_definition.py](examples/mcp_tool_definition.py)** — Expose keep as an MCP tool
154
-
155
- ---
156
-
157
- ## Use Cases
158
-
159
- - **Local swarm** — agents on same VM use `localhost:9009` for zero-latency handoff
160
- - **Relay swarm** — agents publish to public relays — relays enforce fee/ttl/reputation
161
- - **Memory sharing** — `scar` field carries gitmem-style commits — agents barter knowledge
162
- - **Anti-spam market** — `fee` field creates micro-economy — pay to get priority
163
-
164
- ## Design Principles
165
-
166
- - **Silent rejection** — unsigned senders don't know if the server exists
167
- - **Identity without accounts** — your keypair is your identity
168
- - **Full visibility** — dropped packets are logged server-side
169
- - **Minimal overhead** — protobuf over raw TCP, no HTTP/JSON
170
- - **Semantic routing** — `dst` is a name, not an address
171
-
172
- ## Contributing
173
-
174
- We welcome contributions. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
175
-
176
- ## License
177
-
178
- MIT. See [LICENSE](LICENSE).
@@ -1,7 +0,0 @@
1
- keep/__init__.py,sha256=m5cf_KgFl9kpJ9sSdnRAkXvpHiJT03m5yMrhdr6df7s,151
2
- keep/client.py,sha256=BhuvazpKR5wKVvwPZxpIfWTJsC8cU3TE7GDhlfr3stc,2109
3
- keep/keep_pb2.py,sha256=KQPLcH56ULH6qlEvhJLwmMBraqUb8hhMTWpmm9MvSO8,1298
4
- keep_protocol-0.1.0.dist-info/METADATA,sha256=N-Z7krLRleKh994kQGYCjJLJ16Hm326GbBWG6OGHcFQ,5855
5
- keep_protocol-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
- keep_protocol-0.1.0.dist-info/top_level.txt,sha256=9mCnmW3qz7x1YOQkAFSorYLrAv4lqVBkJX4HCEvKy4U,5
7
- keep_protocol-0.1.0.dist-info/RECORD,,