pilotprotocol 1.7.2__tar.gz → 1.10.0__tar.gz

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.
Files changed (28) hide show
  1. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/MANIFEST.in +4 -1
  2. {pilotprotocol-1.7.2/pilotprotocol.egg-info → pilotprotocol-1.10.0}/PKG-INFO +5 -6
  3. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/README.md +2 -2
  4. pilotprotocol-1.10.0/pilotprotocol/_runtime.py +382 -0
  5. pilotprotocol-1.10.0/pilotprotocol/bin/.pilot-version +1 -0
  6. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol/bin/libpilot.h +30 -2
  7. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol/bin/libpilot.so +0 -0
  8. pilotprotocol-1.7.2/pilotprotocol/bin/pilotctl → pilotprotocol-1.10.0/pilotprotocol/bin/pilot-daemon +0 -0
  9. pilotprotocol-1.10.0/pilotprotocol/bin/pilot-gateway +0 -0
  10. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol/bin/pilot-updater +0 -0
  11. pilotprotocol-1.7.2/pilotprotocol/bin/pilot-daemon → pilotprotocol-1.10.0/pilotprotocol/bin/pilotctl +0 -0
  12. pilotprotocol-1.10.0/pilotprotocol/cli.py +55 -0
  13. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol/client.py +267 -75
  14. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0/pilotprotocol.egg-info}/PKG-INFO +5 -6
  15. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol.egg-info/SOURCES.txt +2 -0
  16. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pyproject.toml +5 -5
  17. pilotprotocol-1.7.2/pilotprotocol/bin/pilot-gateway +0 -0
  18. pilotprotocol-1.7.2/pilotprotocol/cli.py +0 -169
  19. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/CHANGELOG.md +0 -0
  20. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/LICENSE +0 -0
  21. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol/__init__.py +0 -0
  22. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol/py.typed +0 -0
  23. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol.egg-info/dependency_links.txt +0 -0
  24. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol.egg-info/entry_points.txt +0 -0
  25. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol.egg-info/requires.txt +0 -0
  26. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/pilotprotocol.egg-info/top_level.txt +0 -0
  27. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/setup.cfg +0 -0
  28. {pilotprotocol-1.7.2 → pilotprotocol-1.10.0}/setup.py +0 -0
@@ -3,8 +3,11 @@ include README.md
3
3
  include LICENSE
4
4
  include CHANGELOG.md
5
5
 
6
- # Include all binaries in bin/ directory
6
+ # Include all binaries in bin/ directory (the seed cache).
7
+ # Dotfiles like .pilot-version need an explicit pattern because some
8
+ # setuptools versions skip them under recursive-include.
7
9
  recursive-include pilotprotocol/bin *
10
+ include pilotprotocol/bin/.pilot-version
8
11
 
9
12
  # Include type stubs if any
10
13
  recursive-include pilotprotocol *.pyi
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pilotprotocol
3
- Version: 1.7.2
3
+ Version: 1.10.0
4
4
  Summary: Python SDK for Pilot Protocol - the network stack for AI agents
5
- Author-email: Alexandru Godoroja <alex@vulturelabs.com>
6
- Maintainer-email: Alexandru Godoroja <alex@vulturelabs.com>, Teodor Calin <teodor@vulturelabs.com>
5
+ Author-email: Alexandru Godoroja <alex@pilotprotocol.network>
6
+ Maintainer-email: Alexandru Godoroja <alex@pilotprotocol.network>, Teodor Calin <teodor@pilotprotocol.network>
7
7
  License: AGPL-3.0-or-later
8
8
  Project-URL: Homepage, https://pilotprotocol.network
9
9
  Project-URL: Documentation, https://pilotprotocol.network/docs/
10
10
  Project-URL: Repository, https://github.com/TeoSlayer/pilotprotocol
11
11
  Project-URL: Bug Tracker, https://github.com/TeoSlayer/pilotprotocol/issues
12
12
  Project-URL: Changelog, https://github.com/TeoSlayer/pilotprotocol/blob/main/sdk/python/CHANGELOG.md
13
- Project-URL: Live Dashboard, https://polo.pilotprotocol.network
14
13
  Keywords: pilot-protocol,networking,p2p,agent,ai,protocol,ctypes,udp,encryption
15
14
  Classifier: Development Status :: 4 - Beta
16
15
  Classifier: Intended Audience :: Developers
@@ -312,7 +311,7 @@ See `examples/python_sdk/` for comprehensive examples:
312
311
  - **`basic_usage.py`** — Connection, identity, trust management
313
312
  - **`data_exchange_demo.py`** — Send messages, files, JSON
314
313
  - **`event_stream_demo.py`** — Pub/sub patterns
315
- - **`task_submit_demo.py`** — Task delegation and polo score
314
+ - **`task_submit_demo.py`** — Task delegation
316
315
  - **`pydantic_ai_agent.py`** — PydanticAI integration with function tools
317
316
  - **`pydantic_ai_multiagent.py`** — Multi-agent collaboration system
318
317
 
@@ -349,7 +348,7 @@ make publish-test # Publish to TestPyPI
349
348
  - **Examples:** `examples/python_sdk/README.md`
350
349
  - **CLI Reference:** `examples/cli/BASIC_USAGE.md`
351
350
  - **Protocol Spec:** `docs/SPEC.md`
352
- - **Agent Skills:** `docs/SKILLS.md`
351
+ - **Agent Skills:** https://github.com/TeoSlayer/pilot-skills
353
352
 
354
353
  ## License
355
354
 
@@ -273,7 +273,7 @@ See `examples/python_sdk/` for comprehensive examples:
273
273
  - **`basic_usage.py`** — Connection, identity, trust management
274
274
  - **`data_exchange_demo.py`** — Send messages, files, JSON
275
275
  - **`event_stream_demo.py`** — Pub/sub patterns
276
- - **`task_submit_demo.py`** — Task delegation and polo score
276
+ - **`task_submit_demo.py`** — Task delegation
277
277
  - **`pydantic_ai_agent.py`** — PydanticAI integration with function tools
278
278
  - **`pydantic_ai_multiagent.py`** — Multi-agent collaboration system
279
279
 
@@ -310,7 +310,7 @@ make publish-test # Publish to TestPyPI
310
310
  - **Examples:** `examples/python_sdk/README.md`
311
311
  - **CLI Reference:** `examples/cli/BASIC_USAGE.md`
312
312
  - **Protocol Spec:** `docs/SPEC.md`
313
- - **Agent Skills:** `docs/SKILLS.md`
313
+ - **Agent Skills:** https://github.com/TeoSlayer/pilot-skills
314
314
 
315
315
  ## License
316
316
 
