Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,31 @@ project uses [Semantic Versioning](https://semver.org/).
Detailed per-release notes for tagged versions are published on the
[GitHub Releases page](https://github.com/TeoSlayer/pilotprotocol/releases).

## [Unreleased]
## [1.9.2] - 2026-05-05

### Changed
- **SDK: updated sdk clients and cgo bindings to the latest version**

- **SDK: removed polo-score surface from Node and Python bindings.**
Dropped `PilotMyPolo` from the CGO export layer (`sdk/cgo/bindings.go`)
and the corresponding `Driver.myPoloScore()` / `Driver.my_polo_score()`
wrappers, fake-lib hooks, and tests in both SDKs. The driver-level
`MyPoloScore()` and the underlying registry/daemon machinery are
untouched — this only narrows what the language SDKs expose. Removed
the now-stale `polo.pilotprotocol.network` "Live Dashboard" project
URL from `sdk/python/pyproject.toml` and a stray polo mention from
the Python SDK README.

### Fixed

- **SDK: macOS binaries shipped via npm/pip codesign parity.**
`sdk/{node,python}/scripts/build-binaries.sh` now mirror the main
release workflow — after building `pilot-daemon`, `pilotctl`,
`pilot-gateway`, `pilot-updater`, and `libpilot.dylib` on darwin,
each artifact is codesigned (`codesign --force --deep --sign -`)
and stripped of the quarantine xattr. Without this, npm/pip-installed
binaries triggered Gatekeeper "killed: 9" / "cannot be opened
because Apple cannot check it for malicious software" on first run.

## [1.9.1] - 2026-05-05

Expand Down
12 changes: 6 additions & 6 deletions cmd/pilotctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1281,12 +1281,12 @@ func cmdContext() {
"enable-tasks": map[string]interface{}{"args": []string{}, "description": "Advertise task-execution capability on port 1003"},
"disable-tasks": map[string]interface{}{"args": []string{}, "description": "Stop advertising task-execution capability"},
// Low-level / plumbing
"connect": map[string]interface{}{"args": []string{"<address|hostname>", "[port]", "[--message <msg>]"}, "description": "Open a raw stream connection"},
"send": map[string]interface{}{"args": []string{"<address|hostname>", "<port>", "--data <msg>"}, "description": "Send a single raw message to a port"},
"recv": map[string]interface{}{"args": []string{"<port>", "[--count <n>]"}, "description": "Accept and print incoming stream messages"},
"dgram": map[string]interface{}{"args": []string{"<address|hostname>", "<port>", "--data <msg>"}, "description": "Send a UDP-style datagram"},
"listen": map[string]interface{}{"args": []string{"<port>", "[--count <n>]"}, "description": "Listen for incoming datagrams"},
"broadcast": map[string]interface{}{"args": []string{"<network_id>", "<message>"}, "description": "Broadcast a datagram to all network members"},
"connect": map[string]interface{}{"args": []string{"<address|hostname>", "[port]", "[--message <msg>]"}, "description": "Open a raw stream connection"},
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

from pre-commits

"send": map[string]interface{}{"args": []string{"<address|hostname>", "<port>", "--data <msg>"}, "description": "Send a single raw message to a port"},
"recv": map[string]interface{}{"args": []string{"<port>", "[--count <n>]"}, "description": "Accept and print incoming stream messages"},
"dgram": map[string]interface{}{"args": []string{"<address|hostname>", "<port>", "--data <msg>"}, "description": "Send a UDP-style datagram"},
"listen": map[string]interface{}{"args": []string{"<port>", "[--count <n>]"}, "description": "Listen for incoming datagrams"},
"broadcast": map[string]interface{}{"args": []string{"<network_id>", "<message>"}, "description": "Broadcast a datagram to all network members"},
// Connection management
"connections": map[string]interface{}{"args": []string{}, "description": "List active daemon connections"},
"disconnect": map[string]interface{}{"args": []string{"<conn_id>"}, "description": "Close a connection by ID"},
Expand Down
18 changes: 9 additions & 9 deletions cmd/pilotctl/redact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@ func TestRedactPeerEndpointsRemovesIPFields(t *testing.T) {
"node_id": 42,
"address": "0:0000.0000.002A",
"hostname": "agent-test",
"endpoint": "203.0.113.5:4000", // must go
"real_addr": "203.0.113.5:4000", // must go
"public_addr": "203.0.113.5:4000", // must go
"endpoint": "203.0.113.5:4000", // must go
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

recommits

"real_addr": "203.0.113.5:4000", // must go
"public_addr": "203.0.113.5:4000", // must go
"lan_addrs": []interface{}{"10.0.0.5:4000"}, // must go
"observed_addr": "203.0.113.5:4000", // must go
"stun_addr": "203.0.113.5:4000", // must go
"observed_addr": "203.0.113.5:4000", // must go
"stun_addr": "203.0.113.5:4000", // must go
"peers": 7,
"encrypted_peers": 7,
"peer_list": []interface{}{
map[string]interface{}{
"node_id": 10,
"endpoint": "198.51.100.10:4000",
"node_id": 10,
"endpoint": "198.51.100.10:4000",
"real_addr": "198.51.100.10:4000",
"encrypted": true,
},
map[string]interface{}{
"node_id": 11,
"endpoint": "198.51.100.11:4000",
"node_id": 11,
"endpoint": "198.51.100.11:4000",
"encrypted": false,
},
},
Expand Down
4 changes: 2 additions & 2 deletions cmd/pilotctl/updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ var changelogFeedURL = "https://teoslayer.github.io/pilot-changelog/feed.xml"
// for the human-readable + JSON output are decoded; unknown elements are
// ignored by encoding/xml.
type rssDoc struct {
XMLName xml.Name `xml:"rss"`
Channel rssChan `xml:"channel"`
XMLName xml.Name `xml:"rss"`
Channel rssChan `xml:"channel"`
}

type rssChan struct {
Expand Down
10 changes: 5 additions & 5 deletions pkg/daemon/accept_queue_bug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import (
// to call Accept and the queue has filled to AcceptQueueLen=64), the
// SYN handler at pkg/daemon/daemon.go:1841 currently:
//
// 1. Sends the SYN-ACK back to the dialer
// 2. Marks the connection StateEstablished
// 3. Tries to push to AcceptCh
// 4. On full: hits the `default` branch, sends a RST, removes the
// Connection, logs WARN
// 1. Sends the SYN-ACK back to the dialer
// 2. Marks the connection StateEstablished
// 3. Tries to push to AcceptCh
// 4. On full: hits the `default` branch, sends a RST, removes the
// Connection, logs WARN
//
// The RST is good — peer learns immediately. But:
// - No Daemon-level counter is incremented (no AcceptQueueDrops)
Expand Down
2 changes: 1 addition & 1 deletion pkg/daemon/beacon_discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
type fakeRegistry struct {
mu sync.Mutex
beacons []string
failNext int // if >0, the next N Send() calls error
failNext int // if >0, the next N Send() calls error
calls atomic.Int64
lastError error
}
Expand Down
16 changes: 8 additions & 8 deletions pkg/daemon/beacon_select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,15 @@ func TestPickBeaconStableAcrossSeparateListInstances(t *testing.T) {
// are kept because they may resolve to public IPs.
func TestFilterUnreachableDropsPrivateAndLoopback(t *testing.T) {
in := []string{
"34.71.57.205:9001", // public — kept
"10.128.0.78:9001", // private RFC1918 — dropped
"192.168.1.5:9001", // private RFC1918 — dropped
"172.16.0.5:9001", // private RFC1918 — dropped
"127.0.0.1:9001", // loopback — dropped
"169.254.1.1:9001", // link-local — dropped
"0.0.0.0:9001", // unspecified — dropped
"34.71.57.205:9001", // public — kept
"10.128.0.78:9001", // private RFC1918 — dropped
"192.168.1.5:9001", // private RFC1918 — dropped
"172.16.0.5:9001", // private RFC1918 — dropped
"127.0.0.1:9001", // loopback — dropped
"169.254.1.1:9001", // link-local — dropped
"0.0.0.0:9001", // unspecified — dropped
"beacon.example.com:9001", // DNS hostname — kept
"8.8.8.8:9001", // public — kept
"8.8.8.8:9001", // public — kept
}
got := filterUnreachable(in)
want := []string{
Expand Down
4 changes: 2 additions & 2 deletions pkg/daemon/ca_growth_abc_cap_bug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ func TestCAGrowthCapsIncrementAtSMSS(t *testing.T) {
// SMSS*SMSS/cwnd = 4096*4096/40960 = 409.
// Bug: SMSS*bytes_acked/cwnd = 4096*8192/40960 = 819.
const (
wantIncrement = MaxSegmentSize * MaxSegmentSize / initialCongWin // 409
wantIncrement = MaxSegmentSize * MaxSegmentSize / initialCongWin // 409
bugIncrement = MaxSegmentSize * (2 * MaxSegmentSize) / initialCongWin // 819
wantCongWin = initialCongWin + wantIncrement // 41369
wantCongWin = initialCongWin + wantIncrement // 41369
)
if c.CongWin != wantCongWin {
t.Errorf("CA growth with bytes_acked=2*SMSS: CongWin=%d, want %d "+
Expand Down
12 changes: 6 additions & 6 deletions pkg/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ const (

// Dial and retransmission constants.
const (
DialDirectRetries = 3 // direct connection attempts before relay
DialMaxRetries = 7 // total attempts (direct + relay). 3 direct + 4 relay. With DialInitialRTO=250ms exponential-backoff capped at DialMaxRTO=8s, the relay phase is ~7.75s — covers cold-start handshake (key_exchange + flushPending + SYN/SYN-ACK round trip) for typical peers while keeping bad dials from blocking longer than the user's --timeout. The probe-and-adapt machinery (see srttHistory below) will let us shorten this for peers we've successfully dialed before.
DialInitialRTO = 250 * time.Millisecond // initial SYN retransmission timeout. Lowered from 1s — modern relay RTT is <200ms; waiting a full second before assuming loss makes cold dials feel like a stall. Three direct retries with exponential backoff (250→500→1000) still cover up to 1.75s of jitter before flipping to relay; that's plenty for an unhealthy direct path while letting the common case (peer is reachable, single retry needed) feel snappy.
DialMaxRTO = 8 * time.Second // max backoff for SYN retransmission
DialDirectRetries = 3 // direct connection attempts before relay
DialMaxRetries = 7 // total attempts (direct + relay). 3 direct + 4 relay. With DialInitialRTO=250ms exponential-backoff capped at DialMaxRTO=8s, the relay phase is ~7.75s — covers cold-start handshake (key_exchange + flushPending + SYN/SYN-ACK round trip) for typical peers while keeping bad dials from blocking longer than the user's --timeout. The probe-and-adapt machinery (see srttHistory below) will let us shorten this for peers we've successfully dialed before.
DialInitialRTO = 250 * time.Millisecond // initial SYN retransmission timeout. Lowered from 1s — modern relay RTT is <200ms; waiting a full second before assuming loss makes cold dials feel like a stall. Three direct retries with exponential backoff (250→500→1000) still cover up to 1.75s of jitter before flipping to relay; that's plenty for an unhealthy direct path while letting the common case (peer is reachable, single retry needed) feel snappy.
DialMaxRTO = 8 * time.Second // max backoff for SYN retransmission
DialCheckInterval = 10 * time.Millisecond // poll interval for state changes during dial
RetxCheckInterval = 100 * time.Millisecond // retransmission check ticker
MaxRetxAttempts = 8 // abandon connection after this many retransmissions
Expand Down Expand Up @@ -3368,8 +3368,8 @@ func (d *Daemon) hostnameCachePath() string {

// hostnameCacheDisk is the on-disk format for the hostname cache.
type hostnameCacheDisk struct {
SavedAt time.Time `json:"saved_at"`
Hostnames map[string]hostnameCacheDiskEntry `json:"hostnames"`
SavedAt time.Time `json:"saved_at"`
Hostnames map[string]hostnameCacheDiskEntry `json:"hostnames"`
}

type hostnameCacheDiskEntry struct {
Expand Down
2 changes: 1 addition & 1 deletion pkg/daemon/daemon_ipc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func TestHandleBindDoubleBindSendsError(t *testing.T) {
}
ic, client := newIPCTestConn(t)
reply := runHandler(t, client, func() { s.handleBind(ic, []byte{0x23, 0x28}) }) // port 9000
assertErrorReply(t, reply, "port") // "already bound" or similar
assertErrorReply(t, reply, "port") // "already bound" or similar
}

// --- handleDial ---
Expand Down
2 changes: 1 addition & 1 deletion pkg/daemon/daemon_networkipc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ func TestHandleNetworkRespondInviteNoInviteSendsError(t *testing.T) {
payload := make([]byte, 4)
payload[0] = SubNetworkRespondInvite
binary.BigEndian.PutUint16(payload[1:3], 0xBEEF) // non-existent network
payload[3] = 1 // accept=true
payload[3] = 1 // accept=true

reply := runHandler(t, client, func() { s.handleNetwork(ic, payload) })
// Either registry rejects or reply is OK — the code path exercises the
Expand Down
10 changes: 5 additions & 5 deletions pkg/daemon/dial_precancelled_ctx_bug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import (
// request, or an upstream timeout fired during request queueing),
// the daemon still:
//
// 1. Calls ensureTunnel (potentially up to 30 s blocked on a
// slow registry — see iter 13 audit notes)
// 2. Allocates an ephemeral port
// 3. Creates a Connection in StateSynSent
// 4. Sends a SYN over the tunnel to the peer
// 1. Calls ensureTunnel (potentially up to 30 s blocked on a
// slow registry — see iter 13 audit notes)
// 2. Allocates an ephemeral port
// 3. Creates a Connection in StateSynSent
// 4. Sends a SYN over the tunnel to the peer
//
// Only AFTER all of that does the for-loop's ctx.Done case fire.
// The peer received a phantom SYN they'll respond to (SYN-ACK)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ func TestFreshFastRecoveryPerDupAckInflation(t *testing.T) {
now := time.Now()
c.RetxMu.Lock()
c.LastAck = seqA
c.DupAckCount = 3 // third dup ACK just fired
c.InRecovery = true // entered by DupAckCount==3 path
c.FastRecovery = true // fast retransmit entered this episode
c.RecoveryPoint = sendSeq // set to sendSeq by DupAckCount==3 path
c.SSThresh = 2 * MaxSegmentSize // halved: max(3*MSS/2, 2*MSS) = 2*MSS
c.CongWin = c.SSThresh + 3*MaxSegmentSize // = 5*MSS = 20480
c.DupAckCount = 3 // third dup ACK just fired
c.InRecovery = true // entered by DupAckCount==3 path
c.FastRecovery = true // fast retransmit entered this episode
c.RecoveryPoint = sendSeq // set to sendSeq by DupAckCount==3 path
c.SSThresh = 2 * MaxSegmentSize // halved: max(3*MSS/2, 2*MSS) = 2*MSS
c.CongWin = c.SSThresh + 3*MaxSegmentSize // = 5*MSS = 20480
c.Unacked = []*retxEntry{
{seq: seqB, data: make([]byte, MaxSegmentSize), attempts: 2, sentAt: now}, // fast-retransmitted
{seq: seqC, data: make([]byte, MaxSegmentSize), attempts: 1, sentAt: now},
Expand Down
8 changes: 4 additions & 4 deletions pkg/daemon/dup_ack_in_recovery_ssthresh_halving_bug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ func TestDupAckFastRetransmitInRecoveryDoesNotRehalveSSThresh(t *testing.T) {
// SSThresh halved once, CongWin = InitialCongWin, InRecovery = true.
conn.RetxMu.Lock()
conn.LastAck = seqA
conn.CongWin = InitialCongWin // timeout set this (10*MSS)
conn.SSThresh = 10 * MaxSegmentSize // timeout halved from 20*MSS → 10*MSS
conn.DupAckCount = 0 // timeout reset this (iter-51)
conn.InRecovery = true // timeout set this
conn.CongWin = InitialCongWin // timeout set this (10*MSS)
conn.SSThresh = 10 * MaxSegmentSize // timeout halved from 20*MSS → 10*MSS
conn.DupAckCount = 0 // timeout reset this (iter-51)
conn.InRecovery = true // timeout set this
conn.RecoveryPoint = seqA + MaxSegmentSize
conn.RTO = InitialRTO
conn.Unacked = []*retxEntry{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestAdditionalDupAckInTimeoutRecoveryDoesNotInflateConn(t *testing.T) {
conn.RetxStop = make(chan struct{})

const (
seqA = uint32(1000)
seqA = uint32(1000)
ssthreshAfterTimeout = 5 * MaxSegmentSize
)

Expand All @@ -70,7 +70,7 @@ func TestAdditionalDupAckInTimeoutRecoveryDoesNotInflateConn(t *testing.T) {

conn.RetxMu.Lock()
conn.LastAck = seqA
conn.CongWin = MaxSegmentSize // post-timeout: 1 SMSS
conn.CongWin = MaxSegmentSize // post-timeout: 1 SMSS
conn.SSThresh = ssthreshAfterTimeout
conn.DupAckCount = 0
conn.InRecovery = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestDupAckInTimeoutRecoveryDoesNotReinflateConn(t *testing.T) {
conn.RetxStop = make(chan struct{})

const (
seqA = uint32(1000)
seqA = uint32(1000)
ssthreshAfterTimeout = 5 * MaxSegmentSize // 20480 — timeout halved from 10*MSS
)

Expand All @@ -82,11 +82,11 @@ func TestDupAckInTimeoutRecoveryDoesNotReinflateConn(t *testing.T) {

conn.RetxMu.Lock()
conn.LastAck = seqA
conn.CongWin = MaxSegmentSize // RFC 5681 §3.1: post-timeout cwnd = 1 SMSS
conn.CongWin = MaxSegmentSize // RFC 5681 §3.1: post-timeout cwnd = 1 SMSS
conn.SSThresh = ssthreshAfterTimeout
conn.DupAckCount = 0
conn.InRecovery = true // set by the retransmission timeout
conn.FastRecovery = false // cleared by retransmitUnacked
conn.InRecovery = true // set by the retransmission timeout
conn.FastRecovery = false // cleared by retransmitUnacked
conn.RecoveryPoint = seqA + MaxSegmentSize // = SendSeq (no new data)
conn.RTO = InitialRTO
conn.Unacked = []*retxEntry{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestNewEpisodeDupAcksInRecoveryHalveSSThresh(t *testing.T) {

const (
seqA = uint32(1000)
seqB = seqA + MaxSegmentSize // 5096 — timeout-retransmitted, in Unacked
seqB = seqA + MaxSegmentSize // 5096 — timeout-retransmitted, in Unacked
seqD = seqA + 2*MaxSegmentSize // 9192 — RecoveryPoint from timeout
seqE = seqA + 3*MaxSegmentSize // 13288 — new data, in Unacked
ssthreshAfterTimeout = 5 * MaxSegmentSize // 20480 — halved from 10*MSS by timeout
Expand All @@ -92,12 +92,12 @@ func TestNewEpisodeDupAcksInRecoveryHalveSSThresh(t *testing.T) {

conn.RetxMu.Lock()
conn.LastAck = seqA
conn.CongWin = MaxSegmentSize // RFC 5681 §3.1: post-timeout cwnd = 1 SMSS
conn.CongWin = MaxSegmentSize // RFC 5681 §3.1: post-timeout cwnd = 1 SMSS
conn.SSThresh = ssthreshAfterTimeout // 5*MSS, set by the retransmission timeout
conn.DupAckCount = 0
conn.InRecovery = true // timeout set InRecovery=true
conn.FastRecovery = false // cleared by timeout
conn.RecoveryPoint = seqD // timeout's recovery window ends at seqD
conn.InRecovery = true // timeout set InRecovery=true
conn.FastRecovery = false // cleared by timeout
conn.RecoveryPoint = seqD // timeout's recovery window ends at seqD
conn.RTO = InitialRTO
now := time.Now()
conn.Unacked = []*retxEntry{
Expand Down
Loading
Loading