Skip to content

feat: build python sdk for pilotprotocol - #10

Merged
TeoSlayer merged 37 commits into
mainfrom
build/python_sdk
Mar 11, 2026
Merged

TeoSlayer merged 37 commits into
mainfrom
build/python_sdk

Conversation

@Alexgodoroja

@Alexgodoroja Alexgodoroja commented Mar 5, 2026

Copy link
Copy Markdown
Collaborator

Python SDK for Pilot Protocol

Overview

Python client library that wraps the Go driver via ctypes/FFI, providing full access to the Pilot Protocol network with zero protocol reimplementation.

Architecture

Single Source of Truth: Go pkg/driver compiled to C-shared library (libpilot.so/.dylib/.dll) called from Python via ctypes.

Python SDK (ctypes) → libpilot.so (Go) → Unix socket → Daemon

Core Features

Network Operations

  • Connection management: Connect to local daemon, dial peers, listen on ports
  • Peer discovery: Resolve hostnames, find agents by address
  • Trust & handshakes: Send/accept handshakes, manage trusted peers
  • Data exchange: Stream connections, datagrams, file transfers
  • Event streaming: Publish/subscribe to topics
  • Task submission: Submit tasks to remote agents

Python API

Driver (Main Client)

Driver() - Connect to daemon
  .info() - Get node info (address, hostname, ID)
  .set_hostname(name) - Set node hostname
  .set_visibility(public/private) - Control discoverability
  .set_tags([tags]) - Set searchable tags
  .resolve_hostname(name) - Find peer by hostname
  .handshake(address) - Initiate trust with peer
  .trusted_peers() - List trusted connections
  .dial(address:port) - Open stream to peer
  .listen(port) - Listen for incoming connections
  .send_datagram(address, data) - Send UDP-style message
  .send_message(address, data) - High-level message send with ACK
  .send_file(address, path) - High-level file transfer
  .publish_event(topic, data) - Publish to topic
  .submit_task(address, task) - Submit task to agent

Connection (Stream I/O)

Conn(driver, address:port) - Stream connection
  .write(bytes) - Send data
  .read(size) - Receive data
  .close() - Close connection
  Context manager support: with d.dial(...) as conn

Listener (Server)

Listener(driver, port) - Accept connections
  .accept() - Wait for incoming connection
  .close() - Stop listening

CLI Tools (Entry Points)

All available after pip install:

  • pilotctl - Main CLI (info, peers, dial, etc)
  • pilot-daemon - Background network daemon
  • pilot-gateway - HTTP/WebSocket gateway

Bundled Components

Each wheel includes:

  • Python SDK (pilotprotocol package)
  • Go binaries (daemon, gateway, pilotctl)
  • CGO shared library (libpilot.so)
  • Entry point scripts
  • ~7-8MB total per platform

Platform Support

  • Linux: x86_64 (manylinux_2_35+)
  • macOS: Intel + Apple Silicon (universal2)
  • Windows: Coming soon

Auto-Discovery

SDK finds libpilot library from:

  1. ~/.pilot/bin/ (pip install location)
  2. Package directory (site-packages/pilotprotocol/bin/)
  3. PILOT_LIB_PATH environment variable
  4. Development layout (<project>/bin/)

State Management

  • Config: ~/.pilot/config.json
  • Identity: ~/.pilot/identity.key
  • Logs: ~/.pilot/logs/
  • Socket: /tmp/pilot.sock (Unix) or named pipe (Windows)

Installation

pip install pilotprotocol

Auto-configures on first run.

Quick Example

from pilotprotocol import Driver

with Driver() as d:
    info = d.info()
    print(f"Address: {info['address']}")
    
    d.set_hostname("my-agent")
    
    peer = d.resolve_hostname("other-agent")
    
    with d.dial(f"{peer['address']}:1000") as conn:
        conn.write(b"Hello!")
        response = conn.read(4096)

Testing

  • 34 integration tests covering all core functionality
  • Tests run against real daemon in Docker
  • 100% coverage of public API

Package Structure

pilotprotocol/
├── __init__.py        # Public API exports
├── client.py          # Driver, Conn, Listener classes
├── cli.py             # Entry point wrappers
├── bin/               # Bundled binaries
│   ├── libpilot.so    # CGO shared library
│   ├── pilot-daemon   # Network daemon
│   ├── pilot-gateway  # HTTP gateway
│   └── pilotctl       # CLI tool
└── py.typed           # Type checking marker