@@ -0,0 +1,382 @@
1
+ """Runtime environment seeder for the Pilot Protocol Python SDK.
2
+
3
+ Both the CLI shims (``cli.py``) and the FFI loader (``client._load_lib``)
4
+ funnel through :func:`ensure_runtime_seeded`, which idempotently mirrors
5
+ the binaries shipped inside the wheel into ``~/.pilot/bin/``.
6
+
7
+ Design goals:
8
+ - The wheel is the *seed cache*; ``~/.pilot/bin/`` is the canonical runtime.
9
+ - No install-time code runs; seeding happens lazily on first SDK use.
10
+ - Concurrency-safe (flock) and crash-safe (atomic rename).
11
+ - Never downgrades; never replaces a running daemon binary.
12
+ - Coexists with ``install.sh`` (same layout, same ``.pilot-version`` marker).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import errno
18
+ import json
19
+ import os
20
+ import platform
21
+ import shutil
22
+ import socket
23
+ import sys
24
+ import tempfile
25
+ import threading
26
+ from pathlib import Path
27
+ from typing import Optional
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Constants
31
+ # ---------------------------------------------------------------------------
32
+
33
+ _BIN_NAMES = ("pilotctl", "pilot-daemon", "pilot-gateway", "pilot-updater")
34
+ _LIB_NAMES = {
35
+ "Darwin": "libpilot.dylib",
36
+ "Linux": "libpilot.so",
37
+ "Windows": "libpilot.dll",
38
+ }
39
+
40
+ DEFAULT_REGISTRY = "34.71.57.205:9000"
41
+ DEFAULT_BEACON = "34.71.57.205:9001"
42
+ DEFAULT_SOCKET = "/tmp/pilot.sock"
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Path helpers
47
+ # ---------------------------------------------------------------------------
48
+
49
+ def _pkg_bin_dir() -> Path:
50
+ """Where the wheel ships its bundled binaries (the seed cache)."""
51
+ return Path(__file__).resolve().parent / "bin"
52
+
53
+
54
+ def _runtime_root() -> Path:
55
+ """Canonical runtime dir. Honours ``PILOT_HOME`` for CI / multi-tenant."""
56
+ override = os.environ.get("PILOT_HOME")
57
+ if override:
58
+ return Path(override).expanduser()
59
+ return Path.home() / ".pilot"
60
+
61
+
62
+ def _runtime_bin() -> Path:
63
+ return _runtime_root() / "bin"
64
+
65
+
66
+ def _platform_lib_name() -> str:
67
+ name = _LIB_NAMES.get(platform.system())
68
+ if name is None:
69
+ raise OSError(f"unsupported platform: {platform.system()}")
70
+ return name
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Version helpers
75
+ # ---------------------------------------------------------------------------
76
+
77
+ def _semver_tuple(v: str) -> tuple[int, ...]:
78
+ """Parse a SemVer-ish string into a comparable tuple. Unparseable → ()."""
79
+ s = (v or "").strip().lstrip("v").split("-", 1)[0].split("+", 1)[0]
80
+ if not s:
81
+ return ()
82
+ parts = []
83
+ for p in s.split("."):
84
+ try:
85
+ parts.append(int(p))
86
+ except ValueError:
87
+ return ()
88
+ return tuple(parts)
89
+
90
+
91
+ def _bundled_version() -> str:
92
+ """Version of the binaries bundled in this wheel."""
93
+ f = _pkg_bin_dir() / ".pilot-version"
94
+ if f.is_file():
95
+ try:
96
+ return f.read_text().strip()
97
+ except OSError:
98
+ pass
99
+ # Fall back to the package metadata if the marker file is missing.
100
+ try:
101
+ from importlib.metadata import version as _pkg_version
102
+ return _pkg_version("pilotprotocol")
103
+ except Exception:
104
+ return ""
105
+
106
+
107
+ def _runtime_version(rt: Path) -> str:
108
+ f = rt / ".pilot-version"
109
+ if f.is_file():
110
+ try:
111
+ return f.read_text().strip()
112
+ except OSError:
113
+ return ""
114
+ return ""
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Daemon liveness probe
119
+ # ---------------------------------------------------------------------------
120
+
121
+ def _daemon_running() -> bool:
122
+ """True if a pilot daemon is reachable on its IPC socket."""
123
+ sock_path = DEFAULT_SOCKET
124
+ try:
125
+ with open(_runtime_root() / "config.json") as f:
126
+ cfg = json.load(f)
127
+ sock_path = cfg.get("socket", sock_path) or sock_path
128
+ except (OSError, ValueError):
129
+ pass
130
+
131
+ if not Path(sock_path).exists():
132
+ return False
133
+ s = socket.socket(socket.AF_UNIX)
134
+ s.settimeout(0.2)
135
+ try:
136
+ s.connect(sock_path)
137
+ return True
138
+ except OSError:
139
+ return False
140
+ finally:
141
+ try:
142
+ s.close()
143
+ except OSError:
144
+ pass
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Atomic file ops
149
+ # ---------------------------------------------------------------------------
150
+
151
+ def _atomic_install(src: Path, dst: Path) -> None:
152
+ """Copy *src* → *dst* atomically, surviving in-flight execs.
153
+
154
+ Writes to ``<dst>.tmp.<pid>`` then ``os.replace()`` over the target.
155
+ On POSIX this unlinks the old inode while leaving any running process
156
+ that mapped it untouched.
157
+ """
158
+ tmp = dst.with_name(f"{dst.name}.tmp.{os.getpid()}.{threading.get_ident()}")
159
+ if tmp.exists():
160
+ tmp.unlink()
161
+ shutil.copy2(src, tmp)
162
+ try:
163
+ tmp.chmod(0o755)
164
+ os.replace(tmp, dst)
165
+ except OSError:
166
+ try:
167
+ tmp.unlink()
168
+ except OSError:
169
+ pass
170
+ raise
171
+
172
+
173
+ def _ensure_dir_writable(p: Path) -> None:
174
+ """Create *p* if it does not exist; raise a clear error if we cannot
175
+ write to it (e.g. owned by root after a botched install)."""
176
+ p.mkdir(parents=True, exist_ok=True)
177
+ if not os.access(p, os.W_OK):
178
+ raise PermissionError(
179
+ f"{p} is not writable by user {os.getuid()}. "
180
+ f"Repair with: chown -R $USER {p}"
181
+ )
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # Config seeding
186
+ # ---------------------------------------------------------------------------
187
+
188
+ def _ensure_default_config() -> Path:
189
+ """Make sure ``~/.pilot/config.json`` exists. Never overwrites an
190
+ existing one — install.sh or the user may have set an email.
191
+ """
192
+ root = _runtime_root()
193
+ _ensure_dir_writable(root)
194
+ cfg_path = root / "config.json"
195
+ if cfg_path.is_file():
196
+ return cfg_path
197
+ cfg = {
198
+ "registry": DEFAULT_REGISTRY,
199
+ "beacon": DEFAULT_BEACON,
200
+ "socket": DEFAULT_SOCKET,
201
+ "encrypt": True,
202
+ "identity": str(root / "identity.json"),
203
+ }
204
+ tmp = cfg_path.with_name(
205
+ f"config.json.tmp.{os.getpid()}.{threading.get_ident()}"
206
+ )
207
+ tmp.write_text(json.dumps(cfg, indent=2) + "\n")
208
+ try:
209
+ os.replace(tmp, cfg_path)
210
+ except FileNotFoundError:
211
+ # Another thread won the race; that's fine.
212
+ if tmp.exists():
213
+ try:
214
+ tmp.unlink()
215
+ except OSError:
216
+ pass
217
+ return cfg_path
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Public API
222
+ # ---------------------------------------------------------------------------
223
+
224
+ class SeedReport:
225
+ """Summary of what a seeder pass did. Useful for tests + diagnostics."""
226
+
227
+ def __init__(self) -> None:
228
+ self.copied: list[str] = []
229
+ self.skipped: list[str] = []
230
+ self.action: str = "noop" # one of: noop, seed, upgrade, daemon-skip
231
+ self.bundled_version: str = ""
232
+ self.installed_version: str = ""
233
+ self.runtime_dir: Path = _runtime_bin()
234
+
235
+
236
+ _SEEDED_ONCE = False
237
+
238
+
239
+ def ensure_runtime_seeded(force: bool = False) -> Path:
240
+ """Idempotently mirror bundled binaries into ``~/.pilot/bin/``.
241
+
242
+ Returns the runtime bin dir. Safe to call on every CLI invocation and
243
+ every Driver() construction; the steady state is a single stat() +
244
+ string compare.
245
+
246
+ Set ``force=True`` to re-run even if this process has already seeded.
247
+ """
248
+ global _SEEDED_ONCE
249
+ if _SEEDED_ONCE and not force:
250
+ return _runtime_bin()
251
+
252
+ report = run_seeder()
253
+ _SEEDED_ONCE = True
254
+ return report.runtime_dir
255
+
256
+
257
+ def run_seeder() -> SeedReport:
258
+ """Run one seeder pass and return a structured report."""
259
+ report = SeedReport()
260
+ rt_root = _runtime_root()
261
+ rt = _runtime_bin()
262
+ pkg = _pkg_bin_dir()
263
+
264
+ # Make sure ~/.pilot/ exists and is writable.
265
+ _ensure_dir_writable(rt_root)
266
+ _ensure_dir_writable(rt)
267
+ _ensure_default_config()
268
+
269
+ # Cross-platform fcntl shim. flock is POSIX-only; on Windows we use
270
+ # msvcrt.locking. Tests run on POSIX so the Windows path is best-effort.
271
+ lock_path = rt / ".seed.lock"
272
+ lock_path.touch(exist_ok=True)
273
+ lock_fd = os.open(lock_path, os.O_RDWR)
274
+ try:
275
+ if os.name == "posix":
276
+ import fcntl
277
+ fcntl.flock(lock_fd, fcntl.LOCK_EX)
278
+ else: # pragma: no cover - Windows
279
+ import msvcrt
280
+ msvcrt.locking(lock_fd, msvcrt.LK_LOCK, 1)
281
+
282
+ bundled_str = _bundled_version()
283
+ installed_str = _runtime_version(rt)
284
+ report.bundled_version = bundled_str
285
+ report.installed_version = installed_str
286
+
287
+ bundled = _semver_tuple(bundled_str)
288
+ installed = _semver_tuple(installed_str)
289
+
290
+ # Decide overall action.
291
+ force = os.environ.get("PILOT_FORCE_SEED") == "1"
292
+ if not force and installed and bundled and bundled <= installed:
293
+ # Same or newer already installed. Still verify each file exists.
294
+ need_seed = False
295
+ for name in _BIN_NAMES + (_platform_lib_name(),):
296
+ if not (rt / name).is_file():
297
+ need_seed = True
298
+ break
299
+ if not need_seed:
300
+ report.action = "noop"
301
+ return report
302
+
303
+ report.action = "upgrade" if installed else "seed"
304
+ daemon_busy = _daemon_running()
305
+
306
+ for name in _BIN_NAMES + (_platform_lib_name(),):
307
+ src = pkg / name
308
+ if not src.is_file():
309
+ # Wrong-platform wheel or partial bundle. Skip — caller will
310
+ # surface a clear error when the missing binary is needed.
311
+ continue
312
+ dst = rt / name
313
+ if name == "pilot-daemon" and daemon_busy and dst.is_file():
314
+ report.skipped.append(name)
315
+ report.action = "daemon-skip"
316
+ continue
317
+ try:
318
+ _atomic_install(src, dst)
319
+ report.copied.append(name)
320
+ except OSError as e:
321
+ # ETXTBSY can hit Linux despite atomic rename if a tool has
322
+ # the file mmap'd. Skip with a notice; caller can retry.
323
+ if e.errno in (errno.ETXTBSY, errno.EBUSY):
324
+ report.skipped.append(name)
325
+ continue
326
+ raise
327
+
328
+ # Update the marker last; a partial seed leaves the old marker.
329
+ if bundled_str:
330
+ ver_path = rt / ".pilot-version"
331
+ tmp = ver_path.with_name(f".pilot-version.tmp.{os.getpid()}")
332
+ tmp.write_text(bundled_str + "\n")
333
+ os.replace(tmp, ver_path)
334
+
335
+ return report
336
+ finally:
337
+ try:
338
+ if os.name == "posix":
339
+ import fcntl
340
+ fcntl.flock(lock_fd, fcntl.LOCK_UN)
341
+ finally:
342
+ os.close(lock_fd)
343
+
344
+
345
+ def runtime_binary(name: str) -> Path:
346
+ """Resolve a binary by name, seeding if needed.
347
+
348
+ Use this from CLI shims; it returns the path to exec.
349
+ """
350
+ rt = ensure_runtime_seeded()
351
+ p = rt / name
352
+ if not p.is_file():
353
+ # Last-ditch fallback: run from the wheel itself.
354
+ fallback = _pkg_bin_dir() / name
355
+ if fallback.is_file():
356
+ return fallback
357
+ raise FileNotFoundError(
358
+ f"Binary {name!r} not found in {rt} or {_pkg_bin_dir()}. "
359
+ f"This wheel may be for a different platform."
360
+ )
361
+ return p
362
+
363
+
364
+ def runtime_library() -> Path:
365
+ """Resolve libpilot.{so,dylib,dll}, seeding if needed."""
366
+ rt = ensure_runtime_seeded()
367
+ name = _platform_lib_name()
368
+ p = rt / name
369
+ if p.is_file():
370
+ return p
371
+ fallback = _pkg_bin_dir() / name
372
+ if fallback.is_file():
373
+ return fallback
374
+ raise FileNotFoundError(
375
+ f"libpilot ({name}) not found in {rt} or {_pkg_bin_dir()}."
376
+ )
377
+
378
+
379
+ def reset_seeded_marker() -> None:
380
+ """Test helper: forget that this process has already seeded."""
381
+ global _SEEDED_ONCE
382
+ _SEEDED_ONCE = False
@@ -21,7 +21,7 @@ extern const char *_GoStringPtr(_GoString_ s);
21
21
  /* Start of preamble from import "C" comments. */
22
22
 
23
23
 
24
- #line 3 "bindings.go"
24
+ #line 5 "bindings.go"
25
25
 
26
26
  #include <stdlib.h>
27
27
  #include <stdint.h>
@@ -107,7 +107,6 @@ extern char* PilotRevokeTrust(uint64_t h, uint32_t nodeID);
107
107
  extern char* PilotResolveHostname(uint64_t h, char* hostname);
108
108
  extern char* PilotSetHostname(uint64_t h, char* hostname);
109
109
  extern char* PilotSetVisibility(uint64_t h, int public);
110
- extern char* PilotSetTaskExec(uint64_t h, int enabled);
111
110
  extern char* PilotDeregister(uint64_t h);
112
111
  extern char* PilotSetTags(uint64_t h, char* tagsJSON);
113
112
  extern char* PilotSetWebhook(uint64_t h, char* url);
@@ -152,6 +151,35 @@ extern struct PilotConnWrite_return PilotConnWrite(uint64_t ch, void* data, int
152
151
  extern char* PilotConnClose(uint64_t ch);
153
152
  extern char* PilotSendTo(uint64_t h, char* fullAddr, void* data, int dataLen);
154
153
  extern char* PilotRecvFrom(uint64_t h);
154
+ extern char* PilotHealth(uint64_t h);
155
+ extern char* PilotRotateKey(uint64_t h);
156
+ extern char* PilotBroadcast(uint64_t h, uint16_t netID, uint16_t port, void* data, int dataLen, char* adminToken);
157
+
158
+ /* Return type for PilotDialTimeout */
159
+ struct PilotDialTimeout_return {
160
+ uint64_t r0;
161
+ char* r1;
162
+ };
163
+ extern struct PilotDialTimeout_return PilotDialTimeout(uint64_t h, char* addr, uint64_t timeoutMs);
164
+
165
+ // PilotConnSetReadDeadline sets the read deadline as Unix nanoseconds.
166
+ // Pass 0 to clear the deadline.
167
+ //
168
+ extern char* PilotConnSetReadDeadline(uint64_t ch, int64_t deadlineUnixNanos);
169
+ extern char* PilotNetworkList(uint64_t h);
170
+ extern char* PilotNetworkJoin(uint64_t h, uint16_t networkID, char* token);
171
+ extern char* PilotNetworkLeave(uint64_t h, uint16_t networkID);
172
+ extern char* PilotNetworkMembers(uint64_t h, uint16_t networkID);
173
+ extern char* PilotNetworkInvite(uint64_t h, uint16_t networkID, uint32_t targetNodeID);
174
+ extern char* PilotNetworkPollInvites(uint64_t h);
175
+ extern char* PilotNetworkRespondInvite(uint64_t h, uint16_t networkID, int accept);
176
+ extern char* PilotManagedStatus(uint64_t h, uint16_t networkID);
177
+ extern char* PilotManagedForceCycle(uint64_t h, uint16_t networkID);
178
+ extern char* PilotManagedReconcile(uint64_t h, uint16_t networkID);
179
+ extern char* PilotPolicyGet(uint64_t h, uint16_t networkID);
180
+ extern char* PilotPolicySet(uint64_t h, uint16_t networkID, char* policyJSON);
181
+ extern char* PilotMemberTagsGet(uint64_t h, uint16_t networkID, uint32_t nodeID);
182
+ extern char* PilotMemberTagsSet(uint64_t h, uint16_t networkID, uint32_t nodeID, char* tagsJSON);
155
183
 
156
184
  #ifdef __cplusplus
157
185
  }
@@ -0,0 +1,55 @@
1
+ """Command-line entry points for the Pilot Protocol CLI binaries.
2
+
3
+ The wheel ships pre-built Go binaries inside ``pilotprotocol/bin/``. On
4
+ first call, :mod:`pilotprotocol._runtime` mirrors those into
5
+ ``~/.pilot/bin/`` (the canonical runtime directory shared with
6
+ ``install.sh``) and these wrappers exec the seeded copy.
7
+
8
+ This means:
9
+ - pip-installed and curl-installed users converge on the same daemon.
10
+ - Multiple venvs, multiple SDK versions: highest version wins, no
11
+ parallel binary trees.
12
+ - Uninstalling the wheel never deletes ``~/.pilot/`` (identity, config,
13
+ daemon state are preserved).
14
+ """
15
+
16
+ import subprocess
17
+ import sys
18
+
19
+ from ._runtime import ensure_runtime_seeded, runtime_binary
20
+
21
+
22
+ def _exec_runtime_binary(name: str) -> None:
23
+ """Seed ``~/.pilot/bin/`` if needed, then exec the named binary."""
24
+ ensure_runtime_seeded()
25
+ binary = runtime_binary(name)
26
+ sys.exit(subprocess.call([str(binary)] + sys.argv[1:]))
27
+
28
+
29
+ def run_pilotctl() -> None:
30
+ """Entry point for the ``pilotctl`` console script."""
31
+ _exec_runtime_binary("pilotctl")
32
+
33
+
34
+ def run_daemon() -> None:
35
+ """Entry point for the ``pilot-daemon`` console script.
36
+
37
+ Note: the daemon needs an email address (passed via ``--email`` or
38
+ set in ``~/.pilot/config.json``) to register at the registry. The
39
+ SDK does not auto-prompt for one — call::
40
+
41
+ pilotctl daemon start --email you@example.com
42
+
43
+ on first launch, after which the email is cached in ``config.json``.
44
+ """
45
+ _exec_runtime_binary("pilot-daemon")
46
+
47
+
48
+ def run_gateway() -> None:
49
+ """Entry point for the ``pilot-gateway`` console script."""
50
+ _exec_runtime_binary("pilot-gateway")
51
+
52
+
53
+ def run_updater() -> None:
54
+ """Entry point for the ``pilot-updater`` console script."""
55
+ _exec_runtime_binary("pilot-updater")
@@ -101,8 +101,24 @@ def _find_library() -> str:
101
101
 
102
102
 
103
103
  def _load_lib() -> ctypes.CDLL: # pragma: no cover
104
- path = _find_library()
105
- return ctypes.CDLL(path)
104
+ """Load libpilot.
105
+
106
+ Order:
107
+ 1. ``PILOT_LIB_PATH`` (explicit override) — bypasses the seeder.
108
+ 2. The seeded library at ``~/.pilot/bin/`` (canonical runtime).
109
+ 3. Legacy fallback via :func:`_find_library` (system search etc.).
110
+ """
111
+ env = os.environ.get("PILOT_LIB_PATH")
112
+ if env:
113
+ return ctypes.CDLL(_find_library())
114
+
115
+ try:
116
+ from ._runtime import runtime_library
117
+ return ctypes.CDLL(str(runtime_library()))
118
+ except Exception:
119
+ # Seeder failed (read-only home, etc.) — fall back to legacy lookup
120
+ # so the SDK still loads from the wheel-bundled location.
121
+ return ctypes.CDLL(_find_library())
106
122
 
107
123
 
108
124
  _lib: Optional[ctypes.CDLL] = None
@@ -168,8 +184,10 @@ def _setup_signatures(lib: ctypes.CDLL) -> None: # pragma: no cover
168
184
 
169
185
  # JSON-RPC (single *C.char return → c_void_p)
170
186
  for name in (
171
- "PilotInfo", "PilotPendingHandshakes", "PilotTrustedPeers",
187
+ "PilotInfo", "PilotHealth", "PilotRotateKey",
188
+ "PilotPendingHandshakes", "PilotTrustedPeers",
172
189
  "PilotDeregister", "PilotRecvFrom",
190
+ "PilotNetworkList", "PilotNetworkPollInvites",
173
191
  ):
174
192
  fn = getattr(lib, name)
175
193
  fn.argtypes = [ctypes.c_uint64]
@@ -189,7 +207,7 @@ def _setup_signatures(lib: ctypes.CDLL) -> None: # pragma: no cover
189
207
  fn.restype = ctypes.c_void_p
190
208
 
191
209
  # (handle, int) -> *char
192
- for name in ("PilotSetVisibility", "PilotSetTaskExec"):
210
+ for name in ("PilotSetVisibility",):
193
211
  fn = getattr(lib, name)
194
212
  fn.argtypes = [ctypes.c_uint64, ctypes.c_int]
195
213
  fn.restype = ctypes.c_void_p
@@ -209,6 +227,9 @@ def _setup_signatures(lib: ctypes.CDLL) -> None: # pragma: no cover
209
227
  lib.PilotDial.argtypes = [ctypes.c_uint64, ctypes.c_char_p]
210
228
  lib.PilotDial.restype = _HandleErr
211
229
 
230
+ lib.PilotDialTimeout.argtypes = [ctypes.c_uint64, ctypes.c_char_p, ctypes.c_uint64]
231
+ lib.PilotDialTimeout.restype = _HandleErr
232
+
212
233
  # Listen: (handle, uint16) -> struct{handle, err}
213
234
  lib.PilotListen.argtypes = [ctypes.c_uint64, ctypes.c_uint16]
214
235
  lib.PilotListen.restype = _HandleErr
@@ -220,7 +241,7 @@ def _setup_signatures(lib: ctypes.CDLL) -> None: # pragma: no cover
220
241
  lib.PilotListenerClose.argtypes = [ctypes.c_uint64]
221
242
  lib.PilotListenerClose.restype = ctypes.c_void_p
222
243
 
223
- # Conn Read / Write / Close
244
+ # Conn Read / Write / Close / SetReadDeadline
224
245
  lib.PilotConnRead.argtypes = [ctypes.c_uint64, ctypes.c_int]
225
246
  lib.PilotConnRead.restype = _ReadResult
226
247
 
@@ -230,10 +251,69 @@ def _setup_signatures(lib: ctypes.CDLL) -> None: # pragma: no cover
230
251
  lib.PilotConnClose.argtypes = [ctypes.c_uint64]
231
252
  lib.PilotConnClose.restype = ctypes.c_void_p
232
253
 
254
+ lib.PilotConnSetReadDeadline.argtypes = [ctypes.c_uint64, ctypes.c_int64]
255
+ lib.PilotConnSetReadDeadline.restype = ctypes.c_void_p
256
+
233
257
  # SendTo: (handle, string, void*, int) -> *char
234
258
  lib.PilotSendTo.argtypes = [ctypes.c_uint64, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_int]
235
259
  lib.PilotSendTo.restype = ctypes.c_void_p
236
260
 
261
+ # Broadcast: (handle, uint16 net, uint16 port, void* data, int len, *char token) -> *char
262
+ lib.PilotBroadcast.argtypes = [
263
+ ctypes.c_uint64, ctypes.c_uint16, ctypes.c_uint16,
264
+ ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p,
265
+ ]
266
+ lib.PilotBroadcast.restype = ctypes.c_void_p
267
+
268
+ # Networks (handle, uint16) -> *char
269
+ for name in ("PilotNetworkLeave", "PilotNetworkMembers"):
270
+ fn = getattr(lib, name)
271
+ fn.argtypes = [ctypes.c_uint64, ctypes.c_uint16]
272
+ fn.restype = ctypes.c_void_p
273
+
274
+ # PilotNetworkJoin: (handle, uint16, *char token) -> *char
275
+ lib.PilotNetworkJoin.argtypes = [ctypes.c_uint64, ctypes.c_uint16, ctypes.c_char_p]
276
+ lib.PilotNetworkJoin.restype = ctypes.c_void_p
277
+
278
+ # PilotNetworkInvite: (handle, uint16, uint32) -> *char
279
+ lib.PilotNetworkInvite.argtypes = [ctypes.c_uint64, ctypes.c_uint16, ctypes.c_uint32]
280
+ lib.PilotNetworkInvite.restype = ctypes.c_void_p
281
+
282
+ # PilotNetworkRespondInvite: (handle, uint16, int) -> *char
283
+ lib.PilotNetworkRespondInvite.argtypes = [ctypes.c_uint64, ctypes.c_uint16, ctypes.c_int]
284
+ lib.PilotNetworkRespondInvite.restype = ctypes.c_void_p
285
+
286
+ # Managed (handle, uint16) -> *char
287
+ for name in (
288
+ "PilotManagedStatus", "PilotManagedRankings",
289
+ "PilotManagedForceCycle", "PilotManagedReconcile",
290
+ "PilotPolicyGet",
291
+ ):
292
+ fn = getattr(lib, name)
293
+ fn.argtypes = [ctypes.c_uint64, ctypes.c_uint16]
294
+ fn.restype = ctypes.c_void_p
295
+
296
+ # PilotManagedScore: (handle, uint16 net, uint32 node, int32 delta, *char topic)
297
+ lib.PilotManagedScore.argtypes = [
298
+ ctypes.c_uint64, ctypes.c_uint16, ctypes.c_uint32,
299
+ ctypes.c_int32, ctypes.c_char_p,
300
+ ]
301
+ lib.PilotManagedScore.restype = ctypes.c_void_p
302
+
303
+ # PilotPolicySet: (handle, uint16, *char json)
304
+ lib.PilotPolicySet.argtypes = [ctypes.c_uint64, ctypes.c_uint16, ctypes.c_char_p]
305
+ lib.PilotPolicySet.restype = ctypes.c_void_p
306
+
307
+ # PilotMemberTagsGet: (handle, uint16 net, uint32 node) -> *char
308
+ lib.PilotMemberTagsGet.argtypes = [ctypes.c_uint64, ctypes.c_uint16, ctypes.c_uint32]
309
+ lib.PilotMemberTagsGet.restype = ctypes.c_void_p
310
+
311
+ # PilotMemberTagsSet: (handle, uint16 net, uint32 node, *char tagsJson) -> *char
312
+ lib.PilotMemberTagsSet.argtypes = [
313
+ ctypes.c_uint64, ctypes.c_uint16, ctypes.c_uint32, ctypes.c_char_p,
314
+ ]
315
+ lib.PilotMemberTagsSet.restype = ctypes.c_void_p
316
+
237
317
 
238
318
  # ---------------------------------------------------------------------------
239
319
  # Error helpers
@@ -351,6 +431,23 @@ class Conn:
351
431
  if "error" in obj:
352
432
  raise PilotError(obj["error"])
353
433
 
434
+ def set_read_deadline(self, deadline: Optional[float]) -> None:
435
+ """Set the read deadline.
436
+
437
+ ``deadline`` is a Unix timestamp in seconds (e.g. ``time.time() + 5``)
438
+ or ``None`` to clear. After the deadline passes, ``read()`` returns
439
+ a ``PilotError`` with a "deadline exceeded" message.
440
+ """
441
+ if self._closed:
442
+ raise PilotError("connection closed")
443
+ if deadline is None:
444
+ nanos = 0
445
+ else:
446
+ nanos = int(deadline * 1_000_000_000)
447
+ lib = _get_lib()
448
+ ptr = lib.PilotConnSetReadDeadline(self._h, ctypes.c_int64(nanos))
449
+ _check_err(ptr)
450
+
354
451
  def __enter__(self) -> "Conn":
355
452
  return self
356
453
 
@@ -472,6 +569,14 @@ class Driver:
472
569
  """Return the daemon's status information."""
