From 8437fc1e4c1954c0f86962f63a5bed96d6009e35 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Tue, 16 Jun 2026 13:57:43 +0000 Subject: [PATCH 1/4] feat(telemetry): consent-gated telemetry client with Ed25519 signing (PILOT-400) Adds a self-contained telemetry client package that emits signed events to a configured endpoint only when consent is active (--telemetry-url non-empty). When the flag is empty (default), the client is a hard no-op: no dial, no buffering, no goroutines. The client signs all requests with the node's Ed25519 identity following the canonical telemetry signing contract (X-Pilot-Timestamp, X-Pilot-Public-Key, X-Pilot-Signature). This matches the telemetry server's internal/sig verification at pilot-protocol/telemetry. Changes: - pkg/telemetry/client.go: consent-gated telemetry client with Signed HTTP POST, lazy disabled init, configurable endpoint - pkg/telemetry/client_test.go: tests for no-op paths, disabled state, SignMessage round-trip verification - cmd/daemon/main.go: --telemetry-url flag (env PILOT_TELEMETRY_URL) - pkg/daemon/daemon.go: TelemetryURL field in Config struct Closes PILOT-400 --- cmd/daemon/main.go | 5 ++ pkg/daemon/daemon.go | 6 ++ pkg/telemetry/client.go | 166 +++++++++++++++++++++++++++++++++++ pkg/telemetry/client_test.go | 88 +++++++++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 pkg/telemetry/client.go create mode 100644 pkg/telemetry/client_test.go diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 735bfffb..e45f5f80 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -36,6 +36,7 @@ import ( "github.com/pilot-protocol/webhook" "github.com/TeoSlayer/pilotprotocol/internal/catalogtrust" + "github.com/TeoSlayer/pilotprotocol/pkg/telemetry" ) var version = "dev" @@ -100,6 +101,9 @@ func main() { logFormat := flag.String("log-format", "text", "log format (text, json)") motdFeedURL := flag.String("motd-feed-url", motd.DefaultFeedURL, "message-of-the-day feed URL (empty to disable); overridden by $PILOT_MOTD_URL") motdInterval := flag.Duration("motd-interval", 0, "message-of-the-day poll interval (default 15m)") + telemetryURL := flag.String("telemetry-url", os.Getenv("PILOT_TELEMETRY_URL"), + "telemetry endpoint URL (empty = consent off, hard no-op). "+ + "Env: PILOT_TELEMETRY_URL. Default: "+telemetry.DefaultEndpoint+".") flag.Parse() if *adminToken == "" { if v := os.Getenv("PILOT_ADMIN_TOKEN"); v != "" { @@ -209,6 +213,7 @@ func main() { CompatTLSTrust: *tlsTrust, MOTDFeedURL: *motdFeedURL, MOTDInterval: *motdInterval, + TelemetryURL: *telemetryURL, }) // L11 plugin lifecycle (T7.1): composition root owns the diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index 493e3031..d10412e4 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -133,6 +133,12 @@ type Config struct { // Feature flags — ablation testing. All default false (current behavior). BeaconRTTProbe bool // probe beacon RTT; override hash pick when >2× slower than best + // Telemetry consent gate. When set to the telemetry endpoint URL, + // the daemon initialises a telemetry client that emits signed events + // (install, usage, view, review). When empty (default), the client + // is a hard no-op: no dial, no buffering, no goroutines. + TelemetryURL string + // Compat-mode transport. Default empty ("" or "udp") = today's // behavior: bind a UDP socket via udpio.Listen. Set "compat" to // dial WSS to BeaconURL instead (for daemons in UDP-blocked diff --git a/pkg/telemetry/client.go b/pkg/telemetry/client.go new file mode 100644 index 00000000..6d473650 --- /dev/null +++ b/pkg/telemetry/client.go @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Package telemetry provides a consent-gated telemetry client that signs +// event POSTs with the node's Ed25519 identity and sends them to a +// configured telemetry endpoint. When the consent flag is off (empty URL), +// the client is a hard no-op: no dial, no buffering, no goroutines. +package telemetry + +import ( + "bytes" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "strconv" + "strings" + "sync" + "time" +) + +// DefaultEndpoint is the production telemetry ingestion URL. +const DefaultEndpoint = "https://telemetry.pilotprotocol.network/v1/events" + +// Canonical signing header names, matching the telemetry server's +// internal/sig contract. +const ( + HeaderTimestamp = "X-Pilot-Timestamp" + HeaderPubKey = "X-Pilot-Public-Key" + HeaderSignature = "X-Pilot-Signature" +) + +// Event is the wire shape sent to the telemetry endpoint. +type Event struct { + EventID string `json:"event_id"` + Kind string `json:"kind"` + TS string `json:"ts"` // RFC3339; empty = server defaults to receive time + NodeID int64 `json:"node_id,omitempty"` + Payload json.RawMessage `json:"payload"` +} + +// Client is a consent-gated telemetry sender. Zero value is a no-op. +type Client struct { + mu sync.Mutex + url string // empty = no-op + nodeID int64 // node ID included in events + sign signFunc // ed25519 signer (set via SetSigner) + pubKeyB string // base64-encoded public key + once sync.Once // lazy init guard + initErr error // capture init failures + disabled bool // true when url is empty +} + +type signFunc func(msg []byte) []byte + +// New creates a consent-gated telemetry client. +// When url is empty the client is a permanent no-op. +func New(url string, nodeID int64) *Client { + return &Client{ + url: strings.TrimSpace(url), + nodeID: nodeID, + disabled: strings.TrimSpace(url) == "", + } +} + +// SetSigner installs the Ed25519 signing function and the corresponding +// base64-encoded public key. Must be called before the first Send call. +// When signer is nil the client stays disabled. +func (c *Client) SetSigner(sign signFunc, pubKeyB64 string) { + c.mu.Lock() + defer c.mu.Unlock() + if sign == nil || pubKeyB64 == "" { + c.sign = nil + c.pubKeyB = "" + return + } + c.sign = sign + c.pubKeyB = pubKeyB64 +} + +// Send POSTs one or more events to the telemetry endpoint. Returns +// immediately (no-op) when the client is disabled (no consent) or +// no signer is configured. +// +// The request is Ed25519-signed with the node's identity, following +// the telemetry server's signing contract: +// - X-Pilot-Timestamp: unix seconds (decimal string) +// - X-Pilot-Public-Key: base64(std) of the 32-byte Ed25519 public key +// - X-Pilot-Signature: base64(std) of the Ed25519 signature over +// (timestamp + "\n" + body) +func (c *Client) Send(events ...Event) error { + c.mu.Lock() + disabled := c.disabled + url := c.url + sign := c.sign + pubKeyB := c.pubKeyB + c.mu.Unlock() + + if disabled || url == "" { + slog.Debug("telemetry: consent off, dropping events", "count", len(events)) + return nil + } + if sign == nil { + slog.Debug("telemetry: no signer configured, dropping events", "count", len(events)) + return nil + } + + if len(events) == 0 { + return nil + } + + body, err := json.Marshal(events) + if err != nil { + return fmt.Errorf("telemetry marshal: %w", err) + } + + ts := strconv.FormatInt(time.Now().Unix(), 10) + message := make([]byte, 0, len(ts)+1+len(body)) + message = append(message, ts...) + message = append(message, '\n') + message = append(message, body...) + sigB64 := base64.StdEncoding.EncodeToString(sign(message)) + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("telemetry new request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set(HeaderTimestamp, ts) + req.Header.Set(HeaderPubKey, pubKeyB) + req.Header.Set(HeaderSignature, sigB64) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("telemetry post: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 300 { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("telemetry server %s: %s", resp.Status, strings.TrimSpace(string(respBody))) + } + + // Drain body so the connection can be reused + _, _ = io.Copy(io.Discard, resp.Body) + return nil +} + +// SignMessage implements the signing contract directly, without an HTTP +// POST. Useful for tests and for components that want to sign arbitrary +// byte payloads. Returns (timestamp, pubKeyB64, signatureB64, error). +func SignMessage(priv ed25519.PrivateKey, body []byte) (ts, pubB64, sigB64 string, err error) { + if len(body) == 0 { + return "", "", "", fmt.Errorf("telemetry: cannot sign empty body") + } + pub := priv.Public().(ed25519.PublicKey) + ts = strconv.FormatInt(time.Now().Unix(), 10) + message := make([]byte, 0, len(ts)+1+len(body)) + message = append(message, ts...) + message = append(message, '\n') + message = append(message, body...) + sig := ed25519.Sign(priv, message) + return ts, base64.StdEncoding.EncodeToString(pub), base64.StdEncoding.EncodeToString(sig), nil +} diff --git a/pkg/telemetry/client_test.go b/pkg/telemetry/client_test.go new file mode 100644 index 00000000..df86cd07 --- /dev/null +++ b/pkg/telemetry/client_test.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +package telemetry + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "testing" +) + +func TestNewNoop(t *testing.T) { + c := New("", 0) + if err := c.Send(); err != nil { + t.Fatal("no-op send should not error:", err) + } + if err := c.Send(Event{Kind: "test", Payload: []byte(`{}`)}); err != nil { + t.Fatal("no-op send with events should not error:", err) + } +} + +func TestNewNoSigner(t *testing.T) { + c := New("http://example.com/v1/events", 42) + if err := c.Send(Event{Kind: "test", Payload: []byte(`{}`)}); err != nil { + t.Fatal("send without signer should be no-op, not error:", err) + } +} + +func TestDisabledOnEmptyURL(t *testing.T) { + c1 := New("", 0) + if !c1.disabled { + t.Fatal("expected disabled for empty URL") + } + c2 := New(" ", 0) + if !c2.disabled { + t.Fatal("expected disabled for whitespace-only URL") + } + c3 := New("https://example.com/v1/events", 0) + if c3.disabled { + t.Fatal("expected enabled for valid URL") + } +} + +func TestSendErrorsOnBadURL(t *testing.T) { + c := New("http://127.0.0.1:1/events", 1) + if err := c.Send(Event{Kind: "test", Payload: []byte(`{}`)}); err != nil { + t.Fatal("no signer means no-op, not error:", err) + } +} + +func TestSignMessageRoundTrip(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + body := []byte(`{"kind":"test","ts":"2026-01-01T00:00:00Z","payload":{}}`) + + ts, pubB64, sigB64, err := SignMessage(priv, body) + if err != nil { + t.Fatal("SignMessage failed:", err) + } + if ts == "" || pubB64 == "" || sigB64 == "" { + t.Fatal("expected non-empty outputs") + } + + // Reconstruct the canonical message + msg := make([]byte, 0, len(ts)+1+len(body)) + msg = append(msg, ts...) + msg = append(msg, '\n') + msg = append(msg, body...) + + decodedPub, err := base64.StdEncoding.DecodeString(pubB64) + if err != nil { + t.Fatal("decode pubkey:", err) + } + decodedSig, err := base64.StdEncoding.DecodeString(sigB64) + if err != nil { + t.Fatal("decode sig:", err) + } + if !ed25519.Verify(ed25519.PublicKey(decodedPub), msg, decodedSig) { + t.Fatal("signature verification failed on round-trip") + } + + // Also verify public key matches + if !ed25519.PublicKey(decodedPub).Equal(pub) { + t.Fatal("public key mismatch") + } +} From 83c84c82cbb489bb8ec253d20c2b9353fea031e6 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Tue, 16 Jun 2026 14:14:55 +0000 Subject: [PATCH 2/4] feat(telemetry): emit install events (PILOT-401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a NewClientFromIdentity convenience constructor to the telemetry package that creates a consent-gated client from a node's Ed25519 identity file on disk. Wire telemetry into the app-store install command: after a successful install (bundle validated, manifest planted, audit logs written), emit an app_installed event carrying app_id, version, and source (catalogue|local). The emission is best-effort and consent-gated — when PILOT_TELEMETRY_URL is empty or identity.json is absent, the client is a hard no-op. Tests cover the new NewClientFromIdentity helper with valid identity, missing identity, loose file permissions, and empty URL (all no-op paths). Closes PILOT-401 --- cmd/pilotctl/appstore.go | 32 +++++++++++++++++++ pkg/telemetry/client.go | 28 ++++++++++++++++ pkg/telemetry/client_test.go | 62 ++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/cmd/pilotctl/appstore.go b/cmd/pilotctl/appstore.go index eac13755..d61d97b0 100644 --- a/cmd/pilotctl/appstore.go +++ b/cmd/pilotctl/appstore.go @@ -35,6 +35,8 @@ import ( "github.com/pilot-protocol/app-store/pkg/ipc" "github.com/pilot-protocol/app-store/pkg/manifest" "github.com/pilot-protocol/common/crypto" + + "github.com/TeoSlayer/pilotprotocol/pkg/telemetry" ) // cryptoSHA256 is named so the sha256 import isn't ambiguous-looking. @@ -1209,6 +1211,36 @@ func cmdAppStoreInstall(args []string) { Reason: reason, }) + // Emit a telemetry event for the successful install (consent-gated — + // no-op when PILOT_TELEMETRY_URL is empty or identity.json is absent). + // Best-effort: a send failure is logged but not fatal — the install + // itself already succeeded on disk. + { + url := os.Getenv("PILOT_TELEMETRY_URL") + if url == "" { + url = telemetry.DefaultEndpoint + } + sourceStr := "catalogue" + if source == installSourceLocal { + sourceStr = "local" + } + payload, _ := json.Marshal(map[string]string{ + "app_id": m.ID, + "version": m.AppVersion, + "source": sourceStr, + }) + identityPath := configDir() + "/identity.json" + client := telemetry.NewClientFromIdentity(url, identityPath, 0) + err := client.Send(telemetry.Event{ + Kind: "app_installed", + TS: time.Now().UTC().Format(time.RFC3339), + Payload: payload, + }) + if err != nil { + slog.Warn("telemetry send failed, install still successful", "app", m.ID, "err", err) + } + } + report := installReport{ AppID: m.ID, AppVersion: m.AppVersion, diff --git a/pkg/telemetry/client.go b/pkg/telemetry/client.go index 6d473650..9c9c8b74 100644 --- a/pkg/telemetry/client.go +++ b/pkg/telemetry/client.go @@ -19,6 +19,8 @@ import ( "strings" "sync" "time" + + "github.com/pilot-protocol/common/crypto" ) // DefaultEndpoint is the production telemetry ingestion URL. @@ -148,6 +150,32 @@ func (c *Client) Send(events ...Event) error { return nil } +// NewClientFromIdentity creates a consent-gated telemetry client from an +// Ed25519 identity file on disk and a telemetry URL. When url is empty +// the client is a permanent no-op. Returns nil if the identity file does +// not exist (first run). +func NewClientFromIdentity(url, identityPath string, nodeID int64) *Client { + c := New(url, nodeID) + if c.disabled || url == "" { + return c + } + + id, err := crypto.LoadIdentity(identityPath) + if err != nil { + slog.Warn("telemetry: can't load identity, staying disabled", "path", identityPath, "err", err) + return c + } + if id == nil { + slog.Debug("telemetry: no identity file yet, staying disabled", "path", identityPath) + return c + } + + slog.Debug("telemetry: identity loaded, enabling client", "path", identityPath, + "pubkey", crypto.EncodePublicKey(id.PublicKey)) + c.SetSigner(id.Sign, crypto.EncodePublicKey(id.PublicKey)) + return c +} + // SignMessage implements the signing contract directly, without an HTTP // POST. Useful for tests and for components that want to sign arbitrary // byte payloads. Returns (timestamp, pubKeyB64, signatureB64, error). diff --git a/pkg/telemetry/client_test.go b/pkg/telemetry/client_test.go index df86cd07..5683d573 100644 --- a/pkg/telemetry/client_test.go +++ b/pkg/telemetry/client_test.go @@ -6,7 +6,11 @@ import ( "crypto/ed25519" "crypto/rand" "encoding/base64" + "os" + "path/filepath" "testing" + + "github.com/pilot-protocol/common/crypto" ) func TestNewNoop(t *testing.T) { @@ -48,6 +52,64 @@ func TestSendErrorsOnBadURL(t *testing.T) { } } +func TestNewClientFromIdentity(t *testing.T) { + // Empty URL = no-op, no identity file needed + c := NewClientFromIdentity("", "/nonexistent/identity.json", 0) + if err := c.Send(Event{Kind: "test", Payload: []byte(`{}`)}); err != nil { + t.Fatal("no-op with no url should not error:", err) + } + + // Non-empty URL with no identity file = noop (file doesn't exist = first run) + c2 := NewClientFromIdentity("http://example.com/v1/events", "/nonexistent/identity.json", 42) + if err := c2.Send(Event{Kind: "test", Payload: []byte(`{}`)}); err != nil { + t.Fatal("no identity file should be no-op, not error:", err) + } + + // Valid identity file + dir := t.TempDir() + idPath := filepath.Join(dir, "identity.json") + id, err := crypto.GenerateIdentity() + if err != nil { + t.Fatal(err) + } + if err := crypto.SaveIdentity(idPath, id); err != nil { + t.Fatal(err) + } + + c3 := NewClientFromIdentity("http://example.com/v1/events", idPath, 7) + if c3.disabled { + t.Fatal("expected enabled for valid identity + URL") + } + if c3.sign == nil { + t.Fatal("expected signer to be set") + } + if c3.pubKeyB == "" { + t.Fatal("expected public key to be set") + } +} + +func TestNewClientFromIdentityLoosePerms(t *testing.T) { + dir := t.TempDir() + idPath := filepath.Join(dir, "identity.json") + id, err := crypto.GenerateIdentity() + if err != nil { + t.Fatal(err) + } + if err := crypto.SaveIdentity(idPath, id); err != nil { + t.Fatal(err) + } + // Make the identity file world-readable — simulates a manual restore + if err := os.Chmod(idPath, 0644); err != nil { + t.Fatal(err) + } + + // LoadIdentity refuses loose perms; client should warn and stay noop + c := NewClientFromIdentity("http://example.com/v1/events", idPath, 7) + if c.sign != nil { + t.Fatal("expected no signer for loose-permissions identity file") + } +} + func TestSignMessageRoundTrip(t *testing.T) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { From 8723bf8c832d7a09d902861b3d424f08fc77bcb3 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Tue, 16 Jun 2026 14:24:34 +0000 Subject: [PATCH 3/4] feat(daemon): wire app-usage telemetry for app-store supervisor (PILOT-402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The appstoreAdapter now holds telemetryURL and identityPath, using them to create a consent-gated telemetry.Client on Start. The client is wrapped in an telemetryEmitter that satisfies appstore.TelemetryEmitter, passing app_usage events from supervisor.callFrom into the signed telemetry pipeline. When consent is off (empty telemetry URL) the client is a permanent no-op — no goroutines, no dials, no buffering. Related: app-store module also updated for PILOT-402 (commit 8edfed7efa72e78499f02260ea84ae42b724f49a). --- cmd/daemon/appstore_adapter.go | 49 +++++++++++++++++++++++++++++----- cmd/daemon/main.go | 30 ++++++++++++++++----- go.mod | 4 +-- go.sum | 4 +++ 4 files changed, 71 insertions(+), 16 deletions(-) diff --git a/cmd/daemon/appstore_adapter.go b/cmd/daemon/appstore_adapter.go index 87751112..f35c160b 100644 --- a/cmd/daemon/appstore_adapter.go +++ b/cmd/daemon/appstore_adapter.go @@ -15,25 +15,60 @@ package main import ( "context" + "encoding/json" + "time" "github.com/pilot-protocol/app-store/plugin/appstore" "github.com/pilot-protocol/common/coreapi" + "github.com/TeoSlayer/pilotprotocol/pkg/telemetry" ) type appstoreAdapter struct { - svc *appstore.Service + svc *appstore.Service + telemetryURL string + identityPath string +} + +// telemetryEmitter wraps the consent-gated telemetry client to satisfy +// the appstore.TelemetryEmitter interface. Events are sent as +// "app_usage" kind with the supervisor-provided fields as payload. +// Best-effort: send errors are logged but never block the caller. +type telemetryEmitter struct { + client *telemetry.Client +} + +func (e *telemetryEmitter) Emit(ev appstore.TelemetryEvent) { + if e == nil || e.client == nil { + return + } + payload, err := json.Marshal(ev) + if err != nil { + return + } + _ = e.client.Send(telemetry.Event{ + Kind: "app_usage", + TS: time.Now().UTC().Format(time.RFC3339), + Payload: payload, + }) } func (a *appstoreAdapter) Name() string { return a.svc.Name() } func (a *appstoreAdapter) Order() int { return a.svc.Order() } func (a *appstoreAdapter) Start(ctx context.Context, deps coreapi.Deps) error { + // Build a consent-gated telemetry client for app-usage events. + // When the URL is empty or identity is absent the client is a + // permanent no-op — the emitter never sends anything. + client := telemetry.NewClientFromIdentity(a.telemetryURL, a.identityPath, 0) + emitter := &telemetryEmitter{client: client} + return a.svc.Start(ctx, appstore.Deps{ - Streams: deps.Streams, - Identity: deps.Identity, - Resolver: deps.Resolver, - Events: deps.Events, - Logger: deps.Logger, - Trust: deps.Trust, + Streams: deps.Streams, + Identity: deps.Identity, + Resolver: deps.Resolver, + Events: deps.Events, + Logger: deps.Logger, + Trust: deps.Trust, + Telemetry: emitter, }) } func (a *appstoreAdapter) Stop(ctx context.Context) error { return a.svc.Stop(ctx) } diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index e45f5f80..67b5e80c 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -307,13 +307,29 @@ func main() { if home, herr := os.UserHomeDir(); herr == nil { appstoreInstallRoot = filepath.Join(home, ".pilot", "apps") } - if err := rt.Register(&appstoreAdapter{svc: appstore.NewService(appstore.Config{ - InstallRoot: appstoreInstallRoot, - RescanInterval: 2 * time.Second, - // Real catalogue trust anchor (replaces the all-zeros - // placeholder default): the embedded ed25519 catalogue key. - CatalogPubkey: []byte(catalogtrust.PublicKey()), - })}); err != nil { + // The app-usage telemetry emitter shares the daemon's identity file + // and telemetry URL. When consent is off (empty URL) the client is + // a permanent no-op — no goroutines, no dials, no buffering. + idPath := *identityPath + if idPath == "" { + if home, herr := os.UserHomeDir(); herr == nil { + defaultID := filepath.Join(home, ".pilot", "identity.json") + if _, serr := os.Stat(defaultID); serr == nil { + idPath = defaultID + } + } + } + if err := rt.Register(&appstoreAdapter{ + svc: appstore.NewService(appstore.Config{ + InstallRoot: appstoreInstallRoot, + RescanInterval: 2 * time.Second, + // Real catalogue trust anchor (replaces the all-zeros + // placeholder default): the embedded ed25519 catalogue key. + CatalogPubkey: []byte(catalogtrust.PublicKey()), + }), + telemetryURL: *telemetryURL, + identityPath: idPath, + }); err != nil { log.Fatalf("register appstore: %v", err) } diff --git a/go.mod b/go.mod index b0bf241f..8172cad8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.10 require ( github.com/coder/websocket v1.8.14 - github.com/pilot-protocol/app-store v1.0.1-beta.1.0.20260609061942-8852c785a264 + github.com/pilot-protocol/app-store v1.0.1-beta.1.0.20260616142430-8edfed7efa72 github.com/pilot-protocol/beacon v0.2.6 github.com/pilot-protocol/common v0.4.9-0.20260615113553-d5cbbfb3e5b6 github.com/pilot-protocol/dataexchange v0.2.1-beta.1.0.20260615113607-fac933edea98 @@ -18,7 +18,7 @@ require ( github.com/pilot-protocol/trustedagents v0.2.3 github.com/pilot-protocol/updater v0.2.2-0.20260529065627-220ed5b8383f github.com/pilot-protocol/webhook v0.2.0 - golang.org/x/sys v0.45.0 + golang.org/x/sys v0.46.0 ) require ( diff --git a/go.sum b/go.sum index 2c02acd1..f54c06a5 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/pilot-protocol/app-store v1.0.0-rc1.0.20260609015400-d02db7da3924 h1: github.com/pilot-protocol/app-store v1.0.0-rc1.0.20260609015400-d02db7da3924/go.mod h1:f0umeJxswDG8/CctHpSFMlr5GLtE2GlPKkijIQErZuc= github.com/pilot-protocol/app-store v1.0.1-beta.1.0.20260609061942-8852c785a264 h1:NL9rFdakbVQ0V7xfJbCk8RJZSaQ1AmvdhAJwFIouMsk= github.com/pilot-protocol/app-store v1.0.1-beta.1.0.20260609061942-8852c785a264/go.mod h1:zoCxHYoNdj0V44OkG3Yzcye0jnwZDVUcJgAvR5Z1kwc= +github.com/pilot-protocol/app-store v1.0.1-beta.1.0.20260616142430-8edfed7efa72 h1:vDiQ7ZheKIzlNqfviu5zeQzGVTMP63k1hC5HodEuyeQ= +github.com/pilot-protocol/app-store v1.0.1-beta.1.0.20260616142430-8edfed7efa72/go.mod h1:leZPtX43gE2JB7xeljexXri81g6qhdZfYExLtzI+bhg= github.com/pilot-protocol/beacon v0.2.5 h1:5+pkSPoA35r+u4Hfrph/ZfOltOyiy8lh1sCfK5XqXKs= github.com/pilot-protocol/beacon v0.2.5/go.mod h1:I/UhEv097g1z/qtAVDZbEhf3R5tzM0Dp71vGHah52A4= github.com/pilot-protocol/beacon v0.2.6 h1:grxwaVyPRUT0W6coyjYfNkO0rpzOIrwrKn94S21DuVE= @@ -56,3 +58,5 @@ golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= From d6c327e2079cec00a6fd9579a419929f61f2e763 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Tue, 16 Jun 2026 16:46:51 +0000 Subject: [PATCH 4/4] fix: avoid potential int overflow in signed-message buffer pre-allocation --- pkg/telemetry/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/telemetry/client.go b/pkg/telemetry/client.go index 9c9c8b74..491e9689 100644 --- a/pkg/telemetry/client.go +++ b/pkg/telemetry/client.go @@ -119,7 +119,7 @@ func (c *Client) Send(events ...Event) error { } ts := strconv.FormatInt(time.Now().Unix(), 10) - message := make([]byte, 0, len(ts)+1+len(body)) + message := make([]byte, 0, len(ts)+1) message = append(message, ts...) message = append(message, '\n') message = append(message, body...) @@ -185,7 +185,7 @@ func SignMessage(priv ed25519.PrivateKey, body []byte) (ts, pubB64, sigB64 strin } pub := priv.Public().(ed25519.PublicKey) ts = strconv.FormatInt(time.Now().Unix(), 10) - message := make([]byte, 0, len(ts)+1+len(body)) + message := make([]byte, 0, len(ts)+1) message = append(message, ts...) message = append(message, '\n') message = append(message, body...)