Key Design Decisions

  1. No protocol reimplementation - All logic in Go
  2. Bundled binaries - Zero external dependencies
  3. Entry points over install hooks - Modern packaging
  4. State in home directory - Clean package directory
  5. Context managers - Pythonic resource management
  6. Type hints - Full type coverage with py.typed

@Alexgodoroja Alexgodoroja self-assigned this Mar 5, 2026
@Alexgodoroja Alexgodoroja added the enhancement New feature or request label Mar 5, 2026
@Alexgodoroja
Alexgodoroja marked this pull request as ready for review March 5, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a Python SDK for the Pilot Protocol, built as a ctypes/FFI wrapper over a CGO-compiled shared library (libpilot.so). This allows Python callers to use the full Go driver without reimplementing the protocol.

Changes:

  • New Python SDK (sdk/python/, sdk/cgo/): Package pilotprotocol with Driver, Conn, Listener, and PilotError; CGO bindings in sdk/cgo/bindings.go; CLI entry-point wrappers; build/publish scripts.
  • Integration tests (tests/integration/): Docker-based integration test suite testing the CLI (21 tests) and Python SDK (34 tests) against a live agent-alpha agent.
  • Documentation & website updates: New Python SDK doc page link added to all sidebar navs, landing page tabs for Shell/Python/ClawHub installation, updated README.md, CONTRIBUTING.md, and registry persistence improvements.

Reviewed changes