473
570
  return self._call_json("PilotInfo")
474
571
 
572
+ def health(self) -> dict[str, Any]:
573
+ """Lightweight health check from the daemon."""
574
+ return self._call_json("PilotHealth")
575
+
576
+ def rotate_key(self) -> dict[str, Any]:
577
+ """Rotate the daemon's Ed25519 identity at the registry."""
578
+ return self._call_json("PilotRotateKey")
579
+
475
580
  # -- Handshake / Trust --
476
581
 
477
582
  def handshake(self, node_id: int, justification: str = "") -> dict[str, Any]:
@@ -514,10 +619,6 @@ class Driver:
514
619
  """Set the daemon's visibility on the registry."""
515
620
  return self._call_json("PilotSetVisibility", ctypes.c_int(1 if public else 0))
516
621
 
517
- def set_task_exec(self, enabled: bool) -> dict[str, Any]:
518
- """Enable or disable task execution capability."""
519
- return self._call_json("PilotSetTaskExec", ctypes.c_int(1 if enabled else 0))
520
-
521
622
  def deregister(self) -> dict[str, Any]:
522
623
  """Remove the daemon from the registry."""
523
624
  return self._call_json("PilotDeregister")
@@ -540,10 +641,18 @@ class Driver:
540
641
 
541
642
  # -- Streams --
