Skip to content

Commit b423131

Browse files
committed
Merge v1.9.1-tunnel-reliability → main (v1.9.1-rc1)
Tunnel reliability (RFC 5681/6582 congestion control): - ~100 TDD iterations fixing fast retransmit, partial ACK deflation, SSThresh floor, per-dup-ACK inflation, SACK accounting, timeout recovery, RTT measurement, zero-window handling, and window signaling - 7 performance benchmark files; BenchmarkRecoveryTime: 1.37s → 16ms (84×) Auto-updater (all previously silent bugs): - Binary name mismatch fixed: daemon→pilot-daemon, gateway→pilot-gateway, updater→pilot-updater via archiveToInstall map - Self-exit on updater replacement: os.Exit(0) → launchd/systemd restarts with new binary; recoverPendingRestart() catches missed daemon restarts - Fresh install now treated as v0.0.0 (was fatal error) - writeFileSync: fsync before exit prevents re-download loops - exitFn injectable for tests pilotctl CLI restructure: - task, gateway, set-tags, clear-tags, enable-tasks, disable-tasks gated behind 'pilotctl extras' with clear redirect errors - set-webhook stays in core (agent-facing) - pilotctl context v1.3: commands / pilot_task / pilot_gateway / extras sections install.sh: - Linux daemon systemd unit: Restart=on-failure → Restart=always Skills auto-injection (feat): - pkg/skillinject: fetches inject-manifest.json, reconciles SKILL.md + heartbeat refs into Claude Code, OpenClaw, PicoClaw, OpenHands, Hermes - internal/nodesapi: nodes API client - pilotctl skills: status / paths / check subcommands Integration test suite: 160+ Docker-based scenario tests
2 parents 95054be + 149af97 commit b423131

317 files changed

Lines changed: 39637 additions & 446 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.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,4 @@ tests/integration/local/logs.rerun*
168168
# Untracked sandboxes / personal scratch — never commit
169169
abpn/
170170
scripts/pilot-geo-exporter/
171+
/spoof

cmd/pilotctl/main.go

Lines changed: 328 additions & 164 deletions
Large diffs are not rendered by default.

cmd/pilotctl/skills.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+
package main
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"sort"
11+
"strings"
12+
"time"
13+
14+
"github.com/TeoSlayer/pilotprotocol/pkg/skillinject"
15+
)
16+
17+
// cmdSkills is the user-facing surface for the daemon's auto-installed
18+
// agent skill. It tells the user where the daemon writes the SKILL.md for
19+
// each detected tool, the live state of each path, and (with --paths) just
20+
// the bare paths for shell-friendly use.
21+
//
22+
// Subcommands:
23+
//
24+
// pilotctl skills — alias for `status`
25+
// pilotctl skills status — show per-tool install paths + state
26+
// pilotctl skills paths — print just the install paths
27+
// pilotctl skills check — run one reconcile pass right now
28+
func cmdSkills(args []string) {
29+
sub := "status"
30+
if len(args) > 0 && !strings.HasPrefix(args[0], "--") {
31+
sub = args[0]
32+
args = args[1:]
33+
}
34+
switch sub {
35+
case "status":
36+
cmdSkillsStatus(args)
37+
case "paths":
38+
cmdSkillsPaths(args)
39+
case "check":
40+
cmdSkillsCheck(args)
41+
default:
42+
fatalHint("invalid_argument",
43+
"available: status, paths, check",
44+
"unknown skills subcommand: %s", sub)
45+
}
46+
}
47+
48+
func runTick() (*skillinject.Report, error) {
49+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
50+
defer cancel()
51+
return skillinject.Tick(ctx, skillinject.Config{})
52+
}
53+
54+
// cmdSkillsStatus runs one tick (fetching the manifest + entrypoint over
55+
// HTTPS) and prints what state each managed file is in and what action
56+
// the daemon's next live tick would take.
57+
func cmdSkillsStatus(_ []string) {
58+
report, err := runTick()
59+
if err != nil {
60+
fatalCode("internal", "skills tick: %v", err)
61+
}
62+
63+
if jsonOutput {
64+
out := []map[string]interface{}{}
65+
for _, o := range report.Outcomes {
66+
out = append(out, map[string]interface{}{
67+
"tool": o.Tool,
68+
"kind": string(o.Kind),
69+
"path": o.Path,
70+
"state": string(o.State),
71+
"action": string(o.Action),
72+
"hash": o.Hash,
73+
"err": o.Err,
74+
})
75+
}
76+
output(map[string]interface{}{
77+
"at": report.At,
78+
"outcomes": out,
79+
"skipped": report.Skipped,
80+
})
81+
return
82+
}
83+
84+
fmt.Println("Pilot Protocol skill — install status")
85+
fmt.Println("=====================================")
86+
fmt.Printf("Reconcile cadence: every %s (default), plus once on daemon startup.\n", skillinject.DefaultInterval)
87+
fmt.Println("All paths below are auto-managed by the daemon — edits are reverted on next tick.")
88+
fmt.Println()
89+
90+
if len(report.Outcomes) == 0 {
91+
fmt.Println("No supported agent tools detected on this host.")
92+
fmt.Println("Supported (auto-detected by directory presence):")
93+
fmt.Println(" - Claude Code (~/.claude)")
94+
fmt.Println(" - OpenClaw (~/.openclaw)")
95+
fmt.Println(" - Cursor (~/.cursor)")
96+
fmt.Println(" - OpenHands (~/.openhands)")
97+
fmt.Println(" - Hermes (~/.hermes)")
98+
return
99+
}
100+
101+
// Group outcomes by tool for readable output.
102+
byTool := map[string][]skillinject.Outcome{}
103+
tools := []string{}
104+
for _, o := range report.Outcomes {
105+
if _, seen := byTool[o.Tool]; !seen {
106+
tools = append(tools, o.Tool)
107+
}
108+
byTool[o.Tool] = append(byTool[o.Tool], o)
109+
}
110+
sort.Strings(tools)
111+
112+
for _, tool := range tools {
113+
fmt.Printf("[%s]\n", tool)
114+
for _, o := range byTool[tool] {
115+
label := "skill copy: "
116+
if o.Kind == skillinject.KindMarker {
117+
label = "heartbeat ref: "
118+
}
119+
fmt.Printf(" %s%s\n", label, o.Path)
120+
fmt.Printf(" state=%s next_action=%s\n", o.State, o.Action)
121+
if o.Err != "" {
122+
fmt.Printf(" ERROR: %s\n", o.Err)
123+
}
124+
}
125+
fmt.Println()
126+
}
127+
128+
if len(report.Skipped) > 0 {
129+
fmt.Printf("Not installed (skipped): %s\n", strings.Join(report.Skipped, ", "))
130+
}
131+
}
132+
133+
// cmdSkillsPaths prints just the install paths — one per line, no decoration —
134+
// suitable for shell pipelines (`pilotctl skills paths | xargs ls -la`).
135+
func cmdSkillsPaths(_ []string) {
136+
report, err := runTick()
137+
if err != nil {
138+
fatalCode("internal", "skills tick: %v", err)
139+
}
140+
if jsonOutput {
141+
paths := []string{}
142+
for _, o := range report.Outcomes {
143+
paths = append(paths, o.Path)
144+
}
145+
output(map[string]interface{}{"paths": paths})
146+
return
147+
}
148+
for _, o := range report.Outcomes {
149+
fmt.Println(o.Path)
150+
}
151+
}
152+
153+
// cmdSkillsCheck triggers one reconcile pass right now (instead of waiting
154+
// for the daemon's next tick) and reports what changed. Useful right after
155+
// installing a new agent tool — no need to wait 15 minutes.
156+
func cmdSkillsCheck(_ []string) {
157+
report, err := runTick()
158+
if err != nil {
159+
fatalCode("internal", "skills tick: %v", err)
160+
}
161+
c := report.Counts()
162+
163+
if jsonOutput {
164+
outputOK(map[string]interface{}{
165+
"checked": len(report.Outcomes),
166+
"noops": c[skillinject.ActionNoop],
167+
"creates": c[skillinject.ActionCreate],
168+
"rewrites": c[skillinject.ActionRewrite],
169+
"errors": c[skillinject.ActionError],
170+
"skipped": report.Skipped,
171+
})
172+
return
173+
}
174+
175+
fmt.Printf("Reconcile complete — %d files checked.\n", len(report.Outcomes))
176+
fmt.Printf(" noop: %d\n", c[skillinject.ActionNoop])
177+
fmt.Printf(" create: %d\n", c[skillinject.ActionCreate])
178+
fmt.Printf(" rewrite: %d\n", c[skillinject.ActionRewrite])
179+
if c[skillinject.ActionError] > 0 {
180+
fmt.Printf(" errors: %d (run `pilotctl skills status` for detail)\n", c[skillinject.ActionError])
181+
}
182+
if len(report.Skipped) > 0 {
183+
fmt.Printf("Not installed (skipped): %s\n", strings.Join(report.Skipped, ", "))
184+
}
185+
}
186+
187+
// skillsHomeRel returns a $HOME-relative pretty path for display (purely
188+
// cosmetic). Falls back to the absolute path if HOME isn't resolvable.
189+
func skillsHomeRel(p string) string {
190+
h, err := os.UserHomeDir()
191+
if err != nil {
192+
return p
193+
}
194+
rel, err := filepath.Rel(h, p)
195+
if err != nil || strings.HasPrefix(rel, "..") {
196+
return p
197+
}
198+
return "~/" + rel
199+
}
200+
201+
var _ = skillsHomeRel // reserved for future use; keeps gofmt happy
202+
203+
// printSkillInstallSummary is called from cmdInfo to surface the agent
204+
// skill install paths in the standard daemon diagnostic. Quiet (no header)
205+
// when no agent tools are detected on the host.
206+
func printSkillInstallSummary() {
207+
report, err := runTick()
208+
if err != nil || report == nil || len(report.Outcomes) == 0 {
209+
return
210+
}
211+
// Collapse to one path per tool: prefer the skill copy over the marker.
212+
type entry struct {
213+
Tool, Path string
214+
}
215+
seen := map[string]string{}
216+
order := []string{}
217+
for _, o := range report.Outcomes {
218+
if o.Kind != skillinject.KindSkill {
219+
continue
220+
}
221+
if _, ok := seen[o.Tool]; !ok {
222+
order = append(order, o.Tool)
223+
}
224+
seen[o.Tool] = o.Path
225+
}
226+
if len(order) == 0 {
227+
return
228+
}
229+
fmt.Printf("\nAgent skill installed at:\n")
230+
for _, tool := range order {
231+
fmt.Printf(" %-13s %s\n", tool+":", seen[tool])
232+
}
233+
fmt.Printf(" (auto-managed by daemon — run `pilotctl skills` for full state)\n")
234+
}

cmd/pilotctl/trusted.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/TeoSlayer/pilotprotocol/internal/trustedagents"
9+
)
10+
11+
func cmdTrusted(args []string) {
12+
if len(args) < 1 || args[0] != "list" {
13+
fatalHint("invalid_argument",
14+
"available: pilotctl trusted list",
15+
"usage: pilotctl trusted list")
16+
}
17+
agents := trustedagents.All()
18+
if len(agents) == 0 {
19+
fmt.Println("(no trusted agents — daemon will not auto-accept any handshakes via this path)")
20+
return
21+
}
22+
for _, a := range agents {
23+
fmt.Printf(" %-32s %s node_id=%d\n", a.Hostname, a.Address, a.NodeID)
24+
}
25+
}

install.sh

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,22 @@ fi
143143
TMPDIR=$(mktemp -d)
144144
trap 'rm -rf "$TMPDIR"' EXIT
145145

146-
# Try downloading a release first
147-
# PILOT_RC=1 opts into release candidates (pre-releases)
146+
ARCHIVE="pilot-${OS}-${ARCH}.tar.gz"
147+
148+
# Resolve the latest release tag.
149+
# - Default path uses the unauthenticated /releases/latest/download/ redirect,
150+
# which is not subject to the 60/hr api.github.com rate limit.
151+
# - PILOT_RC=1 still hits the API because pre-releases need the listing endpoint.
148152
if [ "${PILOT_RC:-}" = "1" ]; then
149153
TAG=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases" 2>/dev/null | grep '"tag_name"' | head -1 | cut -d'"' -f4 || true)
150154
else
151-
TAG=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | head -1 | cut -d'"' -f4 || true)
155+
TAG=$(curl -fsSI "https://github.com/${REPO}/releases/latest/download/${ARCHIVE}" 2>/dev/null \
156+
| grep -i '^location:' \
157+
| sed -n 's|.*/releases/download/\([^/]*\)/.*|\1|p' \
158+
| tr -d '\r' | head -1)
152159
fi
153160

154161
if [ -n "$TAG" ]; then
155-
ARCHIVE="pilot-${OS}-${ARCH}.tar.gz"
156162
URL="https://github.com/${REPO}/releases/download/${TAG}/${ARCHIVE}"
157163
CHECKSUMS_URL="https://github.com/${REPO}/releases/download/${TAG}/checksums.txt"
158164
echo "Downloading ${TAG}..."
@@ -196,14 +202,19 @@ if [ -z "$TAG" ]; then
196202
fi
197203
echo "Cloning..."
198204
git clone --depth 1 "https://github.com/${REPO}.git" "$TMPDIR/src" >/dev/null 2>&1
199-
echo "Building daemon..."
200-
CGO_ENABLED=0 go build -o "$TMPDIR/pilot-daemon" "$TMPDIR/src/cmd/daemon"
201-
echo "Building pilotctl..."
202-
CGO_ENABLED=0 go build -o "$TMPDIR/pilotctl" "$TMPDIR/src/cmd/pilotctl"
203-
echo "Building gateway..."
204-
CGO_ENABLED=0 go build -o "$TMPDIR/pilot-gateway" "$TMPDIR/src/cmd/gateway"
205-
echo "Building updater..."
206-
CGO_ENABLED=0 go build -o "$TMPDIR/pilot-updater" "$TMPDIR/src/cmd/updater"
205+
# Build from inside the cloned tree with GOWORK=off so a parent go.work
206+
# in the user's $PWD does not reject the cloned module.
207+
(
208+
cd "$TMPDIR/src"
209+
echo "Building daemon..."
210+
GOWORK=off CGO_ENABLED=0 go build -o "$TMPDIR/pilot-daemon" ./cmd/daemon
211+
echo "Building pilotctl..."
212+
GOWORK=off CGO_ENABLED=0 go build -o "$TMPDIR/pilotctl" ./cmd/pilotctl
213+
echo "Building gateway..."
214+
GOWORK=off CGO_ENABLED=0 go build -o "$TMPDIR/pilot-gateway" ./cmd/gateway
215+
echo "Building updater..."
216+
GOWORK=off CGO_ENABLED=0 go build -o "$TMPDIR/pilot-updater" ./cmd/updater
217+
)
207218
fi
208219

209220
# --- Install binaries to ~/.pilot/bin ---
@@ -309,7 +320,7 @@ ExecStart=${BIN_DIR}/pilot-daemon \\
309320
-identity ${PILOT_DIR}/identity.json \\
310321
-email ${EMAIL} \\
311322
-encrypt ${HOSTNAME_FLAG} ${PUBLIC_FLAG}
312-
Restart=on-failure
323+
Restart=always
313324
RestartSec=5
314325
315326
[Install]
@@ -488,3 +499,30 @@ echo ""
488499
echo " sudo ${BIN_DIR}/pilotctl gateway start --ports 80,3000 <pilot-addr>"
489500
echo " curl http://10.4.0.1:3000/status"
490501
echo ""
502+
echo "Agent skill auto-injection:"
503+
echo ""
504+
echo " The daemon scans every 15 minutes and injects the Pilot Protocol"
505+
echo " skill into installed agent tools. Triggering a first pass right now"
506+
echo " so your agents know about Pilot before the daemon is even started:"
507+
echo ""
508+
if "${BIN_DIR}/pilotctl" skills check 2>&1 | sed 's/^/ /'; then
509+
:
510+
else
511+
echo " (skills check failed — non-fatal; will re-attempt on daemon start)"
512+
fi
513+
echo ""
514+
echo " Per-tool target paths:"
515+
echo " Claude Code ~/.claude/skills/pilot-protocol/SKILL.md"
516+
echo " + heartbeat ref in ~/.claude/CLAUDE.md"
517+
echo " OpenClaw ~/.openclaw/skills/pilot-protocol/SKILL.md"
518+
echo " + heartbeat ref in ~/.openclaw/workspace/AGENTS.md"
519+
echo " PicoClaw ~/.picoclaw/workspace/skills/pilot-protocol/SKILL.md"
520+
echo " + heartbeat ref in ~/.picoclaw/workspace/AGENT.md"
521+
echo " OpenHands ~/.openhands/microagents/pilot-protocol.md (self-heartbeat)"
522+
echo " Hermes ~/.hermes/skills/pilot-protocol/SKILL.md"
523+
echo " + heartbeat ref in ~/.hermes/SOUL.md"
524+
echo ""
525+
echo " Inspect / force a refresh anytime:"
526+
echo " pilotctl skills # status of every install path"
527+
echo " pilotctl skills check # run one reconcile pass right now"
528+
echo ""

0 commit comments

Comments
 (0)