diff --git a/cmd/pilotctl/main.go b/cmd/pilotctl/main.go index cb2547e3..11ceb89e 100644 --- a/cmd/pilotctl/main.go +++ b/cmd/pilotctl/main.go @@ -1352,6 +1352,49 @@ Reliability caveats (current implementation): "appstore": AppStoreHelpText, "review": reviewHelpText, + + "quickstart": `Usage: pilotctl quickstart + +Guided 3-step getting-started walkthrough. Re-run after each step — it detects +whether the daemon is already running and points you to the next action: + 1. start the daemon pilotctl daemon start + 2. discover specialists pilotctl send-message list-agents --data '...' --wait + 3. handshake + query one pilotctl handshake ; pilotctl send-message ... +`, + + "verify": `Usage: pilotctl verify [status] [flags] + +Verified-address badges — prove a node controls a real, attested identity. + + pilotctl verify show this node's verification state (alias: verify status) + pilotctl verify status [--node ] check a specific node's badge + pilotctl verify --provider run a device-flow to become verified + pilotctl verify --badge --badge-sig submit a badge you already hold + pilotctl verify --from submit a badge from a JSON credential file + +Badges are verified OFFLINE against the pinned issuer key — the registry's word +is never trusted. +`, + + "recovery": `Usage: pilotctl recovery ... + +Recover a node's address if its identity key is lost. + + pilotctl recovery enroll record an opaque recovery commitment for this address + pilotctl recovery new-key rotate to a fresh identity key + pilotctl recovery recover reclaim the address using recovery material + +Enrollment + signatures come from the 'pilot-verify' tool. +`, + + "prefer-direct": `Usage: pilotctl prefer-direct + +Reset routing state for a peer so the next connection prefers a direct +(hole-punched) tunnel over the relay. Useful after a relay path got pinned. +Requires daemon v1.12+. + +Returns: had_tunnel, was_relay_active, was_relay_pinned +`, } // printCommandHelp prints the help text for a command and exits. @@ -1406,8 +1449,7 @@ Discovery commands: pilotctl find pilotctl set-hostname pilotctl clear-hostname - pilotctl set-tags [tag2] ... - pilotctl clear-tags + (discovery tags are operator setup: pilotctl extras set-tags / clear-tags) Communication commands: pilotctl connect [port] [--message ] [--timeout ] @@ -1424,7 +1466,15 @@ Trust commands: pilotctl reject [reason] pilotctl untrust pilotctl pending - pilotctl trust + pilotctl trust [--search ] live trust state (peers you trust) + pilotctl trusted embedded directory of auto-approved service agents + pilotctl prefer-direct prefer a direct tunnel over the relay (daemon v1.12+) + +Identity & recovery: + pilotctl verify [status] show this node's verified-address badge state + pilotctl verify --provider run a device-flow to get a verified-address badge + pilotctl recovery ... enroll / rotate / reclaim the address if the key is lost + pilotctl review [--rating <1-5>] [--text "..."] rate Pilot or an installed app Management commands: pilotctl connections @@ -1437,6 +1487,28 @@ Mailbox: Service Agents: pilotctl send-message list-agents --data "list all agents" +Agent tool discovery: + pilotctl context + pilotctl skills [status] show where the daemon installs SKILL.md per detected agent tool + pilotctl skills paths print only the install paths (shell-friendly) + pilotctl skills check run one reconcile pass right now + +App store (install + call local capability apps; full help: pilotctl appstore help): + pilotctl appstore catalogue list apps available for one-command install + pilotctl appstore view [--all-changelog] app detail page (description, methods, permissions) + pilotctl appstore install [--force] install by catalogue ID (fetch + verify + extract) + pilotctl appstore list list installed apps + their IPC methods + pilotctl appstore call [json-args] dispatch an IPC call into an app + pilotctl appstore status|caps|audit|restart|uninstall + +Updates: + pilotctl update [status|enable|disable] [--pin ] self-update (auto-update OFF by default) + pilotctl updates [--count ] [--scope ] read the Pilot changelog feed + +Operator / admin (run 'pilotctl extras' or 'pilotctl context' for the full list): + pilotctl extras network / managed / policy / member-tags / enterprise / low-level plumbing + pilotctl extras gateway start|stop|map|unmap|list IP gateway (requires root for ports <1024) + Diagnostic commands: pilotctl info pilotctl health @@ -1446,21 +1518,6 @@ Diagnostic commands: pilotctl bench [size_mb] [--timeout ] pilotctl listen [--count ] [--timeout ] pilotctl broadcast - pilotctl update [--pin ] run the updater once — check and install new release - pilotctl updates [--count ] [--scope ] read https://pilot-protocol.github.io/pilot-changelog/feed.xml - -Agent tool discovery: - pilotctl context - pilotctl skills [status] show where the daemon installs SKILL.md per detected agent tool - pilotctl skills paths print only the install paths (shell-friendly) - pilotctl skills check run one reconcile pass right now - -Gateway (requires root for ports <1024): - pilotctl gateway start [--subnet ] [--ports ] [...] - pilotctl gateway stop - pilotctl gateway map [local-ip] - pilotctl gateway unmap - pilotctl gateway list Environment: PILOT_REGISTRY Registry address (default: 34.71.57.205:9000) @@ -1996,8 +2053,8 @@ func cmdConfig(args []string) { // serve single-command lookups without holding the map at package scope. func contextCatalog() map[string]interface{} { return map[string]interface{}{ - "version": "1.3", - "note": "Core commands cover everything an agent needs. Use 'pilotctl extras ' for operator/admin operations. 'pilot-gateway' is a separate installed binary.", + "version": "1.4", + "note": "Core commands cover everything an agent needs. 'app_store' lists the 'pilotctl appstore ' command family (install + call local capability apps). Use 'pilotctl extras ' for operator/admin operations. 'pilot-gateway' is a separate installed binary.", // ── Core agent commands ────────────────────────────────────────────── "commands": map[string]interface{}{ @@ -2017,6 +2074,31 @@ func contextCatalog() map[string]interface{} { "description": "Print the installed binary version", "returns": "version string", }, + "quickstart": map[string]interface{}{ + "args": []string{}, + "description": "Guided 3-step getting-started walkthrough (start daemon → discover agents → handshake + query). Re-run after each step; detects daemon state", + "returns": "quickstart [{step, title, command, description, done}]", + }, + "update": map[string]interface{}{ + "args": []string{"[status|enable|disable]", "[--pin ]"}, + "description": "Self-update. Bare: run the updater once (check + install latest). Subcommands: status, enable, disable (auto-update is OFF by default)", + "returns": "updated (bool), from, to | enabled (bool)", + }, + "updates": map[string]interface{}{ + "args": []string{"[--count ]", "[--scope ]"}, + "description": "Read the Pilot changelog feed (release notes / important updates)", + "returns": "entries [{title, scope, published, summary}]", + }, + "skills": map[string]interface{}{ + "args": []string{"[status|paths|check|enable|disable|set-mode ]"}, + "description": "Manage SKILL.md install across detected agent tools. status (default), paths (shell-friendly), check (reconcile once), enable/disable, set-mode", + "returns": "tools [{tool, path, installed}] | paths", + }, + "review": map[string]interface{}{ + "args": []string{"", "[--rating <1-5>]", "[--text <\"...\">]"}, + "description": "Submit a rating/review for Pilot itself ('pilot') or for an installed app (e.g. io.pilot.cosift)", + "returns": "ok, target, rating", + }, // Daemon lifecycle "daemon start": map[string]interface{}{ @@ -2108,6 +2190,28 @@ func contextCatalog() map[string]interface{} { "description": "Revoke trust for a peer", "returns": "node_id", }, + "trusted": map[string]interface{}{ + "args": []string{}, + "description": "List the embedded trusted-agents directory — well-known service agents (list-agents, weather, etc.) auto-approved on first contact. For live trust state use 'trust'", + "returns": "agents [{hostname, node_id}], total", + }, + "prefer-direct": map[string]interface{}{ + "args": []string{""}, + "description": "Reset routing state for a peer so the next connection prefers a direct (hole-punched) tunnel over the relay. Requires daemon v1.12+", + "returns": "had_tunnel, was_relay_active, was_relay_pinned", + }, + + // Identity verification & key recovery + "verify": map[string]interface{}{ + "args": []string{"[status]", "[--node ]", "[--provider ]", "[--badge --badge-sig ]", "[--from ]"}, + "description": "Verified-address badges. Bare 'verify' (or 'verify status') shows your verification state; '--provider ' runs a device-flow to get verified; '--badge/--badge-sig' (or '--from ') submits a badge", + "returns": "verified (bool), node_id, address, issuer | submitted", + }, + "recovery": map[string]interface{}{ + "args": []string{"", "..."}, + "description": "Address recovery if the identity key is lost. enroll: record a recovery commitment; new-key: rotate to a fresh key; recover: reclaim the address with recovery material", + "returns": "status, node_id, address", + }, // Networks (agent self-management) "network list": map[string]interface{}{ @@ -2236,6 +2340,29 @@ func contextCatalog() map[string]interface{} { }, }, + // ── App store ──────────────────────────────────────────────────────── + // Invoke as: pilotctl appstore [args...] + "app_store": map[string]interface{}{ + "description": "Install and call local capability apps that run on the daemon as typed IPC services. All invoked as: pilotctl appstore . Install root: $PILOT_APPSTORE_ROOT or ~/.pilot/apps.", + "subcommands": map[string]interface{}{ + "catalogue": map[string]interface{}{"args": []string{}, "description": "List apps available for one-command install (alias: catalog)"}, + "view": map[string]interface{}{"args": []string{"", "[--all-changelog]"}, "description": "Detail page: description, vendor, changelog, size, source, methods, permissions (installed or not)"}, + "install": map[string]interface{}{"args": []string{" [--force]", "| --local [--force]"}, "description": "Install by catalogue ID (fetch + verify + extract), or sideload a local bundle with --local"}, + "list": map[string]interface{}{"args": []string{}, "description": "List installed apps and the IPC methods each exposes"}, + "call": map[string]interface{}{"args": []string{"", "", "[json-args]", "[--timeout ]"}, "description": "Dispatch an IPC call into an app (default timeout 120s; $PILOT_APPSTORE_CALL_TIMEOUT)"}, + "status": map[string]interface{}{"args": []string{""}, "description": "Deep-dive on one app's pinned state"}, + "caps": map[string]interface{}{"args": []string{""}, "description": "Show the manifest's spend caps and current rolling-window usage"}, + "audit": map[string]interface{}{"args": []string{"", "[--tail ]", "[--event ]", "[--since ]"}, "description": "Show the supervisor lifecycle log (spawn/exit/suspend/verify-fail)"}, + "actions": map[string]interface{}{"args": []string{"[--tail ]", "[--event ]"}, "description": "Show the pilotctl-side install/uninstall action log (survives app removal)"}, + "restart": map[string]interface{}{"args": []string{""}, "description": "Clear crash-loop suspension and respawn the app"}, + "uninstall": map[string]interface{}{"args": []string{"", "--yes"}, "description": "Remove an installed app from the install root"}, + "verify": map[string]interface{}{"args": []string{""}, "description": "sha256-check a pre-install bundle against its manifest"}, + "gen-key": map[string]interface{}{"args": []string{""}, "description": "Generate a fresh ed25519 publisher keypair (publisher tooling)"}, + "sign": map[string]interface{}{"args": []string{"--key ", ""}, "description": "Sign (or re-sign) a manifest's store.signature (publisher tooling)"}, + "sign-catalogue": map[string]interface{}{"args": []string{"--key ", ""}, "description": "Sign the catalogue, writing a detached .sig (alias: sign-catalog; publisher tooling)"}, + }, + }, + // ── Extras (operator / admin) ──────────────────────────────────────── // Invoke as: pilotctl extras [args...] "extras": map[string]interface{}{ diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 2e66819b..6afd0f48 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -36,8 +36,7 @@ Discovery commands: pilotctl find pilotctl set-hostname pilotctl clear-hostname - pilotctl set-tags [tag2] ... - pilotctl clear-tags + (discovery tags are operator setup: pilotctl extras set-tags / clear-tags) Communication commands: pilotctl connect [port] [--message ] [--timeout ] @@ -54,7 +53,15 @@ Trust commands: pilotctl reject [reason] pilotctl untrust pilotctl pending - pilotctl trust + pilotctl trust [--search ] live trust state (peers you trust) + pilotctl trusted embedded directory of auto-approved service agents + pilotctl prefer-direct prefer a direct tunnel over the relay (daemon v1.12+) + +Identity & recovery: + pilotctl verify [status] show this node's verified-address badge state + pilotctl verify --provider run a device-flow to get a verified-address badge + pilotctl recovery ... enroll / rotate / reclaim the address if the key is lost + pilotctl review [--rating <1-5>] [--text "..."] rate Pilot or an installed app Management commands: pilotctl connections @@ -67,6 +74,28 @@ Mailbox: Service Agents: pilotctl send-message list-agents --data "list all agents" +Agent tool discovery: + pilotctl context + pilotctl skills [status] show where the daemon installs SKILL.md per detected agent tool + pilotctl skills paths print only the install paths (shell-friendly) + pilotctl skills check run one reconcile pass right now + +App store (install + call local capability apps; full help: pilotctl appstore help): + pilotctl appstore catalogue list apps available for one-command install + pilotctl appstore view [--all-changelog] app detail page (description, methods, permissions) + pilotctl appstore install [--force] install by catalogue ID (fetch + verify + extract) + pilotctl appstore list list installed apps + their IPC methods + pilotctl appstore call [json-args] dispatch an IPC call into an app + pilotctl appstore status|caps|audit|restart|uninstall + +Updates: + pilotctl update [status|enable|disable] [--pin ] self-update (auto-update OFF by default) + pilotctl updates [--count ] [--scope ] read the Pilot changelog feed + +Operator / admin (run 'pilotctl extras' or 'pilotctl context' for the full list): + pilotctl extras network / managed / policy / member-tags / enterprise / low-level plumbing + pilotctl extras gateway start|stop|map|unmap|list IP gateway (requires root for ports <1024) + Diagnostic commands: pilotctl info pilotctl health @@ -76,21 +105,6 @@ Diagnostic commands: pilotctl bench [size_mb] [--timeout ] pilotctl listen [--count ] [--timeout ] pilotctl broadcast - pilotctl update [--pin ] run the updater once — check and install new release - pilotctl updates [--count ] [--scope ] read https://pilot-protocol.github.io/pilot-changelog/feed.xml - -Agent tool discovery: - pilotctl context - pilotctl skills [status] show where the daemon installs SKILL.md per detected agent tool - pilotctl skills paths print only the install paths (shell-friendly) - pilotctl skills check run one reconcile pass right now - -Gateway (requires root for ports <1024): - pilotctl gateway start [--subnet ] [--ports ] [...] - pilotctl gateway stop - pilotctl gateway map [local-ip] - pilotctl gateway unmap - pilotctl gateway list Environment: PILOT_REGISTRY Registry address (default: 34.71.57.205:9000) diff --git a/pkg/daemon/ipc.go b/pkg/daemon/ipc.go index f758461a..98bb050c 100644 --- a/pkg/daemon/ipc.go +++ b/pkg/daemon/ipc.go @@ -14,8 +14,8 @@ import ( "log/slog" "net" "os" + "path/filepath" "sync" - "syscall" "time" "github.com/pilot-protocol/common/badgeverify" @@ -517,22 +517,41 @@ func (s *IPCServer) Start() error { // Remove stale socket os.Remove(s.socketPath) - // PILOT-246: Set umask before Listen so the Unix socket is created - // with 0600 permissions directly, eliminating the TOCTOU window - // between Listen and the explicit Chmod that follows. - oldUmask := syscall.Umask(0o177) - ln, err := net.Listen("unix", s.socketPath) - syscall.Umask(oldUmask) // restore immediately + // PILOT-246: Bind the socket inside a private, freshly-created 0700 + // directory and atomically rename it into place. The socket therefore + // only ever becomes reachable at its final path once it already exists + // with restricted access — closing the TOCTOU window between Listen and + // the explicit Chmod below without touching the process-global umask. + // + // The previous implementation flipped syscall.Umask around Listen, but + // umask is process-wide (not goroutine-scoped): a concurrent file or + // directory creation elsewhere in the process would inherit the + // restrictive 0177 mask and be created unwritable. Under parallel tests + // this raced with t.TempDir(), whose nested mkdir then failed with + // "permission denied". Binding in a private dir is race-free and keeps + // the same security guarantee. + parent := filepath.Dir(s.socketPath) + stageDir, err := os.MkdirTemp(parent, ".pilot-sock-") + if err != nil { + return fmt.Errorf("create socket staging dir under %s: %w", parent, err) + } + defer os.RemoveAll(stageDir) + stagePath := filepath.Join(stageDir, filepath.Base(s.socketPath)) + + ln, err := net.Listen("unix", stagePath) if err != nil { return fmt.Errorf("listen unix %s: %w", s.socketPath, err) } - // Restrict socket access to owner only (belt-and-suspenders — - // umask already ensures 0600 from creation, but explicit Chmod - // catches any platform where umask semantics differ). - if err := os.Chmod(s.socketPath, 0600); err != nil { + // Restrict socket access to owner only before it is reachable at the + // published path. + if err := os.Chmod(stagePath, 0600); err != nil { ln.Close() return fmt.Errorf("chmod socket %s: %w", s.socketPath, err) } + if err := os.Rename(stagePath, s.socketPath); err != nil { + ln.Close() + return fmt.Errorf("publish socket %s: %w", s.socketPath, err) + } s.listener = ln slog.Info("IPC listening", "socket", s.socketPath)