542
643
 
543
- def dial(self, addr: str) -> Conn:
544
- """Open a stream connection to addr (format: "N:XXXX.YYYY.YYYY:PORT")."""
644
+ def dial(self, addr: str, timeout: Optional[float] = None) -> Conn:
645
+ """Open a stream connection to addr (format: "N:XXXX.YYYY.YYYY:PORT").
646
+
647
+ If ``timeout`` is given (seconds), the dial is cancelled if the daemon
648
+ does not respond within that window.
649
+ """
545
650
  lib = _get_lib()
546
- res = lib.PilotDial(self._h, addr.encode())
651
+ if timeout is None:
652
+ res = lib.PilotDial(self._h, addr.encode())
653
+ else:
654
+ ms = max(0, int(timeout * 1000))
655
+ res = lib.PilotDialTimeout(self._h, addr.encode(), ctypes.c_uint64(ms))
547
656
  if res.err:
548
657
  raw = ctypes.string_at(res.err)
549
658
  lib.FreeString(res.err)
@@ -576,6 +685,152 @@ class Driver:
576
685
  """
577
686
  return self._call_json("PilotRecvFrom")
578
687
 
688
+ def broadcast(
689
+ self,
690
+ network_id: int,
691
+ port: int,
692
+ data: bytes,
693
+ admin_token: str,
694
+ ) -> None:
695
+ """Broadcast an unreliable datagram to every member of a network.
696
+
697
+ Requires the daemon's admin token; an empty or mismatched token is
698
+ rejected. Permitted on every network including network 0 (backbone).
699
+ """
700
+ lib = _get_lib()
701
+ buf = ctypes.create_string_buffer(data)
702
+ ptr = lib.PilotBroadcast(
703
+ self._h,
704
+ ctypes.c_uint16(network_id),
705
+ ctypes.c_uint16(port),
706
+ buf,
707
+ ctypes.c_int(len(data)),
708
+ admin_token.encode(),
709
+ )
710
+ _check_err(ptr)
711
+
712
+ # -- Networks --
713
+
714
+ def network_list(self) -> dict[str, Any]:
715
+ """List all networks known to the registry."""
716
+ return self._call_json("PilotNetworkList")
717
+
718
+ def network_join(self, network_id: int, token: str = "") -> dict[str, Any]:
719
+ """Join a network by ID, optionally with a token for token-gated networks."""
720
+ return self._call_json(
721
+ "PilotNetworkJoin", ctypes.c_uint16(network_id), token.encode()
722
+ )
723
+
724
+ def network_leave(self, network_id: int) -> dict[str, Any]:
725
+ """Leave a network by ID."""
726
+ return self._call_json("PilotNetworkLeave", ctypes.c_uint16(network_id))
727
+
728
+ def network_members(self, network_id: int) -> dict[str, Any]:
729
+ """List all members of a network."""
730
+ return self._call_json("PilotNetworkMembers", ctypes.c_uint16(network_id))
731
+
732
+ def network_invite(self, network_id: int, target_node_id: int) -> dict[str, Any]:
733
+ """Invite a target node to a network (requires admin token on daemon)."""
734
+ return self._call_json(
735
+ "PilotNetworkInvite",
736
+ ctypes.c_uint16(network_id),
737
+ ctypes.c_uint32(target_node_id),
738
+ )
739
+
740
+ def network_poll_invites(self) -> dict[str, Any]:
741
+ """Return pending network invites for this node."""
742
+ return self._call_json("PilotNetworkPollInvites")
743
+
744
+ def network_respond_invite(self, network_id: int, accept: bool) -> dict[str, Any]:
745
+ """Accept or reject a pending network invite."""
746
+ return self._call_json(
747
+ "PilotNetworkRespondInvite",
748
+ ctypes.c_uint16(network_id),
749
+ ctypes.c_int(1 if accept else 0),
750
+ )
751
+
752
+ # -- Managed networks --
753
+
754
+ def managed_score(
755
+ self,
756
+ network_id: int,
757
+ node_id: int,
758
+ delta: int,
759
+ topic: str = "",
760
+ ) -> dict[str, Any]:
761
+ """Adjust a peer's score in a managed network."""
762
+ return self._call_json(
763
+ "PilotManagedScore",
764
+ ctypes.c_uint16(network_id),
765
+ ctypes.c_uint32(node_id),
766
+ ctypes.c_int32(delta),
767
+ topic.encode(),
768
+ )
769
+
770
+ def managed_status(self, network_id: int) -> dict[str, Any]:
771
+ """Return the status of a managed network engine."""
772
+ return self._call_json("PilotManagedStatus", ctypes.c_uint16(network_id))
773
+
774
+ def managed_rankings(self, network_id: int) -> dict[str, Any]:
775
+ """Return ranked peers in a managed network."""
776
+ return self._call_json("PilotManagedRankings", ctypes.c_uint16(network_id))
777
+
778
+ def managed_force_cycle(self, network_id: int) -> dict[str, Any]:
779
+ """Force a prune/fill cycle in a managed network."""
780
+ return self._call_json("PilotManagedForceCycle", ctypes.c_uint16(network_id))
781
+
782
+ def managed_reconcile(self, network_id: int) -> dict[str, Any]:
783
+ """Refresh the managed network's peer set without running a policy cycle."""
784
+ return self._call_json("PilotManagedReconcile", ctypes.c_uint16(network_id))
785
+
786
+ # -- Policy --
787
+
788
+ def policy_get(self, network_id: int) -> dict[str, Any]:
789
+ """Retrieve the active policy for a network."""
790
+ return self._call_json("PilotPolicyGet", ctypes.c_uint16(network_id))
791
+
792
+ def policy_set(self, network_id: int, policy: Any) -> dict[str, Any]:
793
+ """Apply a policy document to a network.
794
+
795
+ ``policy`` may be a dict, a JSON string, or pre-encoded bytes.
796
+ """
797
+ if isinstance(policy, (bytes, bytearray)):
798
+ payload = bytes(policy)
799
+ elif isinstance(policy, str):
800
+ payload = policy.encode()
801
+ else:
802
+ payload = json.dumps(policy).encode()
803
+ return self._call_json(
804
+ "PilotPolicySet", ctypes.c_uint16(network_id), payload
805
+ )
806
+
807
+ # -- Member tags --
808
+
809
+ def member_tags_get(self, network_id: int, node_id: int) -> dict[str, Any]:
810
+ """Retrieve admin-assigned member tags for a node in a network."""
811
+ return self._call_json(
812
+ "PilotMemberTagsGet",
813
+ ctypes.c_uint16(network_id),
814
+ ctypes.c_uint32(node_id),
815
+ )
816
+
817
+ def member_tags_set(
818
+ self, network_id: int, node_id: int, tags: list[str]
819
+ ) -> dict[str, Any]:
820
+ """Set admin-assigned member tags for a node in a network."""
821
+ return self._call_json(
822
+ "PilotMemberTagsSet",
823
+ ctypes.c_uint16(network_id),
824
+ ctypes.c_uint32(node_id),
825
+ json.dumps(tags).encode(),
826
+ )
827
+
828
+ # -- Identity --
829
+
830
+ def rotate_identity(self) -> dict[str, Any]:
831
+ """Alias for :meth:`rotate_key`."""
832
+ return self.rotate_key()
833
+
579
834
  # -- High-level service methods --
580
835
 
581
836
  def send_message(self, target: str, data: bytes, msg_type: str = "text") -> dict[str, Any]:
@@ -809,66 +1064,3 @@ class Driver:
809
1064
  finally:
810
1065
  conn.close()
811
1066
 
812
- def submit_task(self, target: str, task_data: dict[str, Any]) -> dict[str, Any]:
813
- """Submit a task via the task submit service (port 1003).
814
-
815
- Args:
816
- target: Hostname or protocol address of task execution server
817
- task_data: Task specification as dict. Must include 'task_description'.
818
- Optional: 'task_id' (auto-generated if not provided)
819
-
820
- Returns:
821
- Response from task submit service (StatusAccepted=200 or StatusRejected=400)
822
- """
823
- import struct
824
- import uuid
825
-
826
- # Resolve hostname if needed
827
- if not target.startswith("0:"):
828
- result = self.resolve_hostname(target)
829
- addr = result.get("address", "")
830
- if not addr:
831
- raise PilotError(f"Could not resolve hostname: {target}")
832
- else:
833
- addr = target
834
-
835
- # Get local address
836
- info = self.info()
837
- from_addr = info.get("address", "unknown")
838
-
839
- # Build proper SubmitRequest
840
- submit_req = {
841
- "task_id": task_data.get("task_id", str(uuid.uuid4())),
842
- "task_description": task_data.get("task_description", json.dumps(task_data)),
843
- "from_addr": from_addr,
844
- "to_addr": addr
845
- }
846
-
847
- # Encode task request as JSON
848
- task_json = json.dumps(submit_req).encode('utf-8')
849
-
850
- # Build submit frame: [4-byte type=1][4-byte length][JSON payload]
851
- frame = struct.pack('>II', 1, len(task_json)) + task_json
852
-
853
- # Connect to task submit service (port 1003)
854
- with self.dial(f"{addr}:1003") as conn:
855
- # Send submit frame
856
- conn.write(frame)
857
-
858
- # Read response frame: [4-byte type][4-byte length][JSON payload]
859
- header = conn.read(8)
860
- if not header or len(header) < 8:
861
- raise PilotError("No response from task submit service")
862
-
863
- resp_type, resp_len = struct.unpack('>II', header)
864
- response_data = conn.read(resp_len)
865
-
866
- if not response_data or len(response_data) < resp_len:
867
- raise PilotError("Incomplete response from task submit service")
868
-
869
- # Parse JSON response
870
- try:
871
- resp = json.loads(response_data.decode('utf-8'))
872
- return resp
873
- except (json.JSONDecodeError, UnicodeDecodeError) as e:
874
- raise PilotError(f"Invalid response format: {e}")
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pilotprotocol
3
- Version: 1.7.2
3
+ Version: 1.10.0
4
4
  Summary: Python SDK for Pilot Protocol - the network stack for AI agents