Copilot reviewed 70 out of 83 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
sdk/cgo/bindings.go CGO C-shared library exposing Go driver to Python via uint64 handles
sdk/python/pilotprotocol/__init__.py Public API exports
sdk/python/pilotprotocol/cli.py Entry-point wrappers for bundled Go binaries
sdk/python/pyproject.toml Package metadata and build config
sdk/python/scripts/generate-coverage-badge.sh Coverage badge generator (has hardcoded local path bug)
tests/integration/Dockerfile Multi-stage Docker build for integration tests (references nonexistent Go 1.25)
tests/integration/Makefile Test runner (missing exit 1 on Docker failure)
tests/integration/test_cli.sh CLI integration tests
pkg/registry/dashboard.go Snapshot endpoint with spoofable localhost check
pkg/registry/server.go Dashboard stat persistence; TriggerSnapshot with misleading error return
web/index.html Landing page with incorrect Python SDK code examples
sdk/python/README.md Duplicate Development section and inconsistent test counts
web/docs/*.html Sidebar nav updates to add Python SDK link
examples/python_sdk/ Python example scripts
examples/go/ New Go example programs
.github/workflows/tests.yml CI workflow for unit + integration tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/registry/dashboard.go
Comment thread sdk/python/README.md Outdated
Comment thread tests/integration/test_cli.sh
Comment thread sdk/python/scripts/generate-coverage-badge.sh Outdated
Comment thread web/index.html
Comment thread pkg/registry/server.go
Comment thread sdk/cgo/bindings.go
Comment thread pkg/registry/server.go
Comment thread sdk/python/pilotprotocol/cli.py
Comment thread tests/integration/Makefile
@Alexgodoroja
Alexgodoroja requested a review from TeoSlayer March 8, 2026 18:36

@TeoSlayer TeoSlayer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked.

@TeoSlayer
TeoSlayer merged commit 0bc42c7 into main Mar 11, 2026
5 checks passed
@TeoSlayer
TeoSlayer deleted the build/python_sdk branch March 23, 2026 22:28
TeoSlayer pushed a commit that referenced this pull request Apr 30, 2026
* feat: implement python sdk

* ci: use github actions to automate sdk build and publish

* netanyahooo

* fix: remove windows build step for python sdk

* docs: update website with python sdk documentation

* fix: remove manual approval from publish step

* fix: use environment secrets directly

* test: add integration testing on real network

* test: add integration testing on real network

* fix: reduce CI logs

* fix: remove dashboard tests

* ci: remove verbose output

* ci: run appropriate tests

* ci: reduce parallelism for tests

* ci: increae timeout

* ci: standardise testing using go runner

* ci: specify usage of bash

* ci: fix regex netanyahu pls

* ci: dispatch sdk pyblish after tests ran

* ci: enable automating versioning

* ci: standardize sdk versioning

* test: test build and publish workflow

* ci: test versioning in ci

* ci: fix the duplicate contents permission

* ci: final ci architecture

* ci: fix stupid regex typo fml

* ci: handle color codes in regex

* ci: fix stupid command

* ci: adhere to pypi versioning requirements

* ci: fix platform tags for wheels

* ci: manylinux compliant tags

* ci: remove outdated config

* ci: fix dev tags

* ci: fix gh tags

* ci: revert corrupted worflow file

* fix: final review

* fix: fix Teo's slopified testing framework

---------

Co-authored-by: Alex Godoroja <alex@vulturelabs.io>
TeoSlayer pushed a commit that referenced this pull request May 3, 2026
Symptom: when a Listener's AcceptCh is full (the application is slow
to Accept and queue has filled to AcceptQueueLen=64), handleStreamPacket
correctly sends RST + cleans up — but does so silently. There's no
Daemon-level counter to expose the drop rate, and no webhook event
operators can wire to alerting. The first signal of trouble is users
complaining connections fail; by then thousands of SYNs have dropped.

Code path (pkg/daemon/daemon.go:1841):

  select {
  case ln.AcceptCh <- conn:
  default:
      slog.Warn("accept queue full after SYN-ACK, closing connection", ...)
      conn.Mu.Lock()
      conn.State = StateClosed
      conn.Mu.Unlock()
      d.ports.RemoveConnection(conn.ID)
      d.sendRST(pkt)
  }

Worse, the conn.established + conn.rst webhook pair fires for a
connection that never reached the application — looks like a
peer-initiated abort, not a queue overflow. Operators dashboards
treat the two cases identically.

Lands in this RED commit:
  - AcceptQueueDrops uint64 field on Daemon (currently never
    incremented — body lands in GREEN)

Tests:
  - TestSYNDropOnFullAcceptQueueIsInvisible: pre-fill AcceptCh to
    capacity, send a SYN, observe RST on peer socket, then assert
    AcceptQueueDrops counter stays at 0. GREEN flips: counter == 1
    plus a conn.accept_queue_full webhook fires.
TeoSlayer pushed a commit that referenced this pull request May 3, 2026
Bug fixed: handleStreamPacket's accept-queue overflow branch now
increments AcceptQueueDrops (atomic.AddUint64) and emits a dedicated
`conn.accept_queue_full` webhook event with port, src_addr, src_port,
and drops_total. Operators get a programmatic signal — counter for
metrics scraping, webhook for paging/autoscale — instead of relying
on log line parsing to detect overflow.

Implementation (pkg/daemon/daemon.go SYN handler):

  select {
  case ln.AcceptCh <- conn:
  default:
      drops := atomic.AddUint64(&d.AcceptQueueDrops, 1)
      slog.Warn("accept queue full after SYN-ACK, closing connection",
          "port", pkt.DstPort, "src_addr", pkt.Src, "drops_total", drops)
      conn.Mu.Lock(); conn.State = StateClosed; conn.Mu.Unlock()
      d.ports.RemoveConnection(conn.ID)
      d.sendRST(pkt)
      d.webhook.Emit("conn.accept_queue_full", map[string]interface{}{
          "port": pkt.DstPort, "src_addr": pkt.Src.String(),
          "src_port": pkt.SrcPort, "drops_total": drops,
      })
  }

Why a dedicated event (not just conn.rst): the existing conn.rst
fires for many causes (peer-initiated abort, dead-peer detection,
explicit close). Without a separate accept_queue_full signal,
operators can't distinguish overflow from real connection failures
in the webhook stream. The dedicated event also carries drops_total
so dashboards can compute drop rate without log mining.

Tests:
  - TestSYNDropOnFullAcceptQueueIsInvisible (renamed-purpose):
    pre-fill AcceptCh, send 1 SYN, observe RST + AcceptQueueDrops=1.
  - TestAcceptQueueDropsAggregateAcrossSYNs: 3 distinct SYNs → 3 drops,
    counter is monotonic.

Race-clean across full pkg/daemon (65s). 16 commits ahead of v1.9.0,
10 bug categories closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants