Skip to content

Commit 79cfab4

Browse files
committed
Fix email feature: config loading, flag forwarding, IPC rename
pilotctl daemon start was not reading email from config.json and not forwarding --email to the daemon subprocess, making v1.4.0 unusable for the mandatory email flow. Changes: - Fix pilotctl to read email from config.json and forward --email flag - Rename DaemonInfo.Owner to Email, IPC key owner→email - Add --email to install.sh (LaunchAgent, systemd, config, prompts) - Rename -owner to -email across all docs, examples, blog posts - Update tests for Email field rename
1 parent f98b274 commit 79cfab4

106 files changed

Lines changed: 266 additions & 165 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<img src="https://img.shields.io/badge/lang-Go-00ADD8?logo=go&logoColor=white" alt="Go">
3030
<img src="https://img.shields.io/badge/deps-zero-brightgreen" alt="Zero Dependencies">
3131
<img src="https://img.shields.io/badge/encryption-AES--256--GCM-blueviolet" alt="Encryption">
32-
<img src="https://img.shields.io/badge/tests-202%20pass-success" alt="Tests">
32+
<img src="https://img.shields.io/badge/tests-683%20pass-success" alt="Tests">
3333
<a href="https://www.ietf.org/archive/id/draft-teodor-pilot-protocol-00.html"><img src="https://img.shields.io/badge/IETF-Internet--Draft-blue" alt="IETF Internet-Draft"></a>
3434
<img src="https://img.shields.io/badge/license-AGPL--3.0-blue" alt="License">
3535
<img src="https://polo.pilotprotocol.network/api/badge/nodes" alt="Online Nodes">
@@ -225,7 +225,7 @@ A public demo agent (`agent-alpha`) is running on the network with auto-accept e
225225
curl -fsSL https://pilotprotocol.network/install.sh | sh
226226

227227
# 2. Start the daemon
228-
pilotctl daemon start --hostname my-agent
228+
pilotctl daemon start --hostname my-agent --email user@example.com
229229

230230
# 3. Request trust (auto-approved within seconds)
231231
pilotctl handshake agent-alpha "hello"
@@ -255,10 +255,10 @@ pilotctl bench agent-alpha
255255
curl -fsSL https://pilotprotocol.network/install.sh | sh
256256
```
257257

258-
Set a hostname during install:
258+
Set a hostname and email during install:
259259

260260
```bash
261-
curl -fsSL https://pilotprotocol.network/install.sh | PILOT_HOSTNAME=my-agent sh
261+
curl -fsSL https://pilotprotocol.network/install.sh | PILOT_EMAIL=user@example.com PILOT_HOSTNAME=my-agent sh
262262
```
263263

264264
<details>
@@ -293,7 +293,7 @@ See the [Python SDK documentation](https://pilotprotocol.network/docs/python-sdk
293293
go test -parallel 4 -count=1 ./tests/
294294
```
295295

296-
328 tests pass, 24 skipped (IPv6, platform-specific). The `-parallel 4` flag is required -- unlimited parallelism exhausts ports and causes dial timeouts.
296+
683 tests pass, 26 skipped (IPv6, platform-specific). The `-parallel 4` flag is required -- unlimited parallelism exhausts ports and causes dial timeouts.
297297

298298
---
299299

cmd/pilotctl/main.go

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,14 @@ Bootstrap:
338338
pilotctl config [--set key=value]
339339
340340
Daemon lifecycle:
341-
pilotctl daemon start [--config <path>] [--registry <addr>] [--beacon <addr>] [--webhook <url>]
341+
pilotctl daemon start [--config <path>] [--registry <addr>] [--beacon <addr>] [--email <addr>] [--webhook <url>]
342342
pilotctl daemon stop
343343
pilotctl daemon status
344344
345345
Registry commands:
346346
pilotctl register [listen_addr]
347347
pilotctl lookup <node_id>
348-
pilotctl rotate-key <node_id> <owner>
348+
pilotctl rotate-key <node_id> <email>
349349
pilotctl set-public
350350
pilotctl set-private
351351
pilotctl deregister
@@ -710,7 +710,7 @@ func cmdContext() {
710710
"returns": "current configuration as JSON",
711711
},
712712
"daemon start": map[string]interface{}{
713-
"args": []string{"[--config <path>]", "[--registry <addr>]", "[--beacon <addr>]", "[--listen <addr>]", "[--identity <path>]", "[--owner <owner>]", "[--hostname <name>]", "[--log-level <level>]", "[--log-format <fmt>]", "[--public]", "[--foreground]", "[--no-encrypt]", "[--socket <path>]", "[--webhook <url>]"},
713+
"args": []string{"[--config <path>]", "[--registry <addr>]", "[--beacon <addr>]", "[--listen <addr>]", "[--identity <path>]", "[--email <addr>]", "[--hostname <name>]", "[--log-level <level>]", "[--log-format <fmt>]", "[--public]", "[--foreground]", "[--no-encrypt]", "[--socket <path>]", "[--webhook <url>]"},
714714
"description": "Start the daemon as a background process. Blocks until registered, then prints status and exits",
715715
"returns": "node_id, address, pid, socket, hostname, log_file",
716716
},
@@ -890,8 +890,8 @@ func cmdContext() {
890890
"returns": "network_id, message",
891891
},
892892
"rotate-key": map[string]interface{}{
893-
"args": []string{"<node_id>", "<owner>"},
894-
"description": "Rotate keypair via owner recovery",
893+
"args": []string{"<node_id>", "<email>"},
894+
"description": "Rotate keypair via email recovery",
895895
"returns": "node_id, new public_key",
896896
},
897897
"set-public": map[string]interface{}{
@@ -1030,7 +1030,16 @@ func cmdDaemonStart(args []string) {
10301030
if identityPath == "" {
10311031
identityPath = configDir() + "/identity.json"
10321032
}
1033+
email := flagString(flags, "email", "")
10331034
owner := flagString(flags, "owner", "")
1035+
if email == "" && owner != "" {
1036+
email = owner // backward compat: -owner as fallback for -email
1037+
}
1038+
if email == "" {
1039+
if e, ok := cfg["email"].(string); ok {
1040+
email = e
1041+
}
1042+
}
10341043
configFile := flagString(flags, "config", "")
10351044
logLevel := flagString(flags, "log-level", "info")
10361045
logFormat := flagString(flags, "log-format", "text")
@@ -1045,7 +1054,7 @@ func cmdDaemonStart(args []string) {
10451054
// If --foreground, run in-process
10461055
if flagBool(flags, "foreground") {
10471056
runDaemonForeground(configFile, registryAddr, beaconAddr, listenAddr,
1048-
socketPath, encrypt, identityPath, owner, hostname, logLevel, logFormat, public, webhookURL)
1057+
socketPath, encrypt, identityPath, email, hostname, logLevel, logFormat, public, webhookURL)
10491058
return
10501059
}
10511060

@@ -1074,8 +1083,8 @@ func cmdDaemonStart(args []string) {
10741083
if !encrypt {
10751084
daemonArgs = append(daemonArgs, "--no-encrypt")
10761085
}
1077-
if owner != "" {
1078-
daemonArgs = append(daemonArgs, "--owner", owner)
1086+
if email != "" {
1087+
daemonArgs = append(daemonArgs, "--email", email)
10791088
}
10801089
if hostname != "" {
10811090
daemonArgs = append(daemonArgs, "--hostname", hostname)
@@ -1315,7 +1324,11 @@ func runDaemonInternal(args []string) {
13151324
listenAddr := flagString(flags, "listen", ":0")
13161325
socketPath := flagString(flags, "socket", defaultSocket)
13171326
identityPath := flagString(flags, "identity", "")
1327+
email := flagString(flags, "email", "")
13181328
owner := flagString(flags, "owner", "")
1329+
if email == "" && owner != "" {
1330+
email = owner
1331+
}
13191332
hostname := flagString(flags, "hostname", "")
13201333
logLevel := flagString(flags, "log-level", "info")
13211334
logFormat := flagString(flags, "log-format", "text")
@@ -1325,11 +1338,11 @@ func runDaemonInternal(args []string) {
13251338
webhookURL := flagString(flags, "webhook", "")
13261339

13271340
runDaemonForeground(configFile, registryAddr, beaconAddr, listenAddr,
1328-
socketPath, encrypt, identityPath, owner, hostname, logLevel, logFormat, public, webhookURL)
1341+
socketPath, encrypt, identityPath, email, hostname, logLevel, logFormat, public, webhookURL)
13291342
}
13301343

13311344
func runDaemonForeground(configFile, registryAddr, beaconAddr, listenAddr,
1332-
socketPath string, encrypt bool, identityPath, owner, hostname,
1345+
socketPath string, encrypt bool, identityPath, email, hostname,
13331346
logLevel, logFormat string, public bool, webhookURL string) {
13341347

13351348
if configFile != "" {
@@ -1360,7 +1373,7 @@ func runDaemonForeground(configFile, registryAddr, beaconAddr, listenAddr,
13601373
SocketPath: socketPath,
13611374
Encrypt: encrypt,
13621375
IdentityPath: identityPath,
1363-
Owner: owner,
1376+
Email: email,
13641377
Public: public,
13651378
Hostname: hostname,
13661379
WebhookURL: webhookURL,
@@ -1660,13 +1673,13 @@ func cmdLookup(args []string) {
16601673

16611674
func cmdRotateKey(args []string) {
16621675
if len(args) < 2 {
1663-
fatalCode("invalid_argument", "usage: pilotctl rotate-key <node_id> <owner>")
1676+
fatalCode("invalid_argument", "usage: pilotctl rotate-key <node_id> <email>")
16641677
}
16651678
nodeID := parseNodeID(args[0])
1666-
owner := args[1]
1679+
email := args[1]
16671680
rc := connectRegistry()
16681681
defer rc.Close()
1669-
resp, err := rc.RotateKey(nodeID, "", owner)
1682+
resp, err := rc.RotateKey(nodeID, "", email)
16701683
if err != nil {
16711684
fatalCode("connection_failed", "rotate-key: %v", err)
16721685
}
@@ -3407,8 +3420,8 @@ func cmdInfo() {
34073420
} else {
34083421
fmt.Printf(" Identity: ephemeral (not persisted)\n")
34093422
}
3410-
if owner, ok := info["owner"].(string); ok && owner != "" {
3411-
fmt.Printf(" Owner: %s\n", owner)
3423+
if email, ok := info["email"].(string); ok && email != "" {
3424+
fmt.Printf(" Email: %s\n", email)
34123425
}
34133426
fmt.Printf(" Traffic: %s sent / %s recv\n", formatBytes(bytesSent), formatBytes(bytesRecv))
34143427
fmt.Printf(" Packets: %d sent / %d recv\n", pktsSent, pktsRecv)

configs/daemon.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"socket": "/tmp/pilot.sock",
66
"encrypt": true,
77
"identity": "/var/lib/pilot/identity.json",
8-
"owner": "",
8+
"email": "",
99
"log-level": "info",
1010
"log-format": "text"
1111
}

docs/SKILLS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ Returns: current configuration as JSON
111111

112112
```bash
113113
pilotctl daemon start [--registry <addr>] [--beacon <addr>] [--listen <addr>] \
114-
[--identity <path>] [--owner <owner>] [--hostname <name>] [--public] \
114+
[--identity <path>] [--email <addr>] [--hostname <name>] [--public] \
115115
[--no-encrypt] [--foreground] [--log-level <level>] [--log-format <fmt>] \
116116
[--socket <path>] [--config <path>] [--webhook <url>]
117117
```
@@ -154,7 +154,7 @@ Returns: `running`, `responsive`, `pid`, `pid_file`, `socket`, `node_id`, `addre
154154
pilotctl info
155155
```
156156

157-
Returns: `node_id`, `address`, `hostname`, `uptime_secs`, `connections`, `ports`, `peers`, `encrypt`, `bytes_sent`, `bytes_recv`, identity status, owner, per-connection stats, peer list with encryption status.
157+
Returns: `node_id`, `address`, `hostname`, `uptime_secs`, `connections`, `ports`, `peers`, `encrypt`, `bytes_sent`, `bytes_recv`, identity status, email, per-connection stats, peer list with encryption status.
158158

159159
### Set your hostname
160160

@@ -908,7 +908,7 @@ Returns: `peers` [{`node_id`, `endpoint`, `encrypted`, `authenticated`}], `total
908908

909909
```bash
910910
pilotctl init --registry 34.71.57.205:9000 --beacon 34.71.57.205:9001
911-
pilotctl daemon start --hostname my-agent
911+
pilotctl daemon start --hostname my-agent --email user@example.com
912912
pilotctl enable-tasks # Advertise task execution capability
913913
pilotctl info
914914
```

docs/WHITEPAPER.tex

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ \subsection{Registry}
244244
\item \textbf{Heartbeats.} Nodes ping the Registry periodically to confirm liveness.
245245
\end{itemize}
246246

247-
\textbf{Persistence.} The Registry supports atomic JSON snapshots (\texttt{-store path/to/registry.json}). Every state mutation---node registration, network creation, network join, deregistration, stale-node reaping---triggers a save. The snapshot captures all nodes, networks, membership lists, public keys, owner bindings, trust pairs, and ID counters. On restart, the Registry loads the snapshot and resumes operation with all state intact. Writes use a temporary file with \texttt{os.Rename} for crash safety---the store file is never partially written.
247+
\textbf{Persistence.} The Registry supports atomic JSON snapshots (\texttt{-store path/to/registry.json}). Every state mutation---node registration, network creation, network join, deregistration, stale-node reaping---triggers a save. The snapshot captures all nodes, networks, membership lists, public keys, email bindings, trust pairs, and ID counters. On restart, the Registry loads the snapshot and resumes operation with all state intact. Writes use a temporary file with \texttt{os.Rename} for crash safety---the store file is never partially written.
248248

249249
\textbf{TLS.} The Registry supports TLS transport (\texttt{-tls-cert}, \texttt{-tls-key}). When configured, all control-plane traffic---registrations, lookups, heartbeats---is encrypted in transit.
250250

@@ -274,7 +274,7 @@ \subsection{Daemon}
274274
\begin{enumerate}
275275
\item Discovers its public endpoint via the Beacon
276276
\item Loads persisted Ed25519 identity (if configured)
277-
\item Registers with the Registry, receiving a virtual address (or reclaiming an existing one via identity or owner)
277+
\item Registers with the Registry, receiving a virtual address (or reclaiming an existing one via identity or email)
278278
\item Generates an ephemeral X25519 keypair for tunnel encryption (enabled by default)
279279
\item Opens a Unix domain socket (mode 0600) for local drivers to connect
280280
\item Starts the trust handshake service on port~444 (if identity available)
@@ -456,18 +456,18 @@ \subsection{pilotctl}
456456
pilotctl lookup 3
457457
pilotctl deregister
458458

459-
# Key rotation via signature or owner recovery
459+
# Key rotation via signature or email recovery
460460
pilotctl rotate-key 3 agent-a@pilot
461461
\end{lstlisting}
462462

463-
The \texttt{info} command provides a comprehensive view of daemon state, including identity status (ephemeral vs.\ persistent Ed25519), owner binding, encryption mode, authenticated peer count, and per-connection statistics (bytes, segments, retransmissions, SACK blocks, duplicate ACKs, congestion window, in-flight data, SRTT, RTTVAR, and recovery status). The \texttt{peers} command shows each peer's real endpoint, whether the tunnel is encrypted, and whether the key exchange was authenticated with Ed25519 signatures.
463+
The \texttt{info} command provides a comprehensive view of daemon state, including identity status (ephemeral vs.\ persistent Ed25519), email binding, encryption mode, authenticated peer count, and per-connection statistics (bytes, segments, retransmissions, SACK blocks, duplicate ACKs, congestion window, in-flight data, SRTT, RTTVAR, and recovery status). The \texttt{peers} command shows each peer's real endpoint, whether the tunnel is encrypted, and whether the key exchange was authenticated with Ed25519 signatures.
464464

465465
% ============================================================
466466
\section{Security Model}
467467

468468
\textbf{Identity.} Each node receives an Ed25519 keypair from the Registry upon registration. The private key is the node's identity token. The Registry holds all public keys and can verify identity. Identities can be persisted to disk (\texttt{-identity path/to/identity.json}) so that a node retains its keypair and virtual address across daemon restarts. On restart, the daemon re-registers with the stored public key and the registry recognizes the node, preserving its address and network memberships.
469469

470-
\textbf{Owner binding and key rotation.} Nodes can be bound to an owner identifier (typically an email) via the \texttt{-owner} flag. This enables key rotation recovery: if a node's private key is compromised or lost, the owner can request a new keypair from the registry. Key rotation supports two authentication paths: (1)~signature-based, where the node signs a challenge (\texttt{rotate:<node\_id>}) with its current Ed25519 private key, proving possession; or (2)~owner-based, where the owner identifier is matched against the registry's records. After rotation, the registry issues a fresh Ed25519 keypair while preserving the node's ID and network memberships. Owner binding also enables reclaiming a deregistered node's identity---re-registering with the same owner email recovers the original node~ID.
470+
\textbf{Email binding and key rotation.} Nodes can be bound to an email address via the \texttt{-email} flag. This enables key rotation recovery: if a node's private key is compromised or lost, the email holder can request a new keypair from the registry. Key rotation supports two authentication paths: (1)~signature-based, where the node signs a challenge (\texttt{rotate:<node\_id>}) with its current Ed25519 private key, proving possession; or (2)~email-based, where the email address is matched against the registry's records. After rotation, the registry issues a fresh Ed25519 keypair while preserving the node's ID and network memberships. Email binding also enables reclaiming a deregistered node's identity---re-registering with the same email recovers the original node~ID.
471471

472472
\textbf{Trust boundaries.} Network membership is the trust boundary. Joining a network requires satisfying its rules (open enrollment, token verification, or member invitation). Once inside a network, communication is unrestricted. This is a deliberate design choice: complex per-connection ACLs are replaced by simple group membership.
473473

@@ -779,8 +779,8 @@ \subsection{Summary}
779779
Nagle coalescing & Local & Pass (100 writes in 4\,ms) \\
780780
Tunnel encryption + backward compat & Local & Pass (PILK/PILS/plaintext) \\
781781
Authenticated key exchange & Local & Pass (Ed25519 signed) \\
782-
Identity persistence + key rotation & Local & Pass (save/reload, sig/owner rotate) \\
783-
Owner re-registration & Local & Pass (reclaim ID after deregister) \\
782+
Identity persistence + key rotation & Local & Pass (save/reload, sig/email rotate) \\
783+
Email re-registration & Local & Pass (reclaim ID after deregister) \\
784784
\midrule
785785
Handshake mutual auto-approve & Local & Pass (both request $\rightarrow$ auto) \\
786786
Handshake approve/reject & Local & Pass (pending $\rightarrow$ approve/reject) \\

docs/enterprise-readiness-report.tex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,11 @@ \subsection{Identity}
132132

133133
\textbf{Signed mutations.} All registry write operations routed through the daemon (set-hostname, set-visibility, deregister) are signed with the node's Ed25519 private key. The registry verifies signatures before applying mutations, preventing spoofed requests.
134134

135-
\textbf{Key rotation.} The \texttt{rotate\_key} command supports two authentication paths: signature-based (proving possession of the current key) and owner-based (matching an email identifier). After rotation, the registry issues a fresh keypair while preserving the node's ID and network memberships.
135+
\textbf{Key rotation.} The \texttt{rotate\_key} command supports two authentication paths: signature-based (proving possession of the current key) and email-based (matching the registered email address). After rotation, the registry issues a fresh keypair while preserving the node's ID and network memberships.
136136

137137
\textbf{TLS cert pinning.} The registry client supports TLS with certificate pinning (\texttt{DialTLSPinned}), preventing man-in-the-middle attacks on the control plane.
138138

139-
\textbf{Owner binding.} Nodes can be bound to an owner identifier (typically an email) via the \texttt{-owner} flag. This enables key recovery and re-registration after deregistration.
139+
\textbf{Email binding.} Nodes can be bound to an email address via the \texttt{-email} flag. This enables key recovery and re-registration after deregistration.
140140

141141
\textbf{Authenticated key exchange.} When a daemon has a persisted Ed25519 identity, the tunnel key exchange is upgraded from anonymous (\texttt{PILK}) to authenticated (\texttt{PILA}). The signature proves the X25519 ephemeral key belongs to the claimed identity, preventing man-in-the-middle substitution.
142142

@@ -203,7 +203,7 @@ \subsection{Identity Gaps}
203203
No identity expiry or forced rotation & Medium & Compromised keys live forever \\
204204
No workload attestation & Medium & Cannot verify \emph{what} a process is \\
205205
No trust domain concept & Medium & No federation boundary \\
206-
Owner field is unverified & Low & Email is informational only \\
206+
Email field is unverified & Low & Email is informational only \\
207207
\bottomrule
208208
\end{tabular}
209209
\caption{Identity gaps.}
@@ -494,7 +494,7 @@ \section{Standards Alignment Matrix}
494494
\multicolumn{4}{@{}l}{\textbf{CSA ATF Elements}} \\
495495
\midrule
496496
Progressive Autonomy & Polo score (reputation system) & --- & Aligned \\
497-
Identity Binding & Ed25519 to node, owner field & OIDC/SPIFFE binding (P5), CA binding (P4) & Partial \\
497+
Identity Binding & Ed25519 to node, email field & OIDC/SPIFFE binding (P5), CA binding (P4) & Partial \\
498498
Context Propagation & Handshake justification, tags & Tag-based policies (P3), external identity context (P5) & Partial \\
499499
Revocation \& Quarantine & Bilateral untrust with tunnel teardown & Cascading revocation (P3), block list (P3), cert revocation (P4) & Gap \\
500500
\midrule

docs/media/pilot-demo.gif

42.3 KB
Loading

examples/cli/BASIC_USAGE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ A practical reference for using `pilotctl` to communicate with other nodes on th
1010
curl -fsSL https://pilotprotocol.network/install.sh | sh
1111
```
1212

13-
Optionally set a hostname during install:
13+
Set your email and hostname during install:
1414
```bash
15-
curl -fsSL https://pilotprotocol.network/install.sh | PILOT_HOSTNAME=my-node sh
15+
curl -fsSL https://pilotprotocol.network/install.sh | PILOT_EMAIL=user@example.com PILOT_HOSTNAME=my-node sh
1616
```
1717

1818
### Initialize Configuration

examples/go/config/daemon.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"encrypt": true,
77
"registry-tls": false,
88
"identity": "/var/lib/pilot/identity.json",
9-
"owner": "",
9+
"email": "",
1010
"keepalive": "30s",
1111
"idle-timeout": "120s",
1212
"syn-rate-limit": 100,

examples/python_sdk/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Python script → pilotprotocol (ctypes) → libpilot.so → daemon
2929

3030
3. **Start the Pilot Protocol daemon:**
3131
```bash
32-
pilotctl daemon start --hostname my-agent
32+
pilotctl daemon start --hostname my-agent --email user@example.com
3333
```
3434

3535
4. **For multi-agent examples, establish trust:**

0 commit comments

Comments
 (0)