5
- Author-email: Alexandru Godoroja <alex@vulturelabs.com>
6
- Maintainer-email: Alexandru Godoroja <alex@vulturelabs.com>, Teodor Calin <teodor@vulturelabs.com>
5
+ Author-email: Alexandru Godoroja <alex@pilotprotocol.network>
6
+ Maintainer-email: Alexandru Godoroja <alex@pilotprotocol.network>, Teodor Calin <teodor@pilotprotocol.network>
7
7
  License: AGPL-3.0-or-later
8
8
  Project-URL: Homepage, https://pilotprotocol.network
9
9
  Project-URL: Documentation, https://pilotprotocol.network/docs/
10
10
  Project-URL: Repository, https://github.com/TeoSlayer/pilotprotocol
11
11
  Project-URL: Bug Tracker, https://github.com/TeoSlayer/pilotprotocol/issues
12
12
  Project-URL: Changelog, https://github.com/TeoSlayer/pilotprotocol/blob/main/sdk/python/CHANGELOG.md
13
- Project-URL: Live Dashboard, https://polo.pilotprotocol.network
14
13
  Keywords: pilot-protocol,networking,p2p,agent,ai,protocol,ctypes,udp,encryption
15
14
  Classifier: Development Status :: 4 - Beta
16
15
  Classifier: Intended Audience :: Developers
@@ -312,7 +311,7 @@ See `examples/python_sdk/` for comprehensive examples:
312
311
  - **`basic_usage.py`** — Connection, identity, trust management
313
312
  - **`data_exchange_demo.py`** — Send messages, files, JSON
314
313
  - **`event_stream_demo.py`** — Pub/sub patterns
315
- - **`task_submit_demo.py`** — Task delegation and polo score
314
+ - **`task_submit_demo.py`** — Task delegation
316
315
  - **`pydantic_ai_agent.py`** — PydanticAI integration with function tools
317
316
  - **`pydantic_ai_multiagent.py`** — Multi-agent collaboration system
318
317
 
@@ -349,7 +348,7 @@ make publish-test # Publish to TestPyPI
349
348
  - **Examples:** `examples/python_sdk/README.md`
350
349
  - **CLI Reference:** `examples/cli/BASIC_USAGE.md`
351
350
  - **Protocol Spec:** `docs/SPEC.md`
352
- - **Agent Skills:** `docs/SKILLS.md`
351
+ - **Agent Skills:** https://github.com/TeoSlayer/pilot-skills
353
352
 
354
353
  ## License
355
354
 
@@ -5,6 +5,7 @@ README.md
5
5
  pyproject.toml
6
6
  setup.py
7
7
  pilotprotocol/__init__.py
8
+ pilotprotocol/_runtime.py
8
9
  pilotprotocol/cli.py
9
10
  pilotprotocol/client.py
10
11
  pilotprotocol/py.typed
@@ -14,6 +15,7 @@ pilotprotocol.egg-info/dependency_links.txt
14
15
  pilotprotocol.egg-info/entry_points.txt
15
16
  pilotprotocol.egg-info/requires.txt
16
17
  pilotprotocol.egg-info/top_level.txt
18
+ pilotprotocol/bin/.pilot-version
17
19
  pilotprotocol/bin/libpilot.h
18
20
  pilotprotocol/bin/libpilot.so
19
21
  pilotprotocol/bin/pilot-daemon
@@ -9,22 +9,23 @@ include-package-data = true
9
9
  [tool.setuptools.package-data]
10
10
  pilotprotocol = [
11
11
  "bin/*",
12
+ "bin/.pilot-version",
12
13
  "py.typed"
13
14
  ]
14
15
 
15
16
  [project]
16
17
  name = "pilotprotocol"
