pilotprotocol 1.7.2__tar.gz → 1.9.1__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.
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/MANIFEST.in +4 -1
- {pilotprotocol-1.7.2/pilotprotocol.egg-info → pilotprotocol-1.9.1}/PKG-INFO +5 -6
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/README.md +2 -2
- pilotprotocol-1.9.1/pilotprotocol/_runtime.py +382 -0
- pilotprotocol-1.9.1/pilotprotocol/bin/.pilot-version +1 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol/bin/libpilot.h +32 -1
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol/bin/libpilot.so +0 -0
- pilotprotocol-1.7.2/pilotprotocol/bin/pilotctl → pilotprotocol-1.9.1/pilotprotocol/bin/pilot-daemon +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol/bin/pilot-gateway +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol/bin/pilot-updater +0 -0
- pilotprotocol-1.7.2/pilotprotocol/bin/pilot-daemon → pilotprotocol-1.9.1/pilotprotocol/bin/pilotctl +0 -0
- pilotprotocol-1.9.1/pilotprotocol/cli.py +55 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol/client.py +266 -7
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1/pilotprotocol.egg-info}/PKG-INFO +5 -6
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol.egg-info/SOURCES.txt +2 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pyproject.toml +5 -5
- pilotprotocol-1.7.2/pilotprotocol/cli.py +0 -169
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/CHANGELOG.md +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/LICENSE +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol/__init__.py +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol/py.typed +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol.egg-info/dependency_links.txt +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol.egg-info/entry_points.txt +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol.egg-info/requires.txt +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/pilotprotocol.egg-info/top_level.txt +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/setup.cfg +0 -0
- {pilotprotocol-1.7.2 → pilotprotocol-1.9.1}/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.
|
|
3
|
+
Version: 1.9.1
|
|
4
4
|
Summary: Python SDK for Pilot Protocol - the network stack for AI agents
|
|
5
|
-
Author-email: Alexandru Godoroja <alex@
|
|
6
|
-
Maintainer-email: Alexandru Godoroja <alex@
|
|
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
|
|
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:**
|
|
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
|
|
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:**
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.9.1
|
|
@@ -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
|
|
24
|
+
#line 5 "bindings.go"
|
|
25
25
|
|
|
26
26
|
#include <stdlib.h>
|
|
27
27
|
#include <stdint.h>
|
|
@@ -152,6 +152,37 @@ extern struct PilotConnWrite_return PilotConnWrite(uint64_t ch, void* data, int
|
|
|
152
152
|
extern char* PilotConnClose(uint64_t ch);
|
|
153
153
|
extern char* PilotSendTo(uint64_t h, char* fullAddr, void* data, int dataLen);
|
|
154
154
|
extern char* PilotRecvFrom(uint64_t h);
|
|
155
|
+
extern char* PilotHealth(uint64_t h);
|
|
156
|
+
extern char* PilotRotateKey(uint64_t h);
|
|
157
|
+
extern char* PilotBroadcast(uint64_t h, uint16_t netID, uint16_t port, void* data, int dataLen, char* adminToken);
|
|
158
|
+
|
|
159
|
+
/* Return type for PilotDialTimeout */
|
|
160
|
+
struct PilotDialTimeout_return {
|
|
161
|
+
uint64_t r0;
|
|
162
|
+
char* r1;
|
|
163
|
+
};
|
|
164
|
+
extern struct PilotDialTimeout_return PilotDialTimeout(uint64_t h, char* addr, uint64_t timeoutMs);
|
|
165
|
+
|
|
166
|
+
// PilotConnSetReadDeadline sets the read deadline as Unix nanoseconds.
|
|
167
|
+
// Pass 0 to clear the deadline.
|
|
168
|
+
//
|
|
169
|
+
extern char* PilotConnSetReadDeadline(uint64_t ch, int64_t deadlineUnixNanos);
|
|
170
|
+
extern char* PilotNetworkList(uint64_t h);
|
|
171
|
+
extern char* PilotNetworkJoin(uint64_t h, uint16_t networkID, char* token);
|
|
172
|
+
extern char* PilotNetworkLeave(uint64_t h, uint16_t networkID);
|
|
173
|
+
extern char* PilotNetworkMembers(uint64_t h, uint16_t networkID);
|
|
174
|
+
extern char* PilotNetworkInvite(uint64_t h, uint16_t networkID, uint32_t targetNodeID);
|
|
175
|
+
extern char* PilotNetworkPollInvites(uint64_t h);
|
|
176
|
+
extern char* PilotNetworkRespondInvite(uint64_t h, uint16_t networkID, int accept);
|
|
177
|
+
extern char* PilotManagedScore(uint64_t h, uint16_t networkID, uint32_t nodeID, int32_t delta, char* topic);
|
|
178
|
+
extern char* PilotManagedStatus(uint64_t h, uint16_t networkID);
|
|
179
|
+
extern char* PilotManagedRankings(uint64_t h, uint16_t networkID);
|
|
180
|
+
extern char* PilotManagedForceCycle(uint64_t h, uint16_t networkID);
|
|
181
|
+
extern char* PilotManagedReconcile(uint64_t h, uint16_t networkID);
|
|
182
|
+
extern char* PilotPolicyGet(uint64_t h, uint16_t networkID);
|
|
183
|
+
extern char* PilotPolicySet(uint64_t h, uint16_t networkID, char* policyJSON);
|
|
184
|
+
extern char* PilotMemberTagsGet(uint64_t h, uint16_t networkID, uint32_t nodeID);
|
|
185
|
+
extern char* PilotMemberTagsSet(uint64_t h, uint16_t networkID, uint32_t nodeID, char* tagsJSON);
|
|
155
186
|
|
|
156
187
|
#ifdef __cplusplus
|
|
157
188
|
}
|
|
Binary file
|
pilotprotocol-1.7.2/pilotprotocol/bin/pilotctl → pilotprotocol-1.9.1/pilotprotocol/bin/pilot-daemon
RENAMED
|
Binary file
|
|
Binary file
|
|
Binary file
|
pilotprotocol-1.7.2/pilotprotocol/bin/pilot-daemon → pilotprotocol-1.9.1/pilotprotocol/bin/pilotctl
RENAMED
|
Binary file
|
|
@@ -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
|
-
|
|
105
|
-
|
|
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", "
|
|
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]
|
|
@@ -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]:
|
|
@@ -540,10 +645,18 @@ class Driver:
|
|
|
540
645
|
|
|
541
646
|
# -- Streams --
|
|
542
647
|
|
|
543
|
-
def dial(self, addr: str) -> Conn:
|
|
544
|
-
"""Open a stream connection to addr (format: "N:XXXX.YYYY.YYYY:PORT").
|
|
648
|
+
def dial(self, addr: str, timeout: Optional[float] = None) -> Conn:
|
|
649
|
+
"""Open a stream connection to addr (format: "N:XXXX.YYYY.YYYY:PORT").
|
|
650
|
+
|
|
651
|
+
If ``timeout`` is given (seconds), the dial is cancelled if the daemon
|
|
652
|
+
does not respond within that window.
|
|
653
|
+
"""
|
|
545
654
|
lib = _get_lib()
|
|
546
|
-
|
|
655
|
+
if timeout is None:
|
|
656
|
+
res = lib.PilotDial(self._h, addr.encode())
|
|
657
|
+
else:
|
|
658
|
+
ms = max(0, int(timeout * 1000))
|
|
659
|
+
res = lib.PilotDialTimeout(self._h, addr.encode(), ctypes.c_uint64(ms))
|
|
547
660
|
if res.err:
|
|
548
661
|
raw = ctypes.string_at(res.err)
|
|
549
662
|
lib.FreeString(res.err)
|
|
@@ -576,6 +689,152 @@ class Driver:
|
|
|
576
689
|
"""
|
|
577
690
|
return self._call_json("PilotRecvFrom")
|
|
578
691
|
|
|
692
|
+
def broadcast(
|
|
693
|
+
self,
|
|
694
|
+
network_id: int,
|
|
695
|
+
port: int,
|
|
696
|
+
data: bytes,
|
|
697
|
+
admin_token: str,
|
|
698
|
+
) -> None:
|
|
699
|
+
"""Broadcast an unreliable datagram to every member of a network.
|
|
700
|
+
|
|
701
|
+
Requires the daemon's admin token; an empty or mismatched token is
|
|
702
|
+
rejected. Permitted on every network including network 0 (backbone).
|
|
703
|
+
"""
|
|
704
|
+
lib = _get_lib()
|
|
705
|
+
buf = ctypes.create_string_buffer(data)
|
|
706
|
+
ptr = lib.PilotBroadcast(
|
|
707
|
+
self._h,
|
|
708
|
+
ctypes.c_uint16(network_id),
|
|
709
|
+
ctypes.c_uint16(port),
|
|
710
|
+
buf,
|
|
711
|
+
ctypes.c_int(len(data)),
|
|
712
|
+
admin_token.encode(),
|
|
713
|
+
)
|
|
714
|
+
_check_err(ptr)
|
|
715
|
+
|
|
716
|
+
# -- Networks --
|
|
717
|
+
|
|
718
|
+
def network_list(self) -> dict[str, Any]:
|
|
719
|
+
"""List all networks known to the registry."""
|
|
720
|
+
return self._call_json("PilotNetworkList")
|
|
721
|
+
|
|
722
|
+
def network_join(self, network_id: int, token: str = "") -> dict[str, Any]:
|
|
723
|
+
"""Join a network by ID, optionally with a token for token-gated networks."""
|
|
724
|
+
return self._call_json(
|
|
725
|
+
"PilotNetworkJoin", ctypes.c_uint16(network_id), token.encode()
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def network_leave(self, network_id: int) -> dict[str, Any]:
|
|
729
|
+
"""Leave a network by ID."""
|
|
730
|
+
return self._call_json("PilotNetworkLeave", ctypes.c_uint16(network_id))
|
|
731
|
+
|
|
732
|
+
def network_members(self, network_id: int) -> dict[str, Any]:
|
|
733
|
+
"""List all members of a network."""
|
|
734
|
+
return self._call_json("PilotNetworkMembers", ctypes.c_uint16(network_id))
|
|
735
|
+
|
|
736
|
+
def network_invite(self, network_id: int, target_node_id: int) -> dict[str, Any]:
|
|
737
|
+
"""Invite a target node to a network (requires admin token on daemon)."""
|
|
738
|
+
return self._call_json(
|
|
739
|
+
"PilotNetworkInvite",
|
|
740
|
+
ctypes.c_uint16(network_id),
|
|
741
|
+
ctypes.c_uint32(target_node_id),
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
def network_poll_invites(self) -> dict[str, Any]:
|
|
745
|
+
"""Return pending network invites for this node."""
|
|
746
|
+
return self._call_json("PilotNetworkPollInvites")
|
|
747
|
+
|
|
748
|
+
def network_respond_invite(self, network_id: int, accept: bool) -> dict[str, Any]:
|
|
749
|
+
"""Accept or reject a pending network invite."""
|
|
750
|
+
return self._call_json(
|
|
751
|
+
"PilotNetworkRespondInvite",
|
|
752
|
+
ctypes.c_uint16(network_id),
|
|
753
|
+
ctypes.c_int(1 if accept else 0),
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# -- Managed networks --
|
|
757
|
+
|
|
758
|
+
def managed_score(
|
|
759
|
+
self,
|
|
760
|
+
network_id: int,
|
|
761
|
+
node_id: int,
|
|
762
|
+
delta: int,
|
|
763
|
+
topic: str = "",
|
|
764
|
+
) -> dict[str, Any]:
|
|
765
|
+
"""Adjust a peer's score in a managed network."""
|
|
766
|
+
return self._call_json(
|
|
767
|
+
"PilotManagedScore",
|
|
768
|
+
ctypes.c_uint16(network_id),
|
|
769
|
+
ctypes.c_uint32(node_id),
|
|
770
|
+
ctypes.c_int32(delta),
|
|
771
|
+
topic.encode(),
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
def managed_status(self, network_id: int) -> dict[str, Any]:
|
|
775
|
+
"""Return the status of a managed network engine."""
|
|
776
|
+
return self._call_json("PilotManagedStatus", ctypes.c_uint16(network_id))
|
|
777
|
+
|
|
778
|
+
def managed_rankings(self, network_id: int) -> dict[str, Any]:
|
|
779
|
+
"""Return ranked peers in a managed network."""
|
|
780
|
+
return self._call_json("PilotManagedRankings", ctypes.c_uint16(network_id))
|
|
781
|
+
|
|
782
|
+
def managed_force_cycle(self, network_id: int) -> dict[str, Any]:
|
|
783
|
+
"""Force a prune/fill cycle in a managed network."""
|
|
784
|
+
return self._call_json("PilotManagedForceCycle", ctypes.c_uint16(network_id))
|
|
785
|
+
|
|
786
|
+
def managed_reconcile(self, network_id: int) -> dict[str, Any]:
|
|
787
|
+
"""Refresh the managed network's peer set without running a policy cycle."""
|
|
788
|
+
return self._call_json("PilotManagedReconcile", ctypes.c_uint16(network_id))
|
|
789
|
+
|
|
790
|
+
# -- Policy --
|
|
791
|
+
|
|
792
|
+
def policy_get(self, network_id: int) -> dict[str, Any]:
|
|
793
|
+
"""Retrieve the active policy for a network."""
|
|
794
|
+
return self._call_json("PilotPolicyGet", ctypes.c_uint16(network_id))
|
|
795
|
+
|
|
796
|
+
def policy_set(self, network_id: int, policy: Any) -> dict[str, Any]:
|
|
797
|
+
"""Apply a policy document to a network.
|
|
798
|
+
|
|
799
|
+
``policy`` may be a dict, a JSON string, or pre-encoded bytes.
|
|
800
|
+
"""
|
|
801
|
+
if isinstance(policy, (bytes, bytearray)):
|
|
802
|
+
payload = bytes(policy)
|
|
803
|
+
elif isinstance(policy, str):
|
|
804
|
+
payload = policy.encode()
|
|
805
|
+
else:
|
|
806
|
+
payload = json.dumps(policy).encode()
|
|
807
|
+
return self._call_json(
|
|
808
|
+
"PilotPolicySet", ctypes.c_uint16(network_id), payload
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
# -- Member tags --
|
|
812
|
+
|
|
813
|
+
def member_tags_get(self, network_id: int, node_id: int) -> dict[str, Any]:
|
|
814
|
+
"""Retrieve admin-assigned member tags for a node in a network."""
|
|
815
|
+
return self._call_json(
|
|
816
|
+
"PilotMemberTagsGet",
|
|
817
|
+
ctypes.c_uint16(network_id),
|
|
818
|
+
ctypes.c_uint32(node_id),
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
def member_tags_set(
|
|
822
|
+
self, network_id: int, node_id: int, tags: list[str]
|
|
823
|
+
) -> dict[str, Any]:
|
|
824
|
+
"""Set admin-assigned member tags for a node in a network."""
|
|
825
|
+
return self._call_json(
|
|
826
|
+
"PilotMemberTagsSet",
|
|
827
|
+
ctypes.c_uint16(network_id),
|
|
828
|
+
ctypes.c_uint32(node_id),
|
|
829
|
+
json.dumps(tags).encode(),
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
# -- Identity --
|
|
833
|
+
|
|
834
|
+
def rotate_identity(self) -> dict[str, Any]:
|
|
835
|
+
"""Alias for :meth:`rotate_key`."""
|
|
836
|
+
return self.rotate_key()
|
|
837
|
+
|
|
579
838
|
# -- High-level service methods --
|
|
580
839
|
|
|
581
840
|
def send_message(self, target: str, data: bytes, msg_type: str = "text") -> dict[str, Any]:
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pilotprotocol
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.1
|
|
4
4
|
Summary: Python SDK for Pilot Protocol - the network stack for AI agents
|
|
5
|
-
Author-email: Alexandru Godoroja <alex@
|
|
6
|
-
Maintainer-email: Alexandru Godoroja <alex@
|
|
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
|
|
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:**
|
|
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.
|
|
18
|
+
version = "1.9.1"
|
|
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@
|
|
24
|
+
{name = "Alexandru Godoroja", email = "alex@pilotprotocol.network"}
|
|
24
25
|
]
|
|
25
26
|
maintainers = [
|
|
26
|
-
{name = "Alexandru Godoroja", email = "alex@
|
|
27
|
-
{name = "Teodor Calin", email = "teodor@
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|