17
- version = "1.7.2"
18
+ version = "1.10.0"
18
19
  description = "Python SDK for Pilot Protocol - the network stack for AI agents"
19
20
  readme = "README.md"
20
21
  requires-python = ">=3.10"
21
22
  license = {text = "AGPL-3.0-or-later"}
22
23
  authors = [
23
- {name = "Alexandru Godoroja", email = "alex@vulturelabs.com"}
24
+ {name = "Alexandru Godoroja", email = "alex@pilotprotocol.network"}
24
25
  ]
25
26
  maintainers = [
26
- {name = "Alexandru Godoroja", email = "alex@vulturelabs.com"},
27
- {name = "Teodor Calin", email = "teodor@vulturelabs.com"}
27
+ {name = "Alexandru Godoroja", email = "alex@pilotprotocol.network"},
28
+ {name = "Teodor Calin", email = "teodor@pilotprotocol.network"}
28
29
  ]
29
30
  keywords = [
30
31
  "pilot-protocol",
@@ -68,7 +69,6 @@ Documentation = "https://pilotprotocol.network/docs/"
68
69
  Repository = "https://github.com/TeoSlayer/pilotprotocol"
69
70
  "Bug Tracker" = "https://github.com/TeoSlayer/pilotprotocol/issues"
70
71
  Changelog = "https://github.com/TeoSlayer/pilotprotocol/blob/main/sdk/python/CHANGELOG.md"
71
- "Live Dashboard" = "https://polo.pilotprotocol.network"
72
72
 
73
73
  [project.optional-dependencies]
74
74
  dev = [
@@ -1,169 +0,0 @@
1
- """Command-line interface wrappers for Pilot Protocol binaries.
2
-
3
- This module provides entry points for the bundled Go binaries:
4
- - pilotctl: CLI tool for managing the daemon
5
- - pilot-daemon: Background service
6
- - pilot-gateway: IP traffic bridge
7
-
8
- Each wrapper:
9
- 1. Ensures ~/.pilot/ directory exists
10
- 2. Creates default config.json if missing
11
- 3. Executes the bundled binary with all arguments passed through
12
- """
13
-
14
- import json
15
- import os
16
- import subprocess
17
- import sys
18
- from pathlib import Path
19
-
20
-
21
- def _ensure_pilot_env():
22
- """Ensure ~/.pilot/ directory and config.json exist.
23
-
24
- Creates:
25
- - ~/.pilot/ directory
26
- - ~/.pilot/config.json with default settings (if not present)
27
-
28
- This function is called before every binary execution to ensure
29
- the runtime environment is properly initialized.
30
- """
31
- # Get user's home directory
32
- home = Path.home()
33
- pilot_dir = home / ".pilot"
34
- config_file = pilot_dir / "config.json"
35
-
36
- # Create ~/.pilot/ if it doesn't exist
37
- pilot_dir.mkdir(parents=True, exist_ok=True)
38
-
39
- # Create default config.json if it doesn't exist
40
- if not config_file.exists():
41
- default_config = {
42
- "registry": "34.71.57.205:9000",
43
- "beacon": "34.71.57.205:9001",
44
- "socket": "/tmp/pilot.sock",
45
- "encrypt": True,
46
- "identity": str(pilot_dir / "identity.json")
47
- }
48
-
49
- with open(config_file, 'w') as f:
50
- json.dump(default_config, f, indent=2)
51
-
52
-
53
- def _get_binary_path(binary_name: str) -> Path:
54
- """Get absolute path to a bundled binary.
55
-
56
- Args:
57
- binary_name: Name of the binary (e.g., 'pilotctl', 'pilot-daemon')
58
-
59
- Returns:
60
- Absolute path to the binary
61
-
62
- Raises:
63
- FileNotFoundError: If binary not found in package
64
- """
65
- # Find the bin/ directory relative to this file
66
- package_dir = Path(__file__).resolve().parent
67
- bin_dir = package_dir / "bin"
68
- binary_path = bin_dir / binary_name
69
-
70
- if not binary_path.exists():
71
- raise FileNotFoundError(
72
- f"Binary '{binary_name}' not found at {binary_path}\n"
73
- f"Expected location: {bin_dir}\n"
74
- "The wheel may not have been built correctly."
75
- )
76
-
77
- return binary_path
78
-
79
-
80
- def run_pilotctl():
81
- """Entry point for pilotctl CLI tool.
82
-
83
- This is called when the user runs 'pilotctl' from the command line.
84
- All arguments are passed through to the Go binary.
85
-
86
- Example:
87
- $ pilotctl daemon start --hostname my-agent
88
- $ pilotctl info
89
- $ pilotctl ping other-agent
90
- """
91
- # Ensure environment is set up
92
- _ensure_pilot_env()
93
-
94
- # Get path to bundled binary
95
- binary = _get_binary_path("pilotctl")
96
-
97
- # Execute the binary with all arguments
98
- # subprocess.call() returns the exit code directly
99
- exit_code = subprocess.call([str(binary)] + sys.argv[1:])
100
-
101
- # Exit with the same code as the binary
102
- sys.exit(exit_code)
103
-
104
-
105
- def run_daemon():
106
- """Entry point for pilot-daemon background service.
107
-
108
- This is called when the user runs 'pilot-daemon' from the command line.
109
- All arguments are passed through to the Go binary.
110
-
111
- Example:
112
- $ pilot-daemon -registry 34.71.57.205:9000 -beacon 34.71.57.205:9001
113
- $ pilot-daemon -hostname my-agent -public
114
- """
115
- # Ensure environment is set up
116
- _ensure_pilot_env()
117
-
118
- # Get path to bundled binary
119
- binary = _get_binary_path("pilot-daemon")
120
-
121
- # Execute the binary with all arguments
122
- exit_code = subprocess.call([str(binary)] + sys.argv[1:])
123
-
124
- # Exit with the same code as the binary
125
- sys.exit(exit_code)
126
-
127
-
128
- def run_gateway():
129
- """Entry point for pilot-gateway IP traffic bridge.
130
-
131
- This is called when the user runs 'pilot-gateway' from the command line.
132
- All arguments are passed through to the Go binary.
133
-
134
- Example:
135
- $ pilot-gateway --ports 80,3000 <pilot-addr>
136
- """
137
- # Ensure environment is set up
138
- _ensure_pilot_env()
139
-
140
- # Get path to bundled binary
141
- binary = _get_binary_path("pilot-gateway")
142
-
143
- # Execute the binary with all arguments
144
- exit_code = subprocess.call([str(binary)] + sys.argv[1:])
145
-
146
- # Exit with the same code as the binary
147
- sys.exit(exit_code)
148
-
149
-
150
- def run_updater():
151
- """Entry point for pilot-updater auto-update sidecar.
152
-
153
- This is called when the user runs 'pilot-updater' from the command line.
154
- All arguments are passed through to the Go binary.
155
-
156
- Example:
157
- $ pilot-updater -install-dir ~/.pilot/bin
158
- """
159
- # Ensure environment is set up
160
- _ensure_pilot_env()
161
-
162
- # Get path to bundled binary
163
- binary = _get_binary_path("pilot-updater")
164
-
165
- # Execute the binary with all arguments
166
- exit_code = subprocess.call([str(binary)] + sys.argv[1:])
167
-
168
- # Exit with the same code as the binary
169
- sys.exit(exit_code)
File without changes
File without changes
File without changes