From 7b70199c9ca9b3c2f7e2e5dc9a930daf2452609b Mon Sep 17 00:00:00 2001 From: Teodor Calin Date: Thu, 24 Sep 2026 03:11:59 +0300 Subject: [PATCH 1/5] installer: transport auto/compat, HTTPS-proxy sandboxes, root without systemd Ports the installer half of pilot-protocol/pilotprotocol#feat/native-https-proxy onto the canonical script. The managed-node code (--managed-url, --no-start, PILOT_ENROLLMENT_TOKEN) is unchanged; in managed mode the new "start the daemon manually" hints stay silent because the managed flow starts it. - --transport (or PILOT_TRANSPORT). udp and compat are saved in config.json; auto (the default) never is, and --transport auto removes a saved transport. A saved transport=auto is rewritten to udp for a pilot-daemon that predates it (reinstalling an older --version). - Root is allowed in a Linux container/VM without systemd (hosted agent sandboxes such as Meta Muse run the agent as root); hosts with systemd or launchd still refuse root unless PILOT_ALLOW_ROOT=1. - proxy_cmd: in such a sandbox, when HTTPS_PROXY carries credentials and the daemon supports -proxy-cmd, save bash -c 'printf %s "${https_proxy:-$HTTPS_PROXY}"' (or $PILOT_PROXY_CMD) so the daemon re-reads rotating proxy credentials. An existing proxy_cmd is never replaced. - Compat installs leave the raw-TCP default registry/beacon off the service units; switching back from compat restores the raw registry an older compat install saved. Service units carry PILOT_TRANSPORT_DEFAULT=auto for daemons that support it. - Download failures name the proxy (redacted) and the hosts it must allow; proxy credentials never reach the output. tests/proxy-transport-install.sh covers the above with a fixture release (no network, never sudo); tests/managed-install.sh still passes. Co-Authored-By: Claude Opus 5.5 (1M context) --- install.sh | 392 +++++++++++++++++++++++++++++-- tests/proxy-transport-install.sh | 271 +++++++++++++++++++++ 2 files changed, 637 insertions(+), 26 deletions(-) create mode 100644 tests/proxy-transport-install.sh diff --git a/install.sh b/install.sh index 0ec7d7b..f6dd4b9 100755 --- a/install.sh +++ b/install.sh @@ -9,6 +9,11 @@ set -e # Install: curl -fsSL https://pilotprotocol.network/install.sh | sh # Pin a version: curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --version v1.13.6 # Beta channel: curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --channel beta +# UDP blocked / curl -fsSL https://pilotprotocol.network/install.sh | sh +# HTTPS proxy: (nothing extra: transport "auto" picks TLS/WSS over TCP 443 +# through $HTTPS_PROXY when UDP does not work; add +# `-s -- --transport compat` to skip the UDP probe; proxy +# credentials that rotate: see PILOT_PROXY_CMD below) # Managed node: export PILOT_ENROLLMENT_TOKEN # enter it without putting it in shell history # sh install.sh --managed-url https://management.pilotprotocol.network # Uninstall: curl -fsSL https://pilotprotocol.network/install.sh | sh -s uninstall @@ -21,6 +26,16 @@ set -e # never silently falls back to an unverified source build. # --yes / -y Skip the older-version confirmation prompt. # --no-warn Suppress the older-version warning entirely. +# --transport auto (the default), udp or compat. udp and compat are +# saved as "transport" in ~/.pilot/config.json; auto is +# never saved (it is what `pilotctl daemon start` and the +# service units use when nothing is saved, and a daemon +# that predates auto would refuse it after a downgrade). +# auto: UDP when the beacon answers over UDP, else compat. +# compat: TLS/WSS over TCP 443 only, through +# $HTTPS_PROXY/$ALL_PROXY when set (CONNECT by hostname) — +# for UDP-blocked hosts and agent sandboxes whose only way +# out is an HTTPS proxy. # --managed-url # Install the checksum-pinned core managed runtime, claim # a one-time hosted identity, and start signed reporting. @@ -35,10 +50,26 @@ set -e # non-interactive/headless installs (no TTY prompt). # If omitted headless, the daemon auto-synthesizes a # @nodes.pilotprotocol.network identity. +# PILOT_TRANSPORT=compat Same as --transport compat. +# PILOT_PROXY_CMD= Saved as "proxy_cmd": a command printing the +# current proxy URL, for proxies that rotate their +# credentials. In a Linux container/VM without +# systemd whose HTTPS_PROXY carries credentials +# (hosted agent sandboxes such as Meta Muse), the +# installer saves one that reads a fresh shell's +# $https_proxy when none is set. +# PILOT_ALLOW_ROOT=1 Install as root on a host with systemd/launchd +# (not needed in containers/VMs without systemd). # PILOT_MANAGEMENT_URL=https://management.example # Same as --managed-url. Requires the one-time # PILOT_ENROLLMENT_TOKEN on first adoption. # +# Proxies: every download is a curl HTTPS request, so HTTPS_PROXY / https_proxy / +# ALL_PROXY / NO_PROXY are honored (curl asks the proxy to CONNECT by hostname — +# no local DNS lookup of the target). Nothing here needs UDP, a non-443 port, or +# a direct connection to the registry/beacon. Steps that need root, sudo, +# systemd or launchd are skipped with a message, never fatal. +# # WHAT THIS SCRIPT DOES (read before piping to sh): # 1. Detects OS/arch (Linux/Darwin × amd64/arm64) # 2. Resolves the latest release tag from github.com/pilot-protocol/pilotprotocol/releases @@ -96,8 +127,13 @@ set -e # error. REPO="pilot-protocol/pilotprotocol" -REGISTRY="${PILOT_REGISTRY:-34.71.57.205:9000}" -BEACON="${PILOT_BEACON:-34.71.57.205:9001}" +# Production defaults — the same raw-TCP/UDP endpoints compiled into +# pilot-daemon. Compat mode must not pin them explicitly (see NET_FLAGS). +DEFAULT_REGISTRY="34.71.57.205:9000" +DEFAULT_BEACON="34.71.57.205:9001" +COMPAT_REGISTRY="registry.pilotprotocol.network:443" +REGISTRY="${PILOT_REGISTRY:-$DEFAULT_REGISTRY}" +BEACON="${PILOT_BEACON:-$DEFAULT_BEACON}" PILOT_DIR="$HOME/.pilot" BIN_DIR="$PILOT_DIR/bin" MANAGED_CONTROL_PATH="$PILOT_DIR/managed/enterprise-control.json" @@ -146,6 +182,7 @@ PILOT_NO_WARN=0 PILOT_MANAGED_NO_START=0 PILOT_MANAGEMENT_URL="${PILOT_MANAGEMENT_URL:-}" PILOT_POSITIONAL="" +PILOT_REQUESTED_TRANSPORT="" while [ $# -gt 0 ]; do case "$1" in @@ -159,6 +196,11 @@ while [ $# -gt 0 ]; do PILOT_REQUESTED_CHANNEL="$2"; shift 2 ;; --channel=*) PILOT_REQUESTED_CHANNEL="${1#--channel=}"; shift ;; + --transport) + if [ $# -lt 2 ]; then echo "Error: --transport requires a value" >&2; exit 2; fi + PILOT_REQUESTED_TRANSPORT="$2"; shift 2 ;; + --transport=*) + PILOT_REQUESTED_TRANSPORT="${1#--transport=}"; shift ;; --yes|-y) PILOT_YES=1; shift ;; --no-warn) @@ -171,7 +213,7 @@ while [ $# -gt 0 ]; do --no-start) PILOT_MANAGED_NO_START=1; shift ;; -h|--help) - sed -n '4,32p' "$0" 2>/dev/null || echo "See https://pilotprotocol.network/install.sh" + sed -n '4,71p' "$0" 2>/dev/null || echo "See https://pilotprotocol.network/install.sh" exit 0 ;; --) shift @@ -264,17 +306,35 @@ if [ -n "$PILOT_REQUESTED_CHANNEL" ] \ exit 2 fi +# --transport beats the PILOT_TRANSPORT env var. Empty means "not requested on +# this run": a re-run keeps whatever transport config.json already has. +TRANSPORT="$(printf '%s' "${PILOT_REQUESTED_TRANSPORT:-${PILOT_TRANSPORT:-}}" | tr '[:upper:]' '[:lower:]')" +case "$TRANSPORT" in + ""|udp|compat|auto) ;; + *) + echo "Error: --transport must be 'udp', 'compat' or 'auto' (got: $TRANSPORT)" >&2 + exit 2 ;; +esac + # Restore positional args so the existing uninstall handler still uses $1. # shellcheck disable=SC2086 # intentional word-split on PILOT_POSITIONAL set -- $PILOT_POSITIONAL -# Refuse to run as root — daemon must run as the invoking user so identity.json -# and received files land under that user's home, not /root. +# Refuse to run as root on a regular host — the daemon must run as the +# invoking user so identity.json and received files land under that user's +# home, not /root. A Linux container or VM without systemd (CI runners, +# hosted agent sandboxes such as Meta Muse, where the agent IS root) has no +# other user to install for and no system service to protect, so root is +# allowed there. if [ "${1:-}" != "uninstall" ] && [ "$(id -u)" = "0" ] && [ -z "${PILOT_ALLOW_ROOT:-}" ]; then - echo "Error: refusing to install as root." - echo " Run as a regular user; the installer uses sudo only when needed." - echo " Set PILOT_ALLOW_ROOT=1 to override (not recommended)." - exit 1 + if [ "$(uname -s)" = "Linux" ] && [ ! -d /run/systemd/system ]; then + echo "Note: installing as root (no systemd: container/VM sandbox) into ${HOME}/.pilot" + else + echo "Error: refusing to install as root." + echo " Run as a regular user; the installer uses sudo only when needed." + echo " Set PILOT_ALLOW_ROOT=1 to override (not recommended)." + exit 1 + fi fi # A managed identity is per node, but the CLI links, service label and daemon @@ -318,6 +378,53 @@ if [ "$PILOT_MANAGED_MODE" = "1" ] && [ ! -e "$MANAGED_CONTROL_PATH" ] \ _pilot_collision=""; _pilot_target=""; _pilot_service=""; _pilot_os="" fi +# The transport already saved in config.json, if any ("udp", "compat", +# "auto"). A re-run without --transport keeps it, so regenerated service +# units stay consistent with it. +CONFIG_TRANSPORT="" +if [ -f "$PILOT_DIR/config.json" ]; then + CONFIG_TRANSPORT=$(sed -n 's/.*"transport"[[:space:]]*:[[:space:]]*"\([A-Za-z]*\)".*/\1/p' "$PILOT_DIR/config.json" 2>/dev/null | head -n 1 | tr '[:upper:]' '[:lower:]') +fi +# Without any choice, new installs get auto (settled below, once the +# installed daemon is known to support it). +EFFECTIVE_TRANSPORT="${TRANSPORT:-${CONFIG_TRANSPORT:-auto}}" + +# --- Egress proxy --- +# +# Every download below is a curl HTTPS request, and curl honors HTTPS_PROXY / +# https_proxy / ALL_PROXY / NO_PROXY on its own, asking the proxy to CONNECT +# by hostname (no local DNS lookup of the target — which matters where local +# DNS for pilotprotocol.network is poisoned). PILOT_PROXY_URL is only used in +# messages, and only ever printed redacted: the userinfo of an +# authenticating proxy is a credential. +PILOT_PROXY_URL="${HTTPS_PROXY:-${https_proxy:-${ALL_PROXY:-${all_proxy:-}}}}" + +# redact_proxy URL — print URL with any "user:pass@" replaced by "***@". +redact_proxy() { + case "$1" in + *@*) + _rp_scheme="" + case "$1" in *://*) _rp_scheme="${1%%://*}://" ;; esac + printf '%s***@%s\n' "$_rp_scheme" "${1##*@}" ;; + *) + printf '%s\n' "$1" ;; + esac +} + +# net_hint — after a failed download, say what to check. Behind an egress +# proxy the usual cause is the proxy refusing the CONNECT (407: bad +# credentials, 403: host not allowed), not a missing release. +net_hint() { + if [ -n "$PILOT_PROXY_URL" ]; then + echo " Note: downloads go through the proxy $(redact_proxy "$PILOT_PROXY_URL")." >&2 + echo " Check that it accepts CONNECT to pilotprotocol.network:443, github.com:443" >&2 + echo " and *.githubusercontent.com:443, and that its credentials are right." >&2 + else + echo " Note: check outbound HTTPS to pilotprotocol.network and github.com. If this host" >&2 + echo " can only reach the internet through a proxy, export HTTPS_PROXY and re-run." >&2 + fi +} + # --- Manifest + version helpers --- # fetch_manifest writes the manifest JSON to $1 and returns 0 on success. @@ -516,8 +623,20 @@ echo " Pilot Protocol" echo " The network stack for AI agents." echo "" echo " Platform: ${OS}/${ARCH}" -echo " Registry: ${REGISTRY}" -echo " Beacon: ${BEACON}" +case "$EFFECTIVE_TRANSPORT" in + compat) + echo " Transport: compat (TLS + WSS over TCP 443 only)" ;; + auto) + echo " Transport: auto (UDP when it works, else TLS + WSS over TCP 443)" + echo " Registry: ${REGISTRY}" + echo " Beacon: ${BEACON}" ;; + *) + echo " Registry: ${REGISTRY}" + echo " Beacon: ${BEACON}" ;; +esac +if [ -n "$PILOT_PROXY_URL" ]; then + echo " Proxy: $(redact_proxy "$PILOT_PROXY_URL") (from environment)" +fi echo "" # --- Resolve email --- @@ -693,11 +812,13 @@ if [ -z "$TAG" ]; then if [ -n "$PILOT_REQUESTED_CHANNEL" ]; then echo "Error: channel '$PILOT_REQUESTED_CHANNEL' resolved to no release (manifest reachable: $HAVE_MANIFEST)." >&2 echo " Refusing to fall back to an unverified source build for an explicit channel request." >&2 + [ "$HAVE_MANIFEST" = "1" ] || net_hint exit 1 fi if [ "${PILOT_RC:-}" = "1" ]; then echo "Error: the beta/prerelease channel resolved to no release." >&2 echo " Refusing to fall back to an unverified source build for an explicit channel request." >&2 + [ "$HAVE_MANIFEST" = "1" ] || net_hint exit 1 fi fi @@ -812,11 +933,13 @@ if [ -n "$TAG" ]; then # Archive download failed. Only the automatic default path may fall # back to a source build; an explicit request already hard-failed # above, so reaching here means no version/channel was pinned. + echo " Could not download ${URL}" >&2 TAG="" fi fi if [ -z "$TAG" ]; then + net_hint echo "No release available. Building from source..." if ! command -v go >/dev/null 2>&1; then echo "Error: Go is required to build from source." @@ -1094,6 +1217,197 @@ CONF echo "Config written to ${PILOT_DIR}/config.json" fi +# --- Transport: auto, udp or compat --- +# +# Merged into config.json through pilotctl (atomic write, 0600, every other +# key kept) instead of rewriting the file, so a hand-edited config survives a +# re-run. PILOT_HOME is blanked so the write lands in THIS install's +# $HOME/.pilot, which is also the file pilot-daemon auto-loads. +pilot_config_set() { + PILOT_HOME='' "$BIN_DIR/pilotctl" config --set "$1" >/dev/null 2>&1 +} + +# What the installed binaries support. The probes are local (no network). +DAEMON_HAS_TRANSPORT=false +DAEMON_HAS_PROXY=false +DAEMON_HAS_AUTO=false +_daemon_help=$("$BIN_DIR/pilot-daemon" -help 2>&1 || true) +if printf '%s\n' "$_daemon_help" | grep -qE '^[[:space:]]+-transport([[:space:]]|$)'; then + DAEMON_HAS_TRANSPORT=true + # -transport=auto: its usage line names 'auto'. + if printf '%s\n' "$_daemon_help" | sed -n '/^[[:space:]]*-transport/,/^[[:space:]]*-[a-z]/p' | grep -q "'auto'"; then + DAEMON_HAS_AUTO=true + fi +fi +if printf '%s\n' "$_daemon_help" | grep -qE '^[[:space:]]+-proxy([[:space:]]|$)'; then + DAEMON_HAS_PROXY=true +fi +DAEMON_HAS_PROXY_CMD=false +if printf '%s\n' "$_daemon_help" | grep -qE '^[[:space:]]+-proxy-cmd([[:space:]]|$)'; then + DAEMON_HAS_PROXY_CMD=true +fi + +# auto is never saved in config.json. It is already the default wherever +# this install starts the daemon — `pilotctl daemon start` asks a daemon +# that supports it for auto, and the service units below set +# PILOT_TRANSPORT_DEFAULT=auto — while a pilot-daemon that predates auto +# (reinstalled with --version, or `pilotctl update --pin`) refuses to start +# with "transport":"auto" in config.json. udp and compat are saved. +TRANSPORT_TO_SAVE="" +TRANSPORT_CLEAR=false +case "$TRANSPORT" in + udp|compat) + TRANSPORT_TO_SAVE="$TRANSPORT" ;; + auto) + if [ "$DAEMON_HAS_AUTO" = true ]; then + if [ -n "$CONFIG_TRANSPORT" ]; then TRANSPORT_CLEAR=true; fi + else + echo " Note: this pilot-daemon (${TAG:-source}) predates -transport=auto; it keeps its default (udp)." + fi ;; +esac +if [ "$CONFIG_TRANSPORT" = "auto" ] && [ "$DAEMON_HAS_AUTO" != true ] && [ -z "$TRANSPORT_TO_SAVE" ]; then + # Downgrade: this daemon would exit with "invalid -transport auto". + TRANSPORT_TO_SAVE="udp" + echo " Note: this pilot-daemon (${TAG:-source}) predates -transport=auto, which config.json" + echo " selects; switching it to udp (the daemon's default) so the daemon still starts." +fi + +if [ -n "$TRANSPORT_TO_SAVE" ]; then + if pilot_config_set "transport=$TRANSPORT_TO_SAVE"; then + echo "Transport set to ${TRANSPORT_TO_SAVE} in ${PILOT_DIR}/config.json" + else + echo " Note: could not save transport=${TRANSPORT_TO_SAVE} — run: pilotctl config --set transport=${TRANSPORT_TO_SAVE}" + fi +elif [ "$TRANSPORT_CLEAR" = true ]; then + if pilot_config_set "transport="; then + echo "Transport: auto (the default; removed \"transport\" from ${PILOT_DIR}/config.json)" + fi +fi + +# What the daemon will run: the saved transport, else auto where the +# daemon supports it, else its default (udp). +if [ -n "$TRANSPORT_TO_SAVE" ]; then + EFFECTIVE_TRANSPORT="$TRANSPORT_TO_SAVE" +elif [ "$TRANSPORT_CLEAR" != true ] && [ -n "$CONFIG_TRANSPORT" ] && [ "$CONFIG_TRANSPORT" != "auto" ]; then + EFFECTIVE_TRANSPORT="$CONFIG_TRANSPORT" +elif [ "$DAEMON_HAS_AUTO" = true ]; then + EFFECTIVE_TRANSPORT="auto" +else + EFFECTIVE_TRANSPORT="udp" +fi + +# Rotating proxy credentials. Hosted agent sandboxes (Meta Muse) put the +# proxy credentials in HTTPS_PROXY and rotate them every few minutes; a +# long-running daemon keeps the launch-time ones and new connections start +# failing with 407. proxy_cmd makes the daemon re-read the URL (every 60s +# and on a 407) from a command — here a fresh shell, which sees the current +# value. PILOT_PROXY_CMD sets it explicitly; otherwise it is saved only in a +# Linux container/VM without systemd whose proxy carries credentials, and +# never over an existing proxy_cmd. +# shellcheck disable=SC2016 # literal: the fresh bash expands it, not this shell +SANDBOX_PROXY_CMD='bash -c '\''printf %s "${https_proxy:-$HTTPS_PROXY}"'\''' +PROXY_CMD_TO_SAVE="${PILOT_PROXY_CMD:-}" +if [ -z "$PROXY_CMD_TO_SAVE" ] && [ "$OS" = "linux" ] && [ ! -d /run/systemd/system ] \ + && command -v bash >/dev/null 2>&1 \ + && ! grep -q '"proxy_cmd"' "$PILOT_DIR/config.json" 2>/dev/null; then + case "$PILOT_PROXY_URL" in + *@*) PROXY_CMD_TO_SAVE="$SANDBOX_PROXY_CMD" ;; + esac +fi +if [ -n "$PROXY_CMD_TO_SAVE" ]; then + if [ "$DAEMON_HAS_PROXY_CMD" != true ]; then + echo " Note: this pilot-daemon (${TAG:-source}) predates -proxy-cmd; if the proxy rotates its" + echo " credentials, restart the daemon from a fresh shell when it starts failing." + elif pilot_config_set "proxy_cmd=$PROXY_CMD_TO_SAVE"; then + echo "Proxy credentials: re-read by the daemon via proxy_cmd (${PILOT_DIR}/config.json)" + fi +fi +PROXY_CMD_SAVED=false +if grep -q '"proxy_cmd"' "$PILOT_DIR/config.json" 2>/dev/null; then + PROXY_CMD_SAVED=true +fi + +if [ "$EFFECTIVE_TRANSPORT" = "compat" ]; then + # No "proxy" key is written: the daemon's default, auto, already uses + # $HTTPS_PROXY / $ALL_PROXY in compat mode, and a saved "auto" would only + # get in the way of a proxy passed later with --proxy or $PILOT_PROXY. + if [ "$DAEMON_HAS_TRANSPORT" != true ]; then + echo "" + echo " WARNING: this pilot-daemon (${TAG:-source}) predates compat mode (-transport)." + echo " It will keep using UDP. Re-run without --version to get the latest release." + fi + + # pilotctl releases before --transport forward config.json's registry to + # the daemon verbatim, and a daemon given the raw-TCP default explicitly + # stays on it even in compat mode. Point such installs at the compat TLS + # registry directly — only when the file still holds the stock default. + if ! "$BIN_DIR/pilotctl" daemon start --help 2>&1 | grep -q -- '--transport' \ + && grep -q "\"registry\"[[:space:]]*:[[:space:]]*\"${DEFAULT_REGISTRY}\"" "$PILOT_DIR/config.json" 2>/dev/null; then + if pilot_config_set "registry=${COMPAT_REGISTRY}"; then + echo " Registry set to ${COMPAT_REGISTRY} for compat mode (this pilotctl" + echo " always passes config.json's registry to the daemon). Switching back to" + echo " UDP later: re-run this installer with --transport udp" + fi + fi +elif grep -q "\"registry\"[[:space:]]*:[[:space:]]*\"${COMPAT_REGISTRY}\"" "$PILOT_DIR/config.json" 2>/dev/null; then + # Leaving compat after an install that pointed the registry at the + # compat TLS host: a udp daemon needs the raw-TCP registry back. + if pilot_config_set "registry=${DEFAULT_REGISTRY}"; then + echo " Registry restored to ${DEFAULT_REGISTRY} for transport ${EFFECTIVE_TRANSPORT}" + fi +fi + +if [ "$EFFECTIVE_TRANSPORT" != "udp" ] && [ -n "$PILOT_PROXY_URL" ] && [ "$DAEMON_HAS_PROXY" != true ]; then + echo "" + echo " WARNING: HTTPS_PROXY is set, but this pilot-daemon (${TAG:-source}) cannot use a" + echo " proxy. Where the proxy is the only way out, the daemon will not come" + echo " online. Install a release whose 'pilot-daemon -help' lists -proxy." +fi + +# Network flags for the service units. The transport itself comes from +# config.json, which the daemon reads, so `pilotctl config --set transport=` +# applies to the service too. In compat mode the raw-TCP default +# registry/beacon are left off (an older daemon given -registry explicitly +# stays pinned to a port no 443-only network or HTTPS proxy will carry); a +# custom PILOT_REGISTRY / PILOT_BEACON is kept. +if [ "$EFFECTIVE_TRANSPORT" = "compat" ] && [ "$DAEMON_HAS_TRANSPORT" = true ]; then + NET_FLAGS="" + if [ "$REGISTRY" != "$DEFAULT_REGISTRY" ]; then NET_FLAGS="$NET_FLAGS -registry $REGISTRY"; fi + if [ "$BEACON" != "$DEFAULT_BEACON" ]; then NET_FLAGS="$NET_FLAGS -beacon $BEACON"; fi + NET_FLAGS="${NET_FLAGS# }" +else + NET_FLAGS="-registry $REGISTRY -beacon $BEACON" +fi + +# The service units ask for transport auto through PILOT_TRANSPORT_DEFAULT: +# it applies only when neither -transport, $PILOT_TRANSPORT nor config.json +# chooses, and a daemon that predates auto ignores it (a -transport auto +# flag would stop it from starting after a downgrade). +UNIT_ENV="" +PLIST_ENV="" +if [ "$DAEMON_HAS_AUTO" = true ]; then + UNIT_ENV=" +Environment=PILOT_TRANSPORT_DEFAULT=auto" + PLIST_ENV=" EnvironmentVariables + + PILOT_TRANSPORT_DEFAULT + auto + +" +fi + +# service_proxy_note UNIT — a service manager starts the daemon with its own +# environment, not this shell's, so an HTTPS_PROXY exported here never +# reaches it. config.json (0600, read by the daemon itself) does. +service_proxy_note() { + if [ "$EFFECTIVE_TRANSPORT" != "udp" ] && [ -n "$PILOT_PROXY_URL" ] \ + && ! grep -q '"proxy"[[:space:]]*:[[:space:]]*"http' "$PILOT_DIR/config.json" 2>/dev/null; then + echo " Note: $1 does not inherit this shell's HTTPS_PROXY. For the service to use" + echo " the proxy, save it in config.json (0600):" + echo " pilotctl config --set proxy=''" + fi +} + # Enable background auto-updates by default (opt-out). The install output and # the systemd/launchd units below promise the updater keeps binaries current; # the pilot-updater treats a MISSING control file as "disabled", so without @@ -1168,10 +1482,9 @@ Wants=network-online.target [Service] Type=simple -User=$(whoami) +User=$(whoami)${UNIT_ENV} ExecStart=${BIN_DIR}/pilot-daemon \\ - -registry ${REGISTRY} \\ - -beacon ${BEACON} \\ + ${NET_FLAGS} \\ -listen :4000 \\ -socket /tmp/pilot.sock \\ -identity ${PILOT_DIR}/identity.json \\ @@ -1219,6 +1532,7 @@ USVC if [ "$PILOT_MANAGED_MODE" != "1" ] && [ -f "$BIN_DIR/pilot-updater" ]; then echo " Service: pilot-updater.service (auto-updates)" fi + service_proxy_note "pilot-daemon.service" # Auto-enable + start the updater so future releases land without # operator action. The unit file alone is not enough — without this, @@ -1267,14 +1581,21 @@ USVC fi else echo " Skipped systemd setup (run as root or with passwordless sudo to enable)" + if [ "$PILOT_MANAGED_MODE" != "1" ]; then + echo " Start the daemon without a service manager: pilotctl daemon start" + fi fi elif [ "$OS" = "linux" ]; then - # systemd is not the init system here (container / WSL / CI runner). - # There is no service to install — tell the agent the portable start path - # instead of silently leaving it with no daemon. + # systemd is not the init system here (container / WSL / CI runner / + # hosted agent sandbox). There is no service to install — tell the agent + # the portable start path instead of silently leaving it with no daemon. if [ "$PILOT_MANAGED_MODE" != "1" ]; then - echo "No systemd detected (container / WSL / CI) — start the daemon manually:" + echo "No systemd detected (container / WSL / CI / sandbox) — start the daemon manually:" echo " pilotctl daemon start" + if [ "$EFFECTIVE_TRANSPORT" != "udp" ]; then + echo " (transport=${EFFECTIVE_TRANSPORT}; start it from a shell that has HTTPS_PROXY" + echo " set if this host reaches the internet only through a proxy)" + fi fi fi @@ -1306,6 +1627,13 @@ if [ "$OS" = "darwin" ]; then # empty value passes a blank argv element to the daemon; omitting # it lets the daemon do the documented thing instead — fall back to # ~/.pilot/account.json, then synthesise a fingerprint identity. + # One per word of NET_FLAGS (host:port / flag names only — + # validate_safe already rejected anything with spaces or markup). + PLIST_NET_ARGS="" + for _a in $NET_FLAGS; do + PLIST_NET_ARGS="${PLIST_NET_ARGS} ${_a} +" + done EXTRA_ARGS="" if [ -n "$EMAIL" ]; then EXTRA_ARGS="${EXTRA_ARGS} -email @@ -1331,11 +1659,7 @@ if [ "$OS" = "darwin" ]; then ProgramArguments ${BIN_DIR}/pilot-daemon - -registry - ${REGISTRY} - -beacon - ${BEACON} - -listen +${PLIST_NET_ARGS} -listen :4000 -socket /tmp/pilot.sock @@ -1343,7 +1667,7 @@ if [ "$OS" = "darwin" ]; then ${PILOT_DIR}/identity.json -encrypt ${EXTRA_ARGS} - RunAtLoad +${PLIST_ENV} RunAtLoad KeepAlive @@ -1396,6 +1720,7 @@ UPLIST if [ "$PILOT_MANAGED_MODE" != "1" ] && [ -f "$BIN_DIR/pilot-updater" ]; then echo " Service: network.pilotprotocol.pilot-updater (auto-updates)" fi + service_proxy_note "the launchd agent" # Auto-load the updater LaunchAgent so future releases land without # operator action. Without this, install.sh writes the plist but leaves @@ -1591,8 +1916,23 @@ echo " pilotctl ${BIN_DIR}/pilotctl" [ -f "$BIN_DIR/pilot-updater" ] && echo " pilot-updater ${BIN_DIR}/pilot-updater (auto-updates in background)" echo "" echo "Config: ${PILOT_DIR}/config.json" -echo " Registry: ${REGISTRY}" -echo " Beacon: ${BEACON}" +case "$EFFECTIVE_TRANSPORT" in + compat) + echo " Transport: compat (registry ${COMPAT_REGISTRY} over TLS, beacon over WSS)" ;; + auto) + echo " Transport: auto (UDP when it works, else compat over TCP 443)" + echo " Registry: ${REGISTRY}" + echo " Beacon: ${BEACON}" ;; + *) + echo " Registry: ${REGISTRY}" + echo " Beacon: ${BEACON}" ;; +esac +if [ "$EFFECTIVE_TRANSPORT" != "udp" ] && [ -n "$PILOT_PROXY_URL" ]; then + echo " Proxy: auto -> $(redact_proxy "$PILOT_PROXY_URL") (from environment)" + if [ "$PROXY_CMD_SAVED" = true ]; then + echo " credentials re-read by the daemon (proxy_cmd): rotation needs no restart" + fi +fi echo " Socket: /tmp/pilot.sock" echo " Identity: ${PILOT_DIR}/identity.json" echo " Email: ${EMAIL}" diff --git a/tests/proxy-transport-install.sh b/tests/proxy-transport-install.sh new file mode 100644 index 0000000..9dbf03e --- /dev/null +++ b/tests/proxy-transport-install.sh @@ -0,0 +1,271 @@ +#!/bin/sh +# Contract test for the transport / proxy / sandbox handling in install.sh: +# - root is allowed in a Linux container/VM without systemd (hosted agent +# sandboxes such as Meta Muse run the agent as root), refused elsewhere; +# - --transport udp|compat is saved in config.json, auto never is, and +# --transport auto removes a saved transport; +# - proxy_cmd is saved in such a sandbox when HTTPS_PROXY carries +# credentials (the daemon re-reads rotating proxy credentials with it); +# - a saved transport=auto is rewritten to udp for a daemon that predates it; +# - proxy credentials never reach the installer output. +# +# Like tests/managed-install.sh it installs a fixture release through a fake +# curl, so it needs no network. It never uses sudo (a fake sudo fails), and the +# root cases fake `id -u`; run it in a disposable container to also exercise +# the /usr/local/bin links as real root. +# The per-run environment is set in subshells on purpose (see run_install). +# shellcheck disable=SC2030,SC2031 +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname "$0")/.." && pwd) +WORK=$(mktemp -d "${TMPDIR:-/tmp}/pilot-proxy-install-test.XXXXXX") +FIXTURE="$WORK/fixture" +FAKEBIN="$WORK/fakebin" +mkdir -p "$FIXTURE/new/archive" "$FIXTURE/old/archive" "$FAKEBIN" + +fail() { + echo "FAIL: $*" >&2 + [ -n "${LOG:-}" ] && [ -f "$LOG" ] && sed 's/^/ | /' "$LOG" >&2 + exit 1 +} + +# --- Fixture binaries --------------------------------------------------------- + +# pilot-daemon: only -help matters to the installer. The "new" daemon has +# -transport (with 'auto'), -proxy and -proxy-cmd; the "old" one predates auto, +# -proxy and -proxy-cmd (v1.13.x). +cat > "$FIXTURE/new/archive/daemon" <<'SH' +#!/bin/sh +cat <<'HELP' +Usage of pilot-daemon: + -proxy string + Outbound proxy: 'auto', 'off' or an http(s):// URL. Env: PILOT_PROXY. + -proxy-cmd string + Command printing the current proxy URL. Env: PILOT_PROXY_CMD. + -registry string + registry server address + -transport string + Tunnel transport: 'udp', 'compat' or 'auto' +HELP +exit 0 +SH +cat > "$FIXTURE/old/archive/daemon" <<'SH' +#!/bin/sh +cat <<'HELP' +Usage of pilot-daemon: + -registry string + registry server address + -transport string + Tunnel transport: 'udp' (default) or 'compat' (TCP/443) +HELP +exit 0 +SH +# pilotctl: `config --set key=value` edits $HOME/.pilot/config.json the way the +# real one does (an empty value removes the key); everything else succeeds. +cat > "$FIXTURE/new/archive/pilotctl" <<'SH' +#!/bin/sh +case "$*" in + "config --set "*) + kv="${3:-}" + python3 - "$HOME/.pilot/config.json" "$kv" <<'PY' +import json, os, sys +path, kv = sys.argv[1], sys.argv[2] +k, _, v = kv.partition("=") +try: + with open(path) as f: + cfg = json.load(f) +except FileNotFoundError: + cfg = {} +if v == "": + cfg.pop(k, None) +else: + cfg[k] = v +with open(path, "w") as f: + json.dump(cfg, f, indent=2) +PY + ;; + "daemon start --help") + echo " --transport " + ;; + version) + echo v9.9.9 + ;; + *) + exit 0 + ;; +esac +SH +cp "$FIXTURE/new/archive/pilotctl" "$FIXTURE/old/archive/pilotctl" +chmod 755 "$FIXTURE"/new/archive/* "$FIXTURE"/old/archive/* + +make_release() { # make_release + COPYFILE_DISABLE=1 tar -czf "$1/pilot-linux-amd64.tar.gz" -C "$1/archive" . + _sha=$(shasum -a 256 "$1/pilot-linux-amd64.tar.gz" | awk '{print $1}') + printf '%s %s\n' "$_sha" pilot-linux-amd64.tar.gz > "$1/checksums.txt" + cat > "$1/stable-manifest.json" < "$FAKEBIN/uname" <<'SH' +#!/bin/sh +case "${1:-}" in + -m) echo x86_64 ;; + *) echo Linux ;; +esac +SH +cat > "$FAKEBIN/curl" <<'SH' +#!/bin/sh +: "${PILOT_TEST_RELEASE:?}" +url="" +output="" +while [ "$#" -gt 0 ]; do + case "$1" in + -o) output="$2"; shift 2 ;; + --max-time|-w) shift 2 ;; + http://*|https://*) url="$1"; shift ;; + *) shift ;; + esac +done +[ -n "$output" ] || exit 89 +case "$url" in + */.well-known/latest.json) src="$PILOT_TEST_RELEASE/stable-manifest.json" ;; + */pilot-linux-amd64.tar.gz) src="$PILOT_TEST_RELEASE/pilot-linux-amd64.tar.gz" ;; + */checksums.txt) src="$PILOT_TEST_RELEASE/checksums.txt" ;; + *) echo "unexpected curl URL: $url" >&2; exit 88 ;; +esac +cp "$src" "$output" +SH +# Never escalate on the machine running the test. +printf '#!/bin/sh\nexit 1\n' > "$FAKEBIN/sudo" +# `id -u` answers $PILOT_TEST_UID when set. +REAL_ID=$(command -v id) +cat > "$FAKEBIN/id" < [installer args...] — the caller's +# environment passes through. Callers that set variables for one run do it in +# a subshell: under bash's POSIX mode (macOS sh) an assignment prefixing a +# function call outlives the call. +run_install() { + _home="$1"; _rel="$2"; LOG="$3"; shift 3 + mkdir -p "$_home" + PATH="$FAKEBIN:$PATH" HOME="$_home" PILOT_TEST_RELEASE="$_rel" \ + PILOT_EMAIL=ci@example.com \ + sh "$ROOT/install.sh" "$@" > "$LOG" 2>&1 +} + +cfg_get() { # cfg_get — prints the value, "" when absent + python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2], ""))' \ + "$1/.pilot/config.json" "$2" +} + +SECRET="s3cretPW" +PROXY="http://muse:${SECRET}@egress.test:3128" +# shellcheck disable=SC2016 # the literal command the installer saves +SANDBOX_CMD='bash -c '\''printf %s "${https_proxy:-$HTTPS_PROXY}"'\''' +unset HTTPS_PROXY https_proxy ALL_PROXY all_proxy PILOT_TRANSPORT PILOT_PROXY_CMD PILOT_ALLOW_ROOT 2>/dev/null || true + +# 1. --transport is validated. +if run_install "$WORK/h-bad" "$FIXTURE/new" "$WORK/bad.log" --transport quic; then + fail "--transport quic was accepted" +fi +grep -F "must be 'udp', 'compat' or 'auto'" "$WORK/bad.log" >/dev/null || fail "no --transport error" + +# 2. Default install: auto is not saved; no proxy -> no proxy_cmd. +run_install "$WORK/h-default" "$FIXTURE/new" "$WORK/default.log" || fail "default install" +[ -z "$(cfg_get "$WORK/h-default" transport)" ] || fail "default install saved a transport" +[ -z "$(cfg_get "$WORK/h-default" proxy_cmd)" ] || fail "default install saved proxy_cmd" +grep -F "Transport: auto" "$WORK/default.log" >/dev/null || fail "default install did not report transport auto" + +# 3. --transport compat is saved and survives a re-run without --transport; +# --transport auto then removes it. +run_install "$WORK/h-compat" "$FIXTURE/new" "$WORK/compat.log" --transport compat || fail "compat install" +[ "$(cfg_get "$WORK/h-compat" transport)" = compat ] || fail "--transport compat not saved" +run_install "$WORK/h-compat" "$FIXTURE/new" "$WORK/compat2.log" || fail "compat re-run" +[ "$(cfg_get "$WORK/h-compat" transport)" = compat ] || fail "re-run dropped transport=compat" +run_install "$WORK/h-compat" "$FIXTURE/new" "$WORK/compat3.log" --transport auto || fail "auto re-run" +[ -z "$(cfg_get "$WORK/h-compat" transport)" ] || fail "--transport auto did not remove the saved transport" + +# 4. Sandbox (Linux without systemd) with a credential-bearing HTTPS_PROXY: +# proxy_cmd is saved and the credentials never reach the output. +if [ ! -d /run/systemd/system ]; then + LOG="$WORK/sandbox.log" + (export HTTPS_PROXY="$PROXY" https_proxy="$PROXY" + run_install "$WORK/h-sandbox" "$FIXTURE/new" "$LOG") || fail "sandbox install" + [ "$(cfg_get "$WORK/h-sandbox" proxy_cmd)" = "$SANDBOX_CMD" ] || fail "proxy_cmd not saved in a sandbox: '$(cfg_get "$WORK/h-sandbox" proxy_cmd)'" + grep -F 'http://***@egress.test:3128' "$LOG" >/dev/null || fail "proxy not shown redacted" + if grep -F "$SECRET" "$LOG" "$WORK/h-sandbox/.pilot/config.json" >/dev/null; then + fail "proxy credentials leaked" + fi + + # An explicit PILOT_PROXY_CMD wins; an existing proxy_cmd is never replaced. + LOG="$WORK/sandbox2.log" + (export PILOT_PROXY_CMD='cat /run/proxy-url' HTTPS_PROXY="$PROXY" + run_install "$WORK/h-sandbox2" "$FIXTURE/new" "$LOG") || fail "PILOT_PROXY_CMD install" + [ "$(cfg_get "$WORK/h-sandbox2" proxy_cmd)" = 'cat /run/proxy-url' ] || fail "PILOT_PROXY_CMD not saved" + LOG="$WORK/sandbox3.log" + (export HTTPS_PROXY="$PROXY" + run_install "$WORK/h-sandbox2" "$FIXTURE/new" "$LOG") || fail "sandbox re-run" + [ "$(cfg_get "$WORK/h-sandbox2" proxy_cmd)" = 'cat /run/proxy-url' ] || fail "re-run replaced proxy_cmd" + + # A proxy without credentials has nothing to rotate. + LOG="$WORK/nocreds.log" + (export HTTPS_PROXY=http://egress.test:3128 + run_install "$WORK/h-nocreds" "$FIXTURE/new" "$LOG") || fail "no-creds install" + [ -z "$(cfg_get "$WORK/h-nocreds" proxy_cmd)" ] || fail "proxy_cmd saved for a proxy without credentials" + + # A daemon without -proxy-cmd gets a note instead. + LOG="$WORK/oldcmd.log" + (export HTTPS_PROXY="$PROXY" + run_install "$WORK/h-oldcmd" "$FIXTURE/old" "$LOG") || fail "old daemon sandbox install" + [ -z "$(cfg_get "$WORK/h-oldcmd" proxy_cmd)" ] || fail "proxy_cmd saved for a daemon without -proxy-cmd" + grep -F "predates -proxy-cmd" "$LOG" >/dev/null || fail "no -proxy-cmd note for an old daemon" + + # 5. Root: allowed in a Linux container/VM without systemd. + LOG="$WORK/root.log" + (export PILOT_TEST_UID=0 HTTPS_PROXY="$PROXY" + run_install "$WORK/h-root" "$FIXTURE/new" "$LOG") || fail "root install in a sandbox was refused" + grep -F "installing as root (no systemd" "$LOG" >/dev/null || fail "no root note" + [ -x "$WORK/h-root/.pilot/bin/pilotctl" ] || fail "root install did not install pilotctl" + [ "$(cfg_get "$WORK/h-root" proxy_cmd)" = "$SANDBOX_CMD" ] || fail "root sandbox install did not save proxy_cmd" +else + # 5b. Root on a host with systemd is still refused (PILOT_ALLOW_ROOT=1 overrides). + LOG="$WORK/root.log" + if (export PILOT_TEST_UID=0; run_install "$WORK/h-root" "$FIXTURE/new" "$LOG"); then + fail "root install on a systemd host was accepted" + fi + grep -F "refusing to install as root" "$LOG" >/dev/null || fail "no root refusal" + [ ! -e "$WORK/h-root/.pilot" ] || fail "refused root install left ~/.pilot behind" + echo "skip: sandbox cases need a host without /run/systemd/system" +fi + +# 6. Downgrade: a saved transport=auto is rewritten to udp for a daemon that +# predates auto (it would refuse to start). +mkdir -p "$WORK/h-down/.pilot" +printf '{\n "registry": "34.71.57.205:9000",\n "transport": "auto"\n}\n' > "$WORK/h-down/.pilot/config.json" +run_install "$WORK/h-down" "$FIXTURE/old" "$WORK/down.log" || fail "downgrade install" +[ "$(cfg_get "$WORK/h-down" transport)" = udp ] || fail "transport=auto not rewritten to udp for an old daemon" + +# 7. Leaving compat restores the raw-TCP registry an older compat install saved. +mkdir -p "$WORK/h-back/.pilot" +printf '{\n "registry": "registry.pilotprotocol.network:443",\n "transport": "compat"\n}\n' > "$WORK/h-back/.pilot/config.json" +run_install "$WORK/h-back" "$FIXTURE/new" "$WORK/back.log" --transport udp || fail "switch-back install" +[ "$(cfg_get "$WORK/h-back" transport)" = udp ] || fail "--transport udp not saved" +[ "$(cfg_get "$WORK/h-back" registry)" = "34.71.57.205:9000" ] || fail "raw registry not restored" + +rm -rf "$WORK" +echo "proxy/transport installer contract: ok" From 0102e2afe13205c269d229da03729cd6aae68813 Mon Sep 17 00:00:00 2001 From: Teodor Calin Date: Thu, 24 Sep 2026 09:04:52 +0300 Subject: [PATCH 2/5] installer: no raw-IP registry behind a proxy, rotation-safe downloads, clean onboarding Proxy / sandbox (Meta Muse: root, no systemd, rotating HTTPS_PROXY credentials): - Fresh config.json leaves out the stock raw-TCP registry and UDP beacon (34.71.57.205) when the node runs compat, or auto behind a proxy; the daemon applies the endpoint that fits its transport. A compat install with a pilotctl that predates --transport gets registry.pilotprotocol.network:443 by name (it forwards config.json's registry verbatim). Service units omit the stock endpoints in the same cases. Custom PILOT_REGISTRY/PILOT_BEACON are always kept. - Downloads survive a credential rotation mid-install: pcurl retries once after re-reading the proxy URL from $PILOT_PROXY_CMD or, in a sandbox, a fresh bash (only this process's environment changes; nothing is printed or written). - The sandbox proxy_cmd is the credential-preferring command pilotctl uses (pilotprotocol#470): whichever of $https_proxy/$HTTPS_PROXY carries credentials. It is saved also for a daemon that predates -proxy-cmd (it ignores the key until upgraded), with a note. - PILOT_PROXY (http/https URL) carries the downloads when no *_PROXY is set. - A proxy the installed daemon cannot use (v1.13.x) now gets a warning in every transport, pointing at the pilot-sandbox recipe instead of a release that does not exist yet; the no-systemd start hint refers to it. - The service-unit proxy note no longer suggests writing a credentialed URL without saying so, and offers proxy_cmd. - The `daemon start --help` probe runs only for compat with a daemon that has -transport (v1.11+; per-command help exists since v1.10), so it can never start a daemon. Onboarding text: - Replies are read from `send-message --wait` output (JSON: .data.reply), not "the newest inbox message", which may answer an older question. - `appstore install` examples drop the routine --force (it reinstalls over the app and can delete its saved state). - A blank "Email:" line now says what the daemon uses; the manual pointer names `pilotctl skills` instead of one harness path. Tests: tests/proxy-transport-install.sh adds the rotation retry (BASH_ENV stands in for the sandbox), no raw-IP endpoint for compat/auto behind a proxy, old-pilotctl compat, PILOT_PROXY downloads, root refused on macOS, --help range, and no credentials in output or ~/.pilot. Mutation-checked. Co-Authored-By: Claude Opus 5.5 (1M context) --- install.sh | 428 +++++++++++++++++++++---------- tests/proxy-transport-install.sh | 131 +++++++++- 2 files changed, 422 insertions(+), 137 deletions(-) diff --git a/install.sh b/install.sh index f6dd4b9..adfac3f 100755 --- a/install.sh +++ b/install.sh @@ -57,7 +57,8 @@ set -e # systemd whose HTTPS_PROXY carries credentials # (hosted agent sandboxes such as Meta Muse), the # installer saves one that reads a fresh shell's -# $https_proxy when none is set. +# $https_proxy when none is set. The installer +# also uses it to retry a failed download. # PILOT_ALLOW_ROOT=1 Install as root on a host with systemd/launchd # (not needed in containers/VMs without systemd). # PILOT_MANAGEMENT_URL=https://management.example @@ -66,9 +67,11 @@ set -e # # Proxies: every download is a curl HTTPS request, so HTTPS_PROXY / https_proxy / # ALL_PROXY / NO_PROXY are honored (curl asks the proxy to CONNECT by hostname — -# no local DNS lookup of the target). Nothing here needs UDP, a non-443 port, or -# a direct connection to the registry/beacon. Steps that need root, sudo, -# systemd or launchd are skipped with a message, never fatal. +# no local DNS lookup of the target); a PILOT_PROXY http(s):// URL is used when +# none of those is set. Nothing here needs UDP, a non-443 port, or a direct +# connection to the registry/beacon. Proxy credentials are never printed or +# written to disk. Steps that need root, sudo, systemd or launchd are skipped +# with a message, never fatal. # # WHAT THIS SCRIPT DOES (read before piping to sh): # 1. Detects OS/arch (Linux/Darwin × amd64/arm64) @@ -104,7 +107,9 @@ set -e # Network 9 directory and for receiving identifier-based deliveries. # # WHAT THIS SCRIPT DOES NOT DO: -# - Run as root (refuses if invoked as root; see check at line ~25) +# - Run as root on a host with systemd or launchd (refuses; see the root +# check below). A Linux container/VM without systemd, where the agent is +# root, installs into root's own $HOME/.pilot. # - Send any personal data anywhere (the install script only fetches the # release tarball from GitHub; the daemon registers its public key + a # synthetic or user-supplied email with the rendezvous server, nothing else) @@ -213,7 +218,7 @@ while [ $# -gt 0 ]; do --no-start) PILOT_MANAGED_NO_START=1; shift ;; -h|--help) - sed -n '4,71p' "$0" 2>/dev/null || echo "See https://pilotprotocol.network/install.sh" + sed -n '4,74p' "$0" 2>/dev/null || echo "See https://pilotprotocol.network/install.sh" exit 0 ;; --) shift @@ -326,8 +331,12 @@ set -- $PILOT_POSITIONAL # hosted agent sandboxes such as Meta Muse, where the agent IS root) has no # other user to install for and no system service to protect, so root is # allowed there. +SANDBOX_HOST=false +if [ "$(uname -s)" = "Linux" ] && [ ! -d /run/systemd/system ]; then + SANDBOX_HOST=true +fi if [ "${1:-}" != "uninstall" ] && [ "$(id -u)" = "0" ] && [ -z "${PILOT_ALLOW_ROOT:-}" ]; then - if [ "$(uname -s)" = "Linux" ] && [ ! -d /run/systemd/system ]; then + if [ "$SANDBOX_HOST" = true ]; then echo "Note: installing as root (no systemd: container/VM sandbox) into ${HOME}/.pilot" else echo "Error: refusing to install as root." @@ -396,8 +405,80 @@ EFFECTIVE_TRANSPORT="${TRANSPORT:-${CONFIG_TRANSPORT:-auto}}" # by hostname (no local DNS lookup of the target — which matters where local # DNS for pilotprotocol.network is poisoned). PILOT_PROXY_URL is only used in # messages, and only ever printed redacted: the userinfo of an -# authenticating proxy is a credential. -PILOT_PROXY_URL="${HTTPS_PROXY:-${https_proxy:-${ALL_PROXY:-${all_proxy:-}}}}" +# authenticating proxy is a credential. Nothing in this script writes a proxy +# URL to disk. +# +# PILOT_PROXY is the daemon's own proxy setting; an http(s):// URL there +# carries this run's downloads too when the environment names no proxy +# (exported to this process and its children only). +if [ -z "${https_proxy:-}${HTTPS_PROXY:-}${all_proxy:-}${ALL_PROXY:-}" ]; then + case "${PILOT_PROXY:-}" in + http://*|https://*|HTTP://*|HTTPS://*) + https_proxy="$PILOT_PROXY" + HTTPS_PROXY="$PILOT_PROXY" + export https_proxy HTTPS_PROXY ;; + esac +fi +# The order curl uses for an https:// URL. +PILOT_PROXY_URL="${https_proxy:-${HTTPS_PROXY:-${all_proxy:-${ALL_PROXY:-}}}}" + +# Rotating proxy credentials. Hosted agent sandboxes (Meta Muse) put the proxy +# credentials in HTTPS_PROXY and replace them every few minutes; a process +# keeps the ones it started with, and the proxy answers its next CONNECT with +# 407. A fresh shell sees the current ones. PROXY_REFRESH_CMD prints the +# current proxy URL: $PILOT_PROXY_CMD, else — in a Linux container/VM without +# systemd whose HTTPS_PROXY or https_proxy carries credentials — what a fresh +# bash has: whichever of $https_proxy and $HTTPS_PROXY carries credentials +# ($https_proxy when both do, the variable Meta Muse's guidance reads), else +# ${HTTPS_PROXY:-$https_proxy}, so a URL with credentials is never traded for +# one without (pilotctl uses the same command). It is saved as the daemon's +# proxy_cmd further down, and pcurl uses it here to retry a download once +# after the credentials rotated mid-install. +# shellcheck disable=SC2016 # literal: the fresh bash expands it, not this shell +SANDBOX_PROXY_CMD='bash -c '\''case $https_proxy in *@*) printf %s "$https_proxy";; *) printf %s "${HTTPS_PROXY:-$https_proxy}";; esac'\''' +PROXY_REFRESH_CMD="${PILOT_PROXY_CMD:-}" +if [ -z "$PROXY_REFRESH_CMD" ] && [ "$SANDBOX_HOST" = true ] \ + && command -v bash >/dev/null 2>&1; then + case "${https_proxy:-}${HTTPS_PROXY:-}" in + *@*) PROXY_REFRESH_CMD="$SANDBOX_PROXY_CMD" ;; + esac +fi + +# proxy_refresh — run PROXY_REFRESH_CMD (at most 10s where `timeout` exists) +# and, when it prints an http(s):// URL different from the current one, use +# that for the rest of this run. The URL is never printed; only this process's +# environment changes. Returns 0 when the proxy URL changed. +proxy_refresh() { + [ -n "$PROXY_REFRESH_CMD" ] || return 1 + if command -v timeout >/dev/null 2>&1; then + _pr_url=$(timeout 10 sh -c "$PROXY_REFRESH_CMD" 2>/dev/null /dev/null /dev/null + pcurl -fsSL --max-time 10 "$MANIFEST_URL" -o "$1" 2>/dev/null } # manifest_field "" "" extracts a string field. Supports nested @@ -628,8 +709,10 @@ case "$EFFECTIVE_TRANSPORT" in echo " Transport: compat (TLS + WSS over TCP 443 only)" ;; auto) echo " Transport: auto (UDP when it works, else TLS + WSS over TCP 443)" - echo " Registry: ${REGISTRY}" - echo " Beacon: ${BEACON}" ;; + if [ -z "$PILOT_PROXY_URL" ]; then + echo " Registry: ${REGISTRY}" + echo " Beacon: ${BEACON}" + fi ;; *) echo " Registry: ${REGISTRY}" echo " Beacon: ${BEACON}" ;; @@ -779,7 +862,7 @@ elif [ "${PILOT_RC:-}" = "1" ]; then elif [ "$HAVE_MANIFEST" = "1" ]; then TAG=$(manifest_field "latest_stable" "$MANIFEST_FILE") else - TAG=$(curl -fsSI "/${REPO}/releases/latest/download/${ARCHIVE}" 2>/dev/null \ + TAG=$(pcurl -fsSI "/${REPO}/releases/latest/download/${ARCHIVE}" 2>/dev/null \ | grep -i '^location:' \ | sed -n 's|.*/releases/download/\([^/]*\)/.*|\1|p' \ | tr -d '\r' | head -1) @@ -853,7 +936,7 @@ if [ -n "$TAG" ]; then URL="/${REPO}/releases/download/${TAG}/${ARCHIVE}" CHECKSUMS_URL="/${REPO}/releases/download/${TAG}/checksums.txt" echo "Downloading ${TAG}..." - if curl -fsSL "$URL" -o "$TMPDIR/$ARCHIVE" 2>/dev/null; then + if pcurl -fsSL "$URL" -o "$TMPDIR/$ARCHIVE" 2>/dev/null; then # --- Verify SHA-256 (fail closed) --- # This block NEVER extracts an archive it could not verify. Two # independent anchors are used: @@ -867,7 +950,7 @@ if [ -n "$TAG" ]; then # archive line, or the absence of shasum/sha256sum silently extracted # the archive UNVERIFIED.) EXPECTED_CKS="" - if curl -fsSL "$CHECKSUMS_URL" -o "$TMPDIR/checksums.txt" 2>/dev/null; then + if pcurl -fsSL "$CHECKSUMS_URL" -o "$TMPDIR/checksums.txt" 2>/dev/null; then EXPECTED_CKS=$(grep " ${ARCHIVE}\$" "$TMPDIR/checksums.txt" | awk '{print $1}') fi EXPECTED_MAN="" @@ -1185,49 +1268,19 @@ if [ "$LINK_OK" = true ]; then echo " pilotctl now resolves in non-interactive shells (bash -c, cron, CI, agents)" fi -# --- Fresh install: write config --- -# -# config.json is written ONLY when there isn't one already. A re-run must never -# clobber registry/beacon/consent settings the operator edited by hand. -# -# The UPDATING check alone did not deliver that promise: UPDATING is derived -# purely from `[ -x "$BIN_DIR/pilotctl" ]`, i.e. whether the BINARY exists. A -# host with a hand-edited ~/.pilot/config.json but no binary — binaries removed -# for a clean reinstall, config restored from backup, or a config pre-seeded -# before first install — took the "fresh install" branch and had its config -# silently overwritten. +# --- What the installed binaries support --- # -# That also made consent settings impossible to set BEFORE first start: -# pre-seeding {"consent":{...}} or {"skill_inject":{"mode":"disabled"}} was -# erased by this write, and the erase happened before the first skills pass -# further below. Guarding on the file itself makes the documented opt-outs -# reachable at install time instead of only after the fact. Defaults are -# unchanged — a host with no config still gets the standard one. -if [ "$UPDATING" != true ] && [ ! -f "$PILOT_DIR/config.json" ]; then - cat > "$PILOT_DIR/config.json" </dev/null 2>&1 } -# What the installed binaries support. The probes are local (no network). +# The probes are local (no network). DAEMON_HAS_TRANSPORT=false DAEMON_HAS_PROXY=false DAEMON_HAS_AUTO=false @@ -1247,6 +1300,8 @@ if printf '%s\n' "$_daemon_help" | grep -qE '^[[:space:]]+-proxy-cmd([[:space:]] DAEMON_HAS_PROXY_CMD=true fi +# --- Transport: auto, udp or compat --- +# # auto is never saved in config.json. It is already the default wherever # this install starts the daemon — `pilotctl daemon start` asks a daemon # that supports it for auto, and the service units below set @@ -1272,18 +1327,6 @@ if [ "$CONFIG_TRANSPORT" = "auto" ] && [ "$DAEMON_HAS_AUTO" != true ] && [ -z "$ echo " selects; switching it to udp (the daemon's default) so the daemon still starts." fi -if [ -n "$TRANSPORT_TO_SAVE" ]; then - if pilot_config_set "transport=$TRANSPORT_TO_SAVE"; then - echo "Transport set to ${TRANSPORT_TO_SAVE} in ${PILOT_DIR}/config.json" - else - echo " Note: could not save transport=${TRANSPORT_TO_SAVE} — run: pilotctl config --set transport=${TRANSPORT_TO_SAVE}" - fi -elif [ "$TRANSPORT_CLEAR" = true ]; then - if pilot_config_set "transport="; then - echo "Transport: auto (the default; removed \"transport\" from ${PILOT_DIR}/config.json)" - fi -fi - # What the daemon will run: the saved transport, else auto where the # daemon supports it, else its default (udp). if [ -n "$TRANSPORT_TO_SAVE" ]; then @@ -1296,30 +1339,123 @@ else EFFECTIVE_TRANSPORT="udp" fi -# Rotating proxy credentials. Hosted agent sandboxes (Meta Muse) put the -# proxy credentials in HTTPS_PROXY and rotate them every few minutes; a -# long-running daemon keeps the launch-time ones and new connections start -# failing with 407. proxy_cmd makes the daemon re-read the URL (every 60s -# and on a 407) from a command — here a fresh shell, which sees the current -# value. PILOT_PROXY_CMD sets it explicitly; otherwise it is saved only in a -# Linux container/VM without systemd whose proxy carries credentials, and -# never over an existing proxy_cmd. -# shellcheck disable=SC2016 # literal: the fresh bash expands it, not this shell -SANDBOX_PROXY_CMD='bash -c '\''printf %s "${https_proxy:-$HTTPS_PROXY}"'\''' -PROXY_CMD_TO_SAVE="${PILOT_PROXY_CMD:-}" -if [ -z "$PROXY_CMD_TO_SAVE" ] && [ "$OS" = "linux" ] && [ ! -d /run/systemd/system ] \ - && command -v bash >/dev/null 2>&1 \ - && ! grep -q '"proxy_cmd"' "$PILOT_DIR/config.json" 2>/dev/null; then - case "$PILOT_PROXY_URL" in - *@*) PROXY_CMD_TO_SAVE="$SANDBOX_PROXY_CMD" ;; +# pilotctl releases before `daemon start --transport` pass config.json's +# registry (else the raw-TCP default) to the daemon verbatim, and a daemon +# given the raw-TCP registry explicitly stays on it even in compat mode. +# Probed only for compat, and only with a daemon that has -transport (v1.11+): +# every pilotctl since v1.10 prints help for `daemon start --help` instead of +# starting a daemon. +PILOTCTL_HAS_TRANSPORT=false +if [ "$EFFECTIVE_TRANSPORT" = "compat" ] && [ "$DAEMON_HAS_TRANSPORT" = true ] \ + && "$BIN_DIR/pilotctl" daemon start --help 2>&1 | grep -q -- '--transport'; then + PILOTCTL_HAS_TRANSPORT=true +fi + +# The stock raw-TCP registry (34.71.57.205:9000) and UDP beacon are left out +# of what this installer writes when the node runs compat, or auto behind a +# proxy: the daemon then applies the endpoints that fit the transport it +# runs (registry.pilotprotocol.network:443 over TLS in compat mode or through +# a proxy), and an address that a 443-only network or HTTPS proxy never +# carries is not pinned anywhere. Custom PILOT_REGISTRY / PILOT_BEACON values +# are always kept. +STOCK_ENDPOINTS=true +if [ "$DAEMON_HAS_TRANSPORT" = true ]; then + case "$EFFECTIVE_TRANSPORT" in + compat) STOCK_ENDPOINTS=false ;; + auto) if [ -n "$PILOT_PROXY_URL" ]; then STOCK_ENDPOINTS=false; fi ;; esac fi + +# --- Fresh install: write config --- +# +# config.json is written ONLY when there isn't one already. A re-run must never +# clobber registry/beacon/consent settings the operator edited by hand. +# +# The UPDATING check alone did not deliver that promise: UPDATING is derived +# purely from `[ -x "$BIN_DIR/pilotctl" ]`, i.e. whether the BINARY exists. A +# host with a hand-edited ~/.pilot/config.json but no binary — binaries removed +# for a clean reinstall, config restored from backup, or a config pre-seeded +# before first install — took the "fresh install" branch and had its config +# silently overwritten. +# +# That also made consent settings impossible to set BEFORE first start: +# pre-seeding {"consent":{...}} or {"skill_inject":{"mode":"disabled"}} was +# erased by this write, and the erase happened before the first skills pass +# further below. Guarding on the file itself makes the documented opt-outs +# reachable at install time instead of only after the fact. Defaults are +# unchanged — a host with no config still gets the standard one (without the +# stock endpoints in compat mode or behind a proxy, see STOCK_ENDPOINTS). +if [ "$UPDATING" != true ] && [ ! -f "$PILOT_DIR/config.json" ]; then + CONF_REGISTRY="$REGISTRY" + CONF_BEACON="$BEACON" + if [ "$STOCK_ENDPOINTS" != true ]; then + if [ "$REGISTRY" = "$DEFAULT_REGISTRY" ]; then + CONF_REGISTRY="" + # A pilotctl that predates --transport would pass the raw + # default instead: name the compat TLS registry explicitly. + if [ "$EFFECTIVE_TRANSPORT" = "compat" ] && [ "$PILOTCTL_HAS_TRANSPORT" != true ]; then + CONF_REGISTRY="$COMPAT_REGISTRY" + fi + fi + if [ "$BEACON" = "$DEFAULT_BEACON" ]; then CONF_BEACON=""; fi + fi + CONF_NET="" + if [ -n "$CONF_REGISTRY" ]; then + CONF_NET="${CONF_NET} \"registry\": \"${CONF_REGISTRY}\", +" + fi + if [ -n "$CONF_BEACON" ]; then + CONF_NET="${CONF_NET} \"beacon\": \"${CONF_BEACON}\", +" + fi + cat > "$PILOT_DIR/config.json" </dev/null; then + PROXY_CMD_TO_SAVE="$SANDBOX_PROXY_CMD" +fi if [ -n "$PROXY_CMD_TO_SAVE" ]; then - if [ "$DAEMON_HAS_PROXY_CMD" != true ]; then - echo " Note: this pilot-daemon (${TAG:-source}) predates -proxy-cmd; if the proxy rotates its" - echo " credentials, restart the daemon from a fresh shell when it starts failing." - elif pilot_config_set "proxy_cmd=$PROXY_CMD_TO_SAVE"; then - echo "Proxy credentials: re-read by the daemon via proxy_cmd (${PILOT_DIR}/config.json)" + if ! pilot_config_set "proxy_cmd=$PROXY_CMD_TO_SAVE"; then + echo " Note: could not save proxy_cmd in ${PILOT_DIR}/config.json" + elif [ "$DAEMON_HAS_PROXY_CMD" = true ]; then + echo "Proxy credentials: re-read by the daemon via proxy_cmd (${PILOT_DIR}/config.json stores the command, not the credentials)" + else + echo "Proxy credentials: proxy_cmd saved in ${PILOT_DIR}/config.json (the command, not the credentials)." + echo " This pilot-daemon (${TAG:-source}) predates -proxy-cmd and ignores it until upgraded;" + echo " until then, if the proxy rotates its credentials, restart the daemon from a fresh shell." fi fi PROXY_CMD_SAVED=false @@ -1327,29 +1463,32 @@ if grep -q '"proxy_cmd"' "$PILOT_DIR/config.json" 2>/dev/null; then PROXY_CMD_SAVED=true fi +# --- Registry for the transport --- +# +# No "proxy" key is written: the daemon's default, auto, already uses +# $HTTPS_PROXY / $ALL_PROXY where it needs a proxy, and a saved "auto" would +# only get in the way of a proxy passed later with --proxy or $PILOT_PROXY. +CONFIG_REGISTRY=$(sed -n 's/.*"registry"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PILOT_DIR/config.json" 2>/dev/null | head -n 1) if [ "$EFFECTIVE_TRANSPORT" = "compat" ]; then - # No "proxy" key is written: the daemon's default, auto, already uses - # $HTTPS_PROXY / $ALL_PROXY in compat mode, and a saved "auto" would only - # get in the way of a proxy passed later with --proxy or $PILOT_PROXY. if [ "$DAEMON_HAS_TRANSPORT" != true ]; then echo "" echo " WARNING: this pilot-daemon (${TAG:-source}) predates compat mode (-transport)." echo " It will keep using UDP. Re-run without --version to get the latest release." fi - # pilotctl releases before --transport forward config.json's registry to - # the daemon verbatim, and a daemon given the raw-TCP default explicitly - # stays on it even in compat mode. Point such installs at the compat TLS - # registry directly — only when the file still holds the stock default. - if ! "$BIN_DIR/pilotctl" daemon start --help 2>&1 | grep -q -- '--transport' \ - && grep -q "\"registry\"[[:space:]]*:[[:space:]]*\"${DEFAULT_REGISTRY}\"" "$PILOT_DIR/config.json" 2>/dev/null; then + # A pilotctl that predates --transport passes config.json's registry, + # else the raw-TCP default, to the daemon verbatim: point it at the + # compat TLS registry — only when the file holds the stock default or + # no registry at all. + if [ "$DAEMON_HAS_TRANSPORT" = true ] && [ "$PILOTCTL_HAS_TRANSPORT" != true ] \ + && { [ "$CONFIG_REGISTRY" = "$DEFAULT_REGISTRY" ] || [ -z "$CONFIG_REGISTRY" ]; }; then if pilot_config_set "registry=${COMPAT_REGISTRY}"; then echo " Registry set to ${COMPAT_REGISTRY} for compat mode (this pilotctl" echo " always passes config.json's registry to the daemon). Switching back to" echo " UDP later: re-run this installer with --transport udp" fi fi -elif grep -q "\"registry\"[[:space:]]*:[[:space:]]*\"${COMPAT_REGISTRY}\"" "$PILOT_DIR/config.json" 2>/dev/null; then +elif [ "$CONFIG_REGISTRY" = "$COMPAT_REGISTRY" ]; then # Leaving compat after an install that pointed the registry at the # compat TLS host: a udp daemon needs the raw-TCP registry back. if pilot_config_set "registry=${DEFAULT_REGISTRY}"; then @@ -1357,20 +1496,28 @@ elif grep -q "\"registry\"[[:space:]]*:[[:space:]]*\"${COMPAT_REGISTRY}\"" "$PIL fi fi -if [ "$EFFECTIVE_TRANSPORT" != "udp" ] && [ -n "$PILOT_PROXY_URL" ] && [ "$DAEMON_HAS_PROXY" != true ]; then +# A proxy in the environment that this daemon cannot use: say so, and where +# the proxy is the only way out, point at the recipe that works with it. +PROXY_UNSUPPORTED=false +if [ -n "$PILOT_PROXY_URL" ] && [ "$DAEMON_HAS_PROXY" != true ]; then + PROXY_UNSUPPORTED=true echo "" - echo " WARNING: HTTPS_PROXY is set, but this pilot-daemon (${TAG:-source}) cannot use a" - echo " proxy. Where the proxy is the only way out, the daemon will not come" - echo " online. Install a release whose 'pilot-daemon -help' lists -proxy." + echo " WARNING: a proxy is set ($(redact_proxy "$PILOT_PROXY_URL")), but this pilot-daemon" + echo " (${TAG:-source}) cannot use one. If the proxy is this host's only way" + echo " out (UDP blocked, e.g. an agent sandbox), \`pilotctl daemon start\` will" + echo " not come online with this release. Use the pilot-sandbox recipe:" + echo " https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" fi # Network flags for the service units. The transport itself comes from # config.json, which the daemon reads, so `pilotctl config --set transport=` -# applies to the service too. In compat mode the raw-TCP default -# registry/beacon are left off (an older daemon given -registry explicitly -# stays pinned to a port no 443-only network or HTTPS proxy will carry); a -# custom PILOT_REGISTRY / PILOT_BEACON is kept. -if [ "$EFFECTIVE_TRANSPORT" = "compat" ] && [ "$DAEMON_HAS_TRANSPORT" = true ]; then +# applies to the service too. Where the stock endpoints are left out (compat, +# or auto behind a proxy; see STOCK_ENDPOINTS) the daemon's built-in defaults +# apply — the same raw-TCP/UDP endpoints for udp, the TLS registry for +# compat (an older daemon given -registry explicitly stays pinned to a port +# no 443-only network or HTTPS proxy will carry); a custom PILOT_REGISTRY / +# PILOT_BEACON is kept. +if [ "$STOCK_ENDPOINTS" != true ]; then NET_FLAGS="" if [ "$REGISTRY" != "$DEFAULT_REGISTRY" ]; then NET_FLAGS="$NET_FLAGS -registry $REGISTRY"; fi if [ "$BEACON" != "$DEFAULT_BEACON" ]; then NET_FLAGS="$NET_FLAGS -beacon $BEACON"; fi @@ -1399,12 +1546,21 @@ fi # service_proxy_note UNIT — a service manager starts the daemon with its own # environment, not this shell's, so an HTTPS_PROXY exported here never # reaches it. config.json (0600, read by the daemon itself) does. +# This installer never writes the URL itself: with credentials in it, where +# to store them is the operator's call. service_proxy_note() { if [ "$EFFECTIVE_TRANSPORT" != "udp" ] && [ -n "$PILOT_PROXY_URL" ] \ && ! grep -q '"proxy"[[:space:]]*:[[:space:]]*"http' "$PILOT_DIR/config.json" 2>/dev/null; then echo " Note: $1 does not inherit this shell's HTTPS_PROXY. For the service to use" - echo " the proxy, save it in config.json (0600):" - echo " pilotctl config --set proxy=''" + case "$PILOT_PROXY_URL" in + *@*) + echo " the proxy, save it in config.json (0600; this stores its credentials):" + echo " pilotctl config --set proxy=''" + echo " or save a command that prints it: pilotctl config --set proxy_cmd=''" ;; + *) + echo " the proxy, save it in config.json:" + echo " pilotctl config --set proxy='${PILOT_PROXY_URL}'" ;; + esac fi } @@ -1592,7 +1748,9 @@ elif [ "$OS" = "linux" ]; then if [ "$PILOT_MANAGED_MODE" != "1" ]; then echo "No systemd detected (container / WSL / CI / sandbox) — start the daemon manually:" echo " pilotctl daemon start" - if [ "$EFFECTIVE_TRANSPORT" != "udp" ]; then + if [ "$PROXY_UNSUPPORTED" = true ]; then + echo " (proxy-only host: see the WARNING above)" + elif [ "$EFFECTIVE_TRANSPORT" != "udp" ]; then echo " (transport=${EFFECTIVE_TRANSPORT}; start it from a shell that has HTTPS_PROXY" echo " set if this host reaches the internet only through a proxy)" fi @@ -1918,24 +2076,35 @@ echo "" echo "Config: ${PILOT_DIR}/config.json" case "$EFFECTIVE_TRANSPORT" in compat) - echo " Transport: compat (registry ${COMPAT_REGISTRY} over TLS, beacon over WSS)" ;; + _summary_registry="$COMPAT_REGISTRY" + if [ "$REGISTRY" != "$DEFAULT_REGISTRY" ]; then _summary_registry="$REGISTRY"; fi + echo " Transport: compat (registry ${_summary_registry} over TLS, beacon over WSS)" ;; auto) echo " Transport: auto (UDP when it works, else compat over TCP 443)" - echo " Registry: ${REGISTRY}" - echo " Beacon: ${BEACON}" ;; + if [ "$STOCK_ENDPOINTS" = true ]; then + echo " Registry: ${REGISTRY}" + echo " Beacon: ${BEACON}" + else + echo " Registry: picked by the daemon for its transport (${COMPAT_REGISTRY} over TLS via the proxy)" + fi ;; *) echo " Registry: ${REGISTRY}" echo " Beacon: ${BEACON}" ;; esac -if [ "$EFFECTIVE_TRANSPORT" != "udp" ] && [ -n "$PILOT_PROXY_URL" ]; then +if [ "$EFFECTIVE_TRANSPORT" != "udp" ] && [ -n "$PILOT_PROXY_URL" ] && [ "$DAEMON_HAS_PROXY" = true ]; then echo " Proxy: auto -> $(redact_proxy "$PILOT_PROXY_URL") (from environment)" - if [ "$PROXY_CMD_SAVED" = true ]; then + if [ "$PROXY_CMD_SAVED" = true ] && [ "$DAEMON_HAS_PROXY_CMD" = true ]; then echo " credentials re-read by the daemon (proxy_cmd): rotation needs no restart" fi fi echo " Socket: /tmp/pilot.sock" echo " Identity: ${PILOT_DIR}/identity.json" -echo " Email: ${EMAIL}" +if [ -n "$EMAIL" ]; then + echo " Email: ${EMAIL}" +else + echo " Email: not set (the daemon uses @nodes.pilotprotocol.network;" + echo " set a real one any time: pilotctl set-email )" +fi echo "" echo "============================================" echo " GET STARTED" @@ -1948,16 +2117,16 @@ cat <<'PILOT_GET_STARTED' pilotctl daemon status # confirm it's running pilotctl info # node ID, address, peer count, uptime - Reading any reply is always the same two-step idiom — send with - --wait, then print the newest reply. No jq, no extra dependency: + Ask with --wait: the reply comes back in the send's own output. If + it fails or times out there is no reply to read — do not substitute + the newest inbox message, which may answer an older question: ------------------------------------------------------------------ pilotctl send-message --data '' --wait - pilotctl inbox --latest # full body of the newest reply + pilotctl --json send-message --data '' --wait # reply in .data.reply - # Other ways to read the inbox: + # Every reply is also kept in the inbox: pilotctl inbox # 10 newest, one-line previews pilotctl inbox --from pilot-mom --limit 3 - pilotctl --json inbox --latest # machine-readable envelope 1) MOM (pilot-mom) — your default for anything LIVE or fresh. @@ -1965,7 +2134,6 @@ cat <<'PILOT_GET_STARTED' agents + filters and returns a validated, ready-to-run plan. ------------------------------------------------------------------ pilotctl send-message pilot-mom --data 'current weather and air quality for Berlin' --wait - pilotctl inbox --latest # More examples — hand it the whole sentence, don't hunt for agents yourself: pilotctl send-message pilot-mom --data 'summarize this season F1 drivers' --wait @@ -1980,16 +2148,13 @@ cat <<'PILOT_GET_STARTED' ------------------------------------------------------------------ # Browse the whole live catalogue: pilotctl send-message list-agents --data '/data' --wait - pilotctl inbox --latest # Search by keyword: pilotctl send-message list-agents --data '/data {"search":"bitcoin","limit":10}' --wait - pilotctl inbox --latest # Learn a specialist's query schema, then query it with filters: pilotctl send-message --data '/help' --wait pilotctl send-message --data '/data {"":""}' --wait - pilotctl inbox --latest @@ -2003,8 +2168,10 @@ cat <<'PILOT_GET_STARTED' # See an app's full details (methods, source, permissions, pricing): pilotctl appstore view io.pilot.sqlite - # Install it (daemon auto-spawns it; re-run `list` if state != ready): - pilotctl appstore install io.pilot.sqlite --force + # Install it once (daemon auto-spawns it; re-run `list` if state != ready). + # Do not add --force routinely: it reinstalls over the app and can + # delete its saved state (keys). + pilotctl appstore install io.pilot.sqlite pilotctl appstore list # ALWAYS call .help first — lists every method, its params, @@ -2015,13 +2182,13 @@ cat <<'PILOT_GET_STARTED' pilotctl appstore call io.pilot.sqlite sqlite.query '{"sql":"select 1"}' # A few concrete capability examples (install first, then call): - pilotctl appstore install io.pilot.smol --force + pilotctl appstore install io.pilot.smol pilotctl appstore call io.pilot.smol smol.push '{"image":"alpine","net":true}' - pilotctl appstore install io.pilot.bowmark --force + pilotctl appstore install io.pilot.bowmark pilotctl appstore call io.pilot.bowmark bowmark.ask '{"site":"amazon.com","task":"search for a product"}' - pilotctl appstore install io.pilot.orthogonal --force + pilotctl appstore install io.pilot.orthogonal pilotctl appstore call io.pilot.orthogonal orthogonal.search '{"prompt":"work email for a person given name + company"}' Cost: most apps run locally and are free. A few (orthogonal, sixtyfour, @@ -2040,8 +2207,9 @@ cat <<'PILOT_GET_STARTED' pilotctl send-message --data '' # talk, once trust is mutual pilotctl send-file /path/to/file.tar.gz # exchange artifacts - Full operator manual & task→agent/app maps: - ~/.claude/skills/pilotctl/SKILL.md + Full operator manual & task→agent/app maps: the pilotctl skill + (`pilotctl skills` lists where it is installed, e.g. + ~/.claude/skills/pilotctl/SKILL.md). ============================================ PILOT_GET_STARTED echo "" diff --git a/tests/proxy-transport-install.sh b/tests/proxy-transport-install.sh index 9dbf03e..49ed472 100644 --- a/tests/proxy-transport-install.sh +++ b/tests/proxy-transport-install.sh @@ -5,9 +5,16 @@ # - --transport udp|compat is saved in config.json, auto never is, and # --transport auto removes a saved transport; # - proxy_cmd is saved in such a sandbox when HTTPS_PROXY carries -# credentials (the daemon re-reads rotating proxy credentials with it); +# credentials (the daemon re-reads rotating proxy credentials with it), +# also for a daemon that predates -proxy-cmd (it ignores the key until +# upgraded); +# - a download that fails because the proxy credentials rotated mid-install +# is retried once with the credentials a fresh shell sees; +# - PILOT_PROXY carries the downloads when no *_PROXY variable is set; +# - no raw-IP registry/beacon is written for compat, or auto behind a proxy +# (an older pilotctl in compat mode gets the TLS registry by name); # - a saved transport=auto is rewritten to udp for a daemon that predates it; -# - proxy credentials never reach the installer output. +# - proxy credentials never reach the installer output or ~/.pilot. # # Like tests/managed-install.sh it installs a fixture release through a fake # curl, so it needs no network. It never uses sudo (a fake sudo fails), and the @@ -95,7 +102,8 @@ PY ;; esac SH -cp "$FIXTURE/new/archive/pilotctl" "$FIXTURE/old/archive/pilotctl" +# The "old" pilotctl predates `daemon start --transport` (v1.13.x). +sed 's/--transport /--registry /' "$FIXTURE/new/archive/pilotctl" > "$FIXTURE/old/archive/pilotctl" chmod 755 "$FIXTURE"/new/archive/* "$FIXTURE"/old/archive/* make_release() { # make_release @@ -120,7 +128,7 @@ cat > "$FAKEBIN/uname" <<'SH' #!/bin/sh case "${1:-}" in -m) echo x86_64 ;; - *) echo Linux ;; + *) echo "${PILOT_TEST_UNAME:-Linux}" ;; esac SH cat > "$FAKEBIN/curl" <<'SH' @@ -137,6 +145,28 @@ while [ "$#" -gt 0 ]; do esac done [ -n "$output" ] || exit 89 +# PILOT_TEST_EXPECT_PROXY: the proxy this download must go through. +if [ -n "${PILOT_TEST_EXPECT_PROXY:-}" ] && [ "${https_proxy:-}" != "$PILOT_TEST_EXPECT_PROXY" ]; then + echo "download not through the expected proxy" >&2; exit 5 +fi +# PILOT_TEST_CREDS: file holding the proxy password that is valid right now. +# A request with any other one gets curl's 407 failure (exit 56). After the +# manifest is served the password rotates once. +if [ -n "${PILOT_TEST_CREDS:-}" ]; then + _pw=${https_proxy#*://*:}; _pw=${_pw%%@*} + if [ "$_pw" != "$(cat "$PILOT_TEST_CREDS")" ]; then + echo 407 >> "$PILOT_TEST_CREDS.rejected" + echo "curl: (56) CONNECT tunnel failed, response 407" >&2; exit 56 + fi + case "$url" in + */.well-known/latest.json) + if [ ! -e "$PILOT_TEST_CREDS.rotated" ]; then + : > "$PILOT_TEST_CREDS.rotated" + printf 'rotated-%s\n' "$(cat "$PILOT_TEST_CREDS")" > "$PILOT_TEST_CREDS.new" + mv "$PILOT_TEST_CREDS.new" "$PILOT_TEST_CREDS" + fi ;; + esac +fi case "$url" in */.well-known/latest.json) src="$PILOT_TEST_RELEASE/stable-manifest.json" ;; */pilot-linux-amd64.tar.gz) src="$PILOT_TEST_RELEASE/pilot-linux-amd64.tar.gz" ;; @@ -176,7 +206,7 @@ cfg_get() { # cfg_get — prints the value, "" when absent SECRET="s3cretPW" PROXY="http://muse:${SECRET}@egress.test:3128" # shellcheck disable=SC2016 # the literal command the installer saves -SANDBOX_CMD='bash -c '\''printf %s "${https_proxy:-$HTTPS_PROXY}"'\''' +SANDBOX_CMD='bash -c '\''case $https_proxy in *@*) printf %s "$https_proxy";; *) printf %s "${HTTPS_PROXY:-$https_proxy}";; esac'\''' unset HTTPS_PROXY https_proxy ALL_PROXY all_proxy PILOT_TRANSPORT PILOT_PROXY_CMD PILOT_ALLOW_ROOT 2>/dev/null || true # 1. --transport is validated. @@ -228,12 +258,73 @@ if [ ! -d /run/systemd/system ]; then run_install "$WORK/h-nocreds" "$FIXTURE/new" "$LOG") || fail "no-creds install" [ -z "$(cfg_get "$WORK/h-nocreds" proxy_cmd)" ] || fail "proxy_cmd saved for a proxy without credentials" - # A daemon without -proxy-cmd gets a note instead. + # A daemon without -proxy-cmd (v1.13.x): proxy_cmd is still saved (the + # daemon ignores it until upgraded), with a note, and the proxy it cannot + # use is called out with the recipe that works. LOG="$WORK/oldcmd.log" (export HTTPS_PROXY="$PROXY" run_install "$WORK/h-oldcmd" "$FIXTURE/old" "$LOG") || fail "old daemon sandbox install" - [ -z "$(cfg_get "$WORK/h-oldcmd" proxy_cmd)" ] || fail "proxy_cmd saved for a daemon without -proxy-cmd" + [ "$(cfg_get "$WORK/h-oldcmd" proxy_cmd)" = "$SANDBOX_CMD" ] || fail "proxy_cmd not saved for a daemon without -proxy-cmd" grep -F "predates -proxy-cmd" "$LOG" >/dev/null || fail "no -proxy-cmd note for an old daemon" + grep -F "cannot use one" "$LOG" >/dev/null || fail "no warning about a proxy the old daemon cannot use" + grep -F "https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" "$LOG" >/dev/null \ + || fail "the old-daemon proxy warning does not point at the sandbox recipe" + # auto is not supported, so the daemon runs udp: the stock endpoints stay. + [ "$(cfg_get "$WORK/h-oldcmd" registry)" = "34.71.57.205:9000" ] || fail "udp install lost the raw registry" + if grep -F "$SECRET" "$LOG" >/dev/null || grep -rF "$SECRET" "$WORK/h-oldcmd/.pilot" >/dev/null; then + fail "proxy credentials leaked (old daemon)" + fi + + # Credentials that rotate while the installer runs: the download that + # gets the 407 is retried once with what a fresh bash sees (BASH_ENV + # stands in for the sandbox's mechanism), and nothing is printed. + if command -v bash >/dev/null 2>&1; then + CREDS="$WORK/creds" + printf 'gen1pw\n' > "$CREDS" + cat > "$WORK/bash_env.sh" </dev/null || fail "rotated install did not verify the archive" + if grep -F -e gen1pw -e rotated-gen1pw "$LOG" >/dev/null \ + || grep -rF -e gen1pw "$WORK/h-rotate/.pilot" >/dev/null; then + fail "proxy credentials leaked (rotation)" + fi + else + echo "skip: rotation case needs bash" + fi + + # 4b. Compat / auto behind a proxy: no raw-IP registry or beacon in + # config.json (the daemon picks the TLS registry itself), and no + # credentials anywhere under ~/.pilot. + LOG="$WORK/auto-proxy.log" + (export HTTPS_PROXY="$PROXY" https_proxy="$PROXY" + run_install "$WORK/h-auto-proxy" "$FIXTURE/new" "$LOG") || fail "auto install behind a proxy" + if grep -F "34.71.57.205" "$WORK/h-auto-proxy/.pilot/config.json" >/dev/null; then + fail "auto install behind a proxy wrote a raw-IP endpoint" + fi + [ -z "$(cfg_get "$WORK/h-auto-proxy" transport)" ] || fail "auto install saved a transport" + if grep -F "$SECRET" "$LOG" >/dev/null || grep -rF "$SECRET" "$WORK/h-auto-proxy/.pilot" >/dev/null; then + fail "proxy credentials leaked (auto behind a proxy)" + fi + + LOG="$WORK/compat-proxy.log" + (export HTTPS_PROXY="$PROXY" https_proxy="$PROXY" + run_install "$WORK/h-compat-proxy" "$FIXTURE/new" "$LOG" --transport compat) || fail "compat install behind a proxy" + if grep -F "34.71.57.205" "$WORK/h-compat-proxy/.pilot/config.json" >/dev/null; then + fail "compat install wrote a raw-IP endpoint" + fi + [ "$(cfg_get "$WORK/h-compat-proxy" transport)" = compat ] || fail "compat not saved (proxy)" + + # 4c. PILOT_PROXY carries the downloads when no *_PROXY is set. + LOG="$WORK/pilot-proxy.log" + (export PILOT_PROXY=http://relay.test:3128 PILOT_TEST_EXPECT_PROXY=http://relay.test:3128 + run_install "$WORK/h-pilot-proxy" "$FIXTURE/new" "$LOG") || fail "downloads did not use PILOT_PROXY" # 5. Root: allowed in a Linux container/VM without systemd. LOG="$WORK/root.log" @@ -267,5 +358,31 @@ run_install "$WORK/h-back" "$FIXTURE/new" "$WORK/back.log" --transport udp || fa [ "$(cfg_get "$WORK/h-back" transport)" = udp ] || fail "--transport udp not saved" [ "$(cfg_get "$WORK/h-back" registry)" = "34.71.57.205:9000" ] || fail "raw registry not restored" +# 8. Without a proxy the stock endpoints are written as before (auto or udp). +[ "$(cfg_get "$WORK/h-default" registry)" = "34.71.57.205:9000" ] || fail "default install lost the raw registry" +[ "$(cfg_get "$WORK/h-default" beacon)" = "34.71.57.205:9001" ] || fail "default install lost the raw beacon" + +# 9. compat with a pilotctl that predates --transport (it passes config.json's +# registry to the daemon verbatim): the TLS registry is written by name. +run_install "$WORK/h-oldctl" "$FIXTURE/old" "$WORK/oldctl.log" --transport compat || fail "old pilotctl compat install" +[ "$(cfg_get "$WORK/h-oldctl" registry)" = "registry.pilotprotocol.network:443" ] \ + || fail "old pilotctl compat install: registry '$(cfg_get "$WORK/h-oldctl" registry)'" +if grep -F "34.71.57.205" "$WORK/h-oldctl/.pilot/config.json" >/dev/null; then + fail "old pilotctl compat install wrote a raw-IP endpoint" +fi + +# 10. Root on macOS is refused like on any host with a service manager. +LOG="$WORK/root-mac.log" +if (export PILOT_TEST_UID=0 PILOT_TEST_UNAME=Darwin; run_install "$WORK/h-root-mac" "$FIXTURE/new" "$LOG"); then + fail "root install on macOS was accepted" +fi +grep -F "refusing to install as root" "$LOG" >/dev/null || fail "no root refusal on macOS" + +# 11. --help prints the whole usage header and nothing past it. +sh "$ROOT/install.sh" --help > "$WORK/help.log" 2>&1 || fail "--help failed" +grep -F -- "--transport " "$WORK/help.log" >/dev/null || fail "--help lacks --transport" +grep -F "with a message, never fatal." "$WORK/help.log" >/dev/null || fail "--help cut the header short" +if grep -F "WHAT THIS SCRIPT DOES" "$WORK/help.log" >/dev/null; then fail "--help printed past the usage header"; fi + rm -rf "$WORK" echo "proxy/transport installer contract: ok" From ae447bc0a45b5b54e864e96eff91effdef93b40c Mon Sep 17 00:00:00 2001 From: Teodor Calin Date: Thu, 24 Sep 2026 09:13:47 +0300 Subject: [PATCH 3/5] installer: hand pilotctl skills check the current proxy credentials The first skills pass runs after the downloads; in a sandbox whose proxy credentials rotated in between, pilotctl inherited stale ones (seen as a 407 on raw.githubusercontent.com in the rotating-proxy rig). Refresh from PROXY_REFRESH_CMD first; a no-op everywhere else. Co-Authored-By: Claude Opus 5.5 (1M context) --- install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.sh b/install.sh index adfac3f..81fac81 100755 --- a/install.sh +++ b/install.sh @@ -2229,6 +2229,9 @@ echo " The daemon scans every 15 minutes and injects the Pilot Protocol" echo " skill into installed agent tools. Triggering a first pass right now" echo " so your agents know about Pilot before the daemon is even started:" echo "" +# pilotctl fetches the skills through this process's proxy settings: hand it +# the current credentials if they rotated since the downloads. +proxy_refresh || true if "${BIN_DIR}/pilotctl" skills check 2>&1 | sed 's/^/ /'; then : else From 67e83fc93cf2fef2b9532eea3ae0bdc54455a30d Mon Sep 17 00:00:00 2001 From: Teodor Calin Date: Thu, 24 Sep 2026 10:17:28 +0300 Subject: [PATCH 4/5] installer: refuse root under sudo, no daemon-start advice where it bypasses the proxy, honest transport, pinned tags install - Root reached through sudo/doas/pkexec for a regular user (SUDO_UID/SUDO_USER, DOAS_USER, PKEXEC_UID) is refused again on every host, with the reason and the command to run instead. `curl | sudo sh` on WSL/OpenRC/dev containers installed into /root/.pilot (0700) and left the user with "command not found"; `sudo -E` left a root-owned ~/.pilot. A root agent (Meta Muse) is unaffected; PILOT_ALLOW_ROOT=1 still overrides. - With a daemon that cannot use the proxy (v1.13.9) on a host whose proxy is the way out (credential-bearing proxy without systemd, or PILOT_PROXY_CMD), no output tells the agent to run `pilotctl daemon start` (warning, no-systemd hint, GET STARTED step 0, re-run summary): each points at the pilot-sandbox recipe instead. Elsewhere the command stays, with the condition next to it. - auto is no longer announced before the download. The summary (install and update) states the transport the installed daemon runs; a release without auto says udp and, where nothing chose udp, how to get compat on a UDP-blocked host. compat saved for a daemon without -transport is reported as udp. - --version / --channel beta: the manifest hash is compared only for the tag the manifest describes (its platform url names it, or latest_stable), so a pinned or beta tag installs against checksums.txt instead of aborting with "integrity anchors disagree". The tag it describes still needs both. - --help prints the usage header up to "WHAT THIS SCRIPT DOES" instead of a fixed line range. Co-Authored-By: Claude Opus 5.5 (1M context) --- install.sh | 253 +++++++++++++++++++++++++------ tests/proxy-transport-install.sh | 191 +++++++++++++++++++++-- 2 files changed, 389 insertions(+), 55 deletions(-) diff --git a/install.sh b/install.sh index 81fac81..4e9b10b 100755 --- a/install.sh +++ b/install.sh @@ -9,11 +9,13 @@ set -e # Install: curl -fsSL https://pilotprotocol.network/install.sh | sh # Pin a version: curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --version v1.13.6 # Beta channel: curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --channel beta -# UDP blocked / curl -fsSL https://pilotprotocol.network/install.sh | sh -# HTTPS proxy: (nothing extra: transport "auto" picks TLS/WSS over TCP 443 -# through $HTTPS_PROXY when UDP does not work; add -# `-s -- --transport compat` to skip the UDP probe; proxy -# credentials that rotate: see PILOT_PROXY_CMD below) +# UDP blocked / curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --transport compat +# HTTPS proxy: (with a release that has transport "auto" nothing extra is +# needed: auto picks TLS/WSS over TCP 443, through +# $HTTPS_PROXY when set, where UDP does not work. Releases +# before auto stay on UDP unless given --transport compat, and +# use no proxy: the installer prints what to do instead. +# Proxy credentials that rotate: see PILOT_PROXY_CMD below) # Managed node: export PILOT_ENROLLMENT_TOKEN # enter it without putting it in shell history # sh install.sh --managed-url https://management.pilotprotocol.network # Uninstall: curl -fsSL https://pilotprotocol.network/install.sh | sh -s uninstall @@ -59,8 +61,9 @@ set -e # installer saves one that reads a fresh shell's # $https_proxy when none is set. The installer # also uses it to retry a failed download. -# PILOT_ALLOW_ROOT=1 Install as root on a host with systemd/launchd -# (not needed in containers/VMs without systemd). +# PILOT_ALLOW_ROOT=1 Install as root on a host with systemd/launchd, +# or under sudo/doas (not needed where the agent +# itself is root in a container/VM without systemd). # PILOT_MANAGEMENT_URL=https://management.example # Same as --managed-url. Requires the one-time # PILOT_ENROLLMENT_TOKEN on first adoption. @@ -79,7 +82,8 @@ set -e # 3. Downloads the release tarball + checksums.txt from that release # 4. *** Verifies SHA-256 of the tarball against checksums.txt AND the signed # manifest (aborts on mismatch OR if it cannot verify — never extracts -# an unverified archive) *** +# an unverified archive; the manifest hashes the release it describes, +# so a --version pin or beta tag is checked against checksums.txt) *** # 5. Extracts binaries to ~/.pilot/bin (per-user, NOT system-wide) # 6. Adds ~/.pilot/bin to PATH in your shell profiles (~/.profile, ~/.bashrc, # ~/.zshenv, ~/.zshrc, ~/.bash_profile when it already exists) @@ -107,9 +111,9 @@ set -e # Network 9 directory and for receiving identifier-based deliveries. # # WHAT THIS SCRIPT DOES NOT DO: -# - Run as root on a host with systemd or launchd (refuses; see the root -# check below). A Linux container/VM without systemd, where the agent is -# root, installs into root's own $HOME/.pilot. +# - Run as root on a host with systemd or launchd, or under sudo (refuses; +# see the root check below). A Linux container/VM without systemd, where +# the agent itself is root, installs into root's own $HOME/.pilot. # - Send any personal data anywhere (the install script only fetches the # release tarball from GitHub; the daemon registers its public key + a # synthetic or user-supplied email with the rendezvous server, nothing else) @@ -218,7 +222,9 @@ while [ $# -gt 0 ]; do --no-start) PILOT_MANAGED_NO_START=1; shift ;; -h|--help) - sed -n '4,74p' "$0" 2>/dev/null || echo "See https://pilotprotocol.network/install.sh" + # The usage header: from line 4 up to "WHAT THIS SCRIPT DOES". + awk 'NR >= 4 { if (/^# WHAT THIS SCRIPT DOES/) exit; print }' "$0" 2>/dev/null \ + || echo "See https://pilotprotocol.network/install.sh" exit 0 ;; --) shift @@ -330,13 +336,37 @@ set -- $PILOT_POSITIONAL # home, not /root. A Linux container or VM without systemd (CI runners, # hosted agent sandboxes such as Meta Muse, where the agent IS root) has no # other user to install for and no system service to protect, so root is -# allowed there. +# allowed there — but only when root is who runs it. Root reached through +# sudo/doas/pkexec is a regular user's install (`curl ... | sudo sh` on WSL, +# OpenRC/runit hosts, dev containers): it would land in /root/.pilot (0700, +# with the /usr/local/bin links pointing into it) or, with sudo -E, in a +# root-owned ~/.pilot, and that user could run neither. Refused everywhere. SANDBOX_HOST=false if [ "$(uname -s)" = "Linux" ] && [ ! -d /run/systemd/system ]; then SANDBOX_HOST=true fi +ELEVATED_FROM="" +if [ -n "${SUDO_UID:-}" ]; then + if [ "$SUDO_UID" != "0" ]; then + ELEVATED_FROM="sudo for ${SUDO_USER:-uid $SUDO_UID}" + fi +elif [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then + ELEVATED_FROM="sudo for $SUDO_USER" +elif [ -n "${DOAS_USER:-}" ] && [ "$DOAS_USER" != "root" ]; then + ELEVATED_FROM="doas for $DOAS_USER" +elif [ -n "${PKEXEC_UID:-}" ] && [ "$PKEXEC_UID" != "0" ]; then + ELEVATED_FROM="pkexec for uid $PKEXEC_UID" +fi if [ "${1:-}" != "uninstall" ] && [ "$(id -u)" = "0" ] && [ -z "${PILOT_ALLOW_ROOT:-}" ]; then - if [ "$SANDBOX_HOST" = true ]; then + if [ -n "$ELEVATED_FROM" ]; then + echo "Error: refusing to install as root: this runs under ${ELEVATED_FROM}." + echo " The node would be installed for root, into ${HOME}/.pilot, and that" + echo " user could not use it. Run the installer as that user, without sudo or doas:" + echo " curl -fsSL https://pilotprotocol.network/install.sh | sh" + echo " It uses sudo itself only where needed (never with a password prompt)." + echo " Set PILOT_ALLOW_ROOT=1 to install for root anyway (not recommended)." + exit 1 + elif [ "$SANDBOX_HOST" = true ]; then echo "Note: installing as root (no systemd: container/VM sandbox) into ${HOME}/.pilot" else echo "Error: refusing to install as root." @@ -542,16 +572,36 @@ manifest_field() { # independent integrity anchor (served from pilotprotocol.network) alongside the # release's checksums.txt (served from GitHub). manifest_platform_sha256() { - _mp_plat="$1"; _mp_file="$2" + manifest_platform_field "$1" sha256 "$2" +} + +# manifest_platform_field "-" "" "" extracts one string +# field (sha256, url) of that platform's object; empty when absent. +manifest_platform_field() { + _mp_plat="$1"; _mp_key="$2"; _mp_file="$3" # The authority is free to emit compact JSON. A line-range parser sees all # platform objects on that one line and a greedy replacement can therefore # return the final platform's hash. Collapse whitespace deliberately, then # constrain the match to this platform's first closing brace. tr -d '\r\n' < "$_mp_file" \ - | sed -n -E "s/.*\"${_mp_plat}\"[[:space:]]*:[[:space:]]*\\{[^}]*\"sha256\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"[^}]*\\}.*/\\1/p" \ + | sed -n -E "s/.*\"${_mp_plat}\"[[:space:]]*:[[:space:]]*\\{[^}]*\"${_mp_key}\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"[^}]*\\}.*/\\1/p" \ | head -1 } +# manifest_describes_tag "" "-" "" — whether the +# manifest's per-platform entry is the archive of . The manifest carries +# hashes for one release only: the platform url names it +# (…/releases/download//…), and a manifest without urls (the managed +# runtime's) describes its latest_stable. Any other tag — a --version pin, the +# beta channel — has no second anchor, so its hash must not be compared. +manifest_describes_tag() { + [ "$1" = "$(manifest_field "latest_stable" "$3")" ] && return 0 + case "$(manifest_platform_field "$2" url "$3")" in + */download/"$1"/*) return 0 ;; + esac + return 1 +} + # version_compare a b emits -1 / 0 / 1 for ab. # Honors semver: a prerelease tag ("X.Y.Z-rcN") is LOWER than the same base # without it ("X.Y.Z"). Plain `sort -V` gets this backwards on hyphenated @@ -704,16 +754,13 @@ echo " Pilot Protocol" echo " The network stack for AI agents." echo "" echo " Platform: ${OS}/${ARCH}" +# auto is not announced here: whether the release being installed has it is +# known only after the download (releases before it run udp). The summary at +# the end states the transport the installed daemon will actually use. case "$EFFECTIVE_TRANSPORT" in compat) echo " Transport: compat (TLS + WSS over TCP 443 only)" ;; - auto) - echo " Transport: auto (UDP when it works, else TLS + WSS over TCP 443)" - if [ -z "$PILOT_PROXY_URL" ]; then - echo " Registry: ${REGISTRY}" - echo " Beacon: ${BEACON}" - fi ;; - *) + udp) echo " Registry: ${REGISTRY}" echo " Beacon: ${BEACON}" ;; esac @@ -953,8 +1000,11 @@ if [ -n "$TAG" ]; then if pcurl -fsSL "$CHECKSUMS_URL" -o "$TMPDIR/checksums.txt" 2>/dev/null; then EXPECTED_CKS=$(grep " ${ARCHIVE}\$" "$TMPDIR/checksums.txt" | awk '{print $1}') fi + # The manifest hashes only the release it describes (see + # manifest_describes_tag): for any other tag it is no anchor at all. EXPECTED_MAN="" - if [ "$HAVE_MANIFEST" = "1" ]; then + if [ "$HAVE_MANIFEST" = "1" ] \ + && manifest_describes_tag "$TAG" "${OS}-${ARCH}" "$MANIFEST_FILE"; then EXPECTED_MAN=$(manifest_platform_sha256 "${OS}-${ARCH}" "$MANIFEST_FILE") fi @@ -1454,8 +1504,12 @@ if [ -n "$PROXY_CMD_TO_SAVE" ]; then echo "Proxy credentials: re-read by the daemon via proxy_cmd (${PILOT_DIR}/config.json stores the command, not the credentials)" else echo "Proxy credentials: proxy_cmd saved in ${PILOT_DIR}/config.json (the command, not the credentials)." - echo " This pilot-daemon (${TAG:-source}) predates -proxy-cmd and ignores it until upgraded;" - echo " until then, if the proxy rotates its credentials, restart the daemon from a fresh shell." + if [ "$DAEMON_HAS_PROXY" = true ]; then + echo " This pilot-daemon (${TAG:-source}) predates -proxy-cmd and ignores it until upgraded;" + echo " until then, if the proxy rotates its credentials, restart the daemon from a fresh shell." + else + echo " This pilot-daemon (${TAG:-source}) predates -proxy-cmd and ignores it until upgraded." + fi fi fi PROXY_CMD_SAVED=false @@ -1498,16 +1552,68 @@ fi # A proxy in the environment that this daemon cannot use: say so, and where # the proxy is the only way out, point at the recipe that works with it. +# Such a daemon dials the registry and beacon directly, around the proxy. +# PROXY_ONLY: the proxy is known to be the way out — $PILOT_PROXY_CMD is set +# (rotating credentials), or a credential-bearing proxy in a Linux +# container/VM without systemd (a hosted agent sandbox such as Meta Muse). +# There every "start the daemon" instruction below is replaced by the +# recipe; elsewhere it is kept, with the condition spelled out. +SANDBOX_RECIPE_URL="https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" PROXY_UNSUPPORTED=false +PROXY_ONLY=false if [ -n "$PILOT_PROXY_URL" ] && [ "$DAEMON_HAS_PROXY" != true ]; then PROXY_UNSUPPORTED=true + if [ -n "${PILOT_PROXY_CMD:-}" ]; then + PROXY_ONLY=true + elif [ "$SANDBOX_HOST" = true ]; then + case "${https_proxy:-}${HTTPS_PROXY:-}${PILOT_PROXY_URL}" in + *@*) PROXY_ONLY=true ;; + esac + fi echo "" - echo " WARNING: a proxy is set ($(redact_proxy "$PILOT_PROXY_URL")), but this pilot-daemon" - echo " (${TAG:-source}) cannot use one. If the proxy is this host's only way" - echo " out (UDP blocked, e.g. an agent sandbox), \`pilotctl daemon start\` will" - echo " not come online with this release. Use the pilot-sandbox recipe:" - echo " https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" -fi + if [ "$PROXY_ONLY" = true ]; then + echo " WARNING: this host goes out through a proxy ($(redact_proxy "$PILOT_PROXY_URL")), and this" + echo " pilot-daemon (${TAG:-source}) cannot use one: \`pilotctl daemon start\` would dial" + echo " the registry directly, around the proxy, and never come online." + echo " Do not run it here. Bring the node online with the pilot-sandbox" + echo " recipe (step 3): ${SANDBOX_RECIPE_URL}" + else + echo " WARNING: a proxy is set ($(redact_proxy "$PILOT_PROXY_URL")), but this pilot-daemon" + echo " (${TAG:-source}) cannot use one: it dials the registry directly. If the" + echo " proxy is this host's only way out (UDP blocked, e.g. an agent sandbox)," + echo " \`pilotctl daemon start\` will not come online with this release: use the" + echo " pilot-sandbox recipe (step 3) instead: ${SANDBOX_RECIPE_URL}" + fi +fi + +# UDP_ONLY_HINT: the daemon runs udp only because this release predates auto +# (nothing chose udp), so on a UDP-blocked host it will not fall back to TCP +# 443 by itself. Said in the summary, with the re-run that selects compat +# (which also points an older pilotctl at the TLS registry). Not where the +# proxy warning above already applies: compat would not use the proxy either. +UDP_ONLY_HINT=false +if [ "$EFFECTIVE_TRANSPORT" = "udp" ] && [ "$DAEMON_HAS_AUTO" != true ] \ + && [ "$DAEMON_HAS_TRANSPORT" = true ] && [ "$PROXY_UNSUPPORTED" != true ] \ + && [ "$TRANSPORT" != "udp" ] && [ "$CONFIG_TRANSPORT" != "udp" ]; then + UDP_ONLY_HINT=true +fi + +# start_hint PREFIX COMMAND [NAME] — print how to start the daemon: COMMAND, +# except where this daemon cannot use the proxy (the WARNING above). On a +# proxy-only host (PROXY_ONLY) COMMAND is not printed as something to run: +# only that NAME (default: COMMAND) must not be run there, and the recipe. +start_hint() { + if [ "$PROXY_ONLY" = true ]; then + echo "${1}Do not run \`${3:-$2}\` on this host (see the WARNING above)." + echo "${1}Use the pilot-sandbox recipe (step 3): ${SANDBOX_RECIPE_URL}" + elif [ "$PROXY_UNSUPPORTED" = true ]; then + echo "${1}${2}" + echo "${1}(if the proxy is this host's only way out, use the pilot-sandbox recipe" + echo "${1} instead, see the WARNING above)" + else + echo "${1}${2}" + fi +} # Network flags for the service units. The transport itself comes from # config.json, which the daemon reads, so `pilotctl config --set transport=` @@ -1732,13 +1838,24 @@ USVC elif [ "$PILOT_MANAGED_MODE" != "1" ]; then case " $RESTART_SYSTEMD " in *" pilot-daemon "*) ;; - *) echo " Start daemon: sudo systemctl enable --now pilot-daemon" ;; + *) + if [ "$PROXY_UNSUPPORTED" = true ]; then + echo " Start daemon:" + start_hint " " "sudo systemctl enable --now pilot-daemon" + else + echo " Start daemon: sudo systemctl enable --now pilot-daemon" + fi ;; esac fi else echo " Skipped systemd setup (run as root or with passwordless sudo to enable)" if [ "$PILOT_MANAGED_MODE" != "1" ]; then - echo " Start the daemon without a service manager: pilotctl daemon start" + if [ "$PROXY_UNSUPPORTED" = true ]; then + echo " Start the daemon without a service manager:" + start_hint " " "pilotctl daemon start" + else + echo " Start the daemon without a service manager: pilotctl daemon start" + fi fi fi elif [ "$OS" = "linux" ]; then @@ -1746,11 +1863,13 @@ elif [ "$OS" = "linux" ]; then # hosted agent sandbox). There is no service to install — tell the agent # the portable start path instead of silently leaving it with no daemon. if [ "$PILOT_MANAGED_MODE" != "1" ]; then - echo "No systemd detected (container / WSL / CI / sandbox) — start the daemon manually:" - echo " pilotctl daemon start" - if [ "$PROXY_UNSUPPORTED" = true ]; then - echo " (proxy-only host: see the WARNING above)" - elif [ "$EFFECTIVE_TRANSPORT" != "udp" ]; then + if [ "$PROXY_ONLY" = true ]; then + echo "No systemd detected (container / WSL / CI / sandbox):" + else + echo "No systemd detected (container / WSL / CI / sandbox) — start the daemon manually:" + fi + start_hint " " "pilotctl daemon start" + if [ "$PROXY_UNSUPPORTED" != true ] && [ "$EFFECTIVE_TRANSPORT" != "udp" ]; then echo " (transport=${EFFECTIVE_TRANSPORT}; start it from a shell that has HTTPS_PROXY" echo " set if this host reaches the internet only through a proxy)" fi @@ -2043,6 +2162,36 @@ if [ "$PILOT_MANAGED_MODE" = "1" ]; then exit 0 fi +# transport_line — the transport the installed daemon will run, stated once +# the binaries are known (the banner at the top does not guess), plus what to +# do on a UDP-blocked host when this release will not fall back by itself. +transport_line() { + case "$EFFECTIVE_TRANSPORT" in + compat) + if [ "$DAEMON_HAS_TRANSPORT" != true ]; then + echo " Transport: udp (compat is saved, but this pilot-daemon, ${TAG:-source}," + echo " predates it; see the WARNING above)" + return 0 + fi + _tl_registry="$COMPAT_REGISTRY" + if [ "$REGISTRY" != "$DEFAULT_REGISTRY" ]; then _tl_registry="$REGISTRY"; fi + echo " Transport: compat (registry ${_tl_registry} over TLS, beacon over WSS)" ;; + auto) + echo " Transport: auto (UDP when it works, else compat over TCP 443)" ;; + *) + if [ "$DAEMON_HAS_AUTO" != true ]; then + echo " Transport: udp (this pilot-daemon, ${TAG:-source}, predates auto and never" + echo " falls back to TCP 443 by itself)" + else + echo " Transport: udp" + fi ;; + esac + if [ "$UDP_ONLY_HINT" = true ]; then + echo " UDP blocked on this host? Use TLS + WSS over TCP 443 instead:" + echo " curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --transport compat" + fi +} + # --- Upgrade: short summary, skip the first-run onboarding text --- # # Everything above this point (binary swap, unit/plist regeneration, service @@ -2056,11 +2205,12 @@ if [ "$UPDATING" = true ]; then echo " pilotctl ${BIN_DIR}/pilotctl" [ -f "$BIN_DIR/pilot-gateway" ] && echo " pilot-gateway ${BIN_DIR}/pilot-gateway" [ -f "$BIN_DIR/pilot-updater" ] && echo " pilot-updater ${BIN_DIR}/pilot-updater" + transport_line echo "" if [ -z "$RESTART_SYSTEMD" ] && [ -z "$RESTART_LAUNCHD" ]; then echo "No managed service was running. If you run the daemon yourself," echo "restart it to pick up the new version:" - echo " pilotctl daemon stop && pilotctl daemon start" + start_hint " " "pilotctl daemon stop && pilotctl daemon start" "pilotctl daemon start" echo "" fi exit 0 @@ -2074,13 +2224,10 @@ echo " pilotctl ${BIN_DIR}/pilotctl" [ -f "$BIN_DIR/pilot-updater" ] && echo " pilot-updater ${BIN_DIR}/pilot-updater (auto-updates in background)" echo "" echo "Config: ${PILOT_DIR}/config.json" +transport_line case "$EFFECTIVE_TRANSPORT" in - compat) - _summary_registry="$COMPAT_REGISTRY" - if [ "$REGISTRY" != "$DEFAULT_REGISTRY" ]; then _summary_registry="$REGISTRY"; fi - echo " Transport: compat (registry ${_summary_registry} over TLS, beacon over WSS)" ;; + compat) ;; auto) - echo " Transport: auto (UDP when it works, else compat over TCP 443)" if [ "$STOCK_ENDPOINTS" = true ]; then echo " Registry: ${REGISTRY}" echo " Beacon: ${BEACON}" @@ -2112,8 +2259,22 @@ echo "" echo " 0) Put pilotctl on your PATH and bring the node online." echo " ------------------------------------------------------------------" echo " export PATH=\"${BIN_DIR}:\$PATH\" # only needed in THIS shell, before you open a new one" +# The start command itself only where this daemon can come online with it +# (see PROXY_UNSUPPORTED / PROXY_ONLY): a proxy-only host gets the recipe. +if [ "$PROXY_ONLY" = true ]; then + echo " # Do NOT run \`pilotctl daemon start\` on this host: this pilot-daemon (${TAG:-source})" + echo " # cannot use the proxy and would dial the registry directly. Bring the node" + echo " # online with the pilot-sandbox recipe (step 3), then check it here:" + echo " # ${SANDBOX_RECIPE_URL}" +elif [ "$PROXY_UNSUPPORTED" = true ]; then + echo " # This pilot-daemon (${TAG:-source}) cannot use the proxy (see the WARNING above). If the" + echo " # proxy is this host's only way out, skip the next line and use the pilot-sandbox" + echo " # recipe (step 3) instead: ${SANDBOX_RECIPE_URL}" + echo " pilotctl daemon start --hostname my-agent # blocks until registered; email already saved" +else + echo " pilotctl daemon start --hostname my-agent # blocks until registered; email already saved" +fi cat <<'PILOT_GET_STARTED' - pilotctl daemon start --hostname my-agent # blocks until registered; email already saved pilotctl daemon status # confirm it's running pilotctl info # node ID, address, peer count, uptime diff --git a/tests/proxy-transport-install.sh b/tests/proxy-transport-install.sh index 49ed472..3285370 100644 --- a/tests/proxy-transport-install.sh +++ b/tests/proxy-transport-install.sh @@ -14,7 +14,15 @@ # - no raw-IP registry/beacon is written for compat, or auto behind a proxy # (an older pilotctl in compat mode gets the TLS registry by name); # - a saved transport=auto is rewritten to udp for a daemon that predates it; -# - proxy credentials never reach the installer output or ~/.pilot. +# - proxy credentials never reach the installer output or ~/.pilot; +# - root reached through sudo/doas is refused, also without systemd; +# - a daemon that cannot use the proxy is never followed by an instruction +# to run `pilotctl daemon start` where the proxy is the way out, and a +# release without auto is not announced as auto (with the compat re-run +# for UDP-blocked hosts); +# - --version / --channel beta install a tag the manifest does not describe +# (checksums.txt is its anchor), while the manifest hash still has to +# agree for the tag it describes. # # Like tests/managed-install.sh it installs a fixture release through a fake # curl, so it needs no network. It never uses sudo (a fake sudo fails), and the @@ -104,23 +112,54 @@ esac SH # The "old" pilotctl predates `daemon start --transport` (v1.13.x). sed 's/--transport /--registry /' "$FIXTURE/new/archive/pilotctl" > "$FIXTURE/old/archive/pilotctl" -chmod 755 "$FIXTURE"/new/archive/* "$FIXTURE"/old/archive/* +# An "ancient" daemon predates -transport altogether. +mkdir -p "$FIXTURE/ancient/archive" +cp "$FIXTURE/old/archive/pilotctl" "$FIXTURE/ancient/archive/pilotctl" +cat > "$FIXTURE/ancient/archive/daemon" <<'SH' +#!/bin/sh +cat <<'HELP' +Usage of pilot-daemon: + -registry string + registry server address +HELP +exit 0 +SH +chmod 755 "$FIXTURE"/new/archive/* "$FIXTURE"/old/archive/* "$FIXTURE"/ancient/archive/* -make_release() { # make_release +make_release() { # make_release [ [ []]] COPYFILE_DISABLE=1 tar -czf "$1/pilot-linux-amd64.tar.gz" -C "$1/archive" . _sha=$(shasum -a 256 "$1/pilot-linux-amd64.tar.gz" | awk '{print $1}') printf '%s %s\n' "$_sha" pilot-linux-amd64.tar.gz > "$1/checksums.txt" + _url="" + if [ -n "${5:-}" ]; then + _url="\"url\": \"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/pilot-protocol/pilotprotocol/releases/download/$5/pilot-linux-amd64.tar.gz\", " + fi cat > "$1/stable-manifest.json" < "$LOG" 2>&1 + "${PILOT_TEST_SH:-sh}" "$ROOT/install.sh" "$@" > "$LOG" 2>&1 — the log never tells anyone to run the daemon start +# command (a line that starts with it, i.e. an instruction to run it). +no_start_command() { + if grep -E '^[[:space:]]*(pilotctl daemon (stop && pilotctl daemon )?start|sudo systemctl enable --now pilot-daemon)' "$1" >/dev/null; then + fail "$2: the output still tells the agent to start the daemon directly" + fi } cfg_get() { # cfg_get — prints the value, "" when absent @@ -207,7 +254,8 @@ SECRET="s3cretPW" PROXY="http://muse:${SECRET}@egress.test:3128" # shellcheck disable=SC2016 # the literal command the installer saves SANDBOX_CMD='bash -c '\''case $https_proxy in *@*) printf %s "$https_proxy";; *) printf %s "${HTTPS_PROXY:-$https_proxy}";; esac'\''' -unset HTTPS_PROXY https_proxy ALL_PROXY all_proxy PILOT_TRANSPORT PILOT_PROXY_CMD PILOT_ALLOW_ROOT 2>/dev/null || true +unset HTTPS_PROXY https_proxy ALL_PROXY all_proxy PILOT_TRANSPORT PILOT_PROXY_CMD PILOT_ALLOW_ROOT \ + PILOT_PROXY SUDO_USER SUDO_UID SUDO_GID SUDO_COMMAND DOAS_USER PKEXEC_UID 2>/dev/null || true # 1. --transport is validated. if run_install "$WORK/h-bad" "$FIXTURE/new" "$WORK/bad.log" --transport quic; then @@ -220,6 +268,29 @@ run_install "$WORK/h-default" "$FIXTURE/new" "$WORK/default.log" || fail "defaul [ -z "$(cfg_get "$WORK/h-default" transport)" ] || fail "default install saved a transport" [ -z "$(cfg_get "$WORK/h-default" proxy_cmd)" ] || fail "default install saved proxy_cmd" grep -F "Transport: auto" "$WORK/default.log" >/dev/null || fail "default install did not report transport auto" +grep -F "Verified SHA-256 (checksums.txt + manifest)" "$WORK/default.log" >/dev/null \ + || fail "latest_stable install was not checked against both anchors" +if grep -F -- "--transport compat" "$WORK/default.log" >/dev/null; then + fail "a daemon with auto got the UDP-blocked compat hint" +fi + +# 2b. A release without auto (v1.13.x) is not announced as auto: the summary +# says udp, and how to get compat where UDP is blocked. +run_install "$WORK/h-default-old" "$FIXTURE/old" "$WORK/default-old.log" || fail "default install (old daemon)" +if grep -iE 'Transport: +auto' "$WORK/default-old.log" >/dev/null; then + fail "a release without auto was announced as auto" +fi +grep -F "Transport: udp (this pilot-daemon, v9.9.9, predates auto" "$WORK/default-old.log" >/dev/null \ + || fail "old daemon: transport udp not stated" +grep -F "install.sh | sh -s -- --transport compat" "$WORK/default-old.log" >/dev/null \ + || fail "old daemon: no compat hint for UDP-blocked hosts" +grep -E '^[[:space:]]*pilotctl daemon start --hostname' "$WORK/default-old.log" >/dev/null \ + || fail "a host without a proxy lost the daemon start instruction" +# ...but not when udp was chosen. +run_install "$WORK/h-udp-old" "$FIXTURE/old" "$WORK/udp-old.log" --transport udp || fail "--transport udp (old daemon)" +if grep -F -- "--transport compat" "$WORK/udp-old.log" >/dev/null; then + fail "--transport udp got the compat hint" +fi # 3. --transport compat is saved and survives a re-run without --transport; # --transport auto then removes it. @@ -238,6 +309,12 @@ if [ ! -d /run/systemd/system ]; then run_install "$WORK/h-sandbox" "$FIXTURE/new" "$LOG") || fail "sandbox install" [ "$(cfg_get "$WORK/h-sandbox" proxy_cmd)" = "$SANDBOX_CMD" ] || fail "proxy_cmd not saved in a sandbox: '$(cfg_get "$WORK/h-sandbox" proxy_cmd)'" grep -F 'http://***@egress.test:3128' "$LOG" >/dev/null || fail "proxy not shown redacted" + # A daemon that uses the proxy is started the ordinary way. + grep -E '^[[:space:]]*pilotctl daemon start --hostname' "$LOG" >/dev/null \ + || fail "a daemon that can use the proxy lost the daemon start instruction" + if grep -F "pilot-sandbox recipe" "$LOG" >/dev/null; then + fail "a daemon that can use the proxy was sent to the sandbox recipe" + fi if grep -F "$SECRET" "$LOG" "$WORK/h-sandbox/.pilot/config.json" >/dev/null; then fail "proxy credentials leaked" fi @@ -269,6 +346,48 @@ if [ ! -d /run/systemd/system ]; then grep -F "cannot use one" "$LOG" >/dev/null || fail "no warning about a proxy the old daemon cannot use" grep -F "https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" "$LOG" >/dev/null \ || fail "the old-daemon proxy warning does not point at the sandbox recipe" + # The proxy is the way out (its credentials rotate): nothing tells the + # agent to run `pilotctl daemon start`, which would dial the registry + # around the proxy; GET STARTED points at the recipe instead. + no_start_command "$LOG" "old daemon in a proxy-only sandbox" + # shellcheck disable=SC2016 # literal backquotes + grep -F 'Do NOT run `pilotctl daemon start` on this host' "$LOG" >/dev/null \ + || fail "GET STARTED does not warn against pilotctl daemon start" + [ "$(grep -c -F "https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" "$LOG")" -ge 3 ] \ + || fail "the warning, the no-systemd hint and GET STARTED do not all point at the recipe" + if grep -iE 'Transport: +auto' "$LOG" >/dev/null; then fail "old daemon announced as auto (sandbox)"; fi + if grep -F -- "--transport compat" "$LOG" >/dev/null; then + fail "the compat hint was shown where compat cannot use the proxy either" + fi + if grep -F "restart the daemon from a fresh shell" "$LOG" >/dev/null; then + fail "a daemon that cannot use a proxy was told to refresh proxy credentials" + fi + # A re-run (update) does not tell it to restart the daemon directly either. + LOG="$WORK/oldcmd-rerun.log" + (export HTTPS_PROXY="$PROXY" + run_install "$WORK/h-oldcmd" "$FIXTURE/old" "$LOG") || fail "old daemon sandbox re-run" + no_start_command "$LOG" "old daemon sandbox re-run" + grep -F "https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" "$LOG" >/dev/null \ + || fail "the re-run does not point at the recipe" + LOG="$WORK/oldcmd.log" + # --transport compat on the same host: same story. + LOG="$WORK/oldcmd-compat.log" + (export HTTPS_PROXY="$PROXY" + run_install "$WORK/h-oldcmd-compat" "$FIXTURE/old" "$LOG" --transport compat) || fail "old daemon sandbox compat install" + no_start_command "$LOG" "old daemon, compat, proxy-only sandbox" + LOG="$WORK/oldcmd.log" + + # A proxy without credentials may not be the only way out: the start + # command stays, with the condition and the recipe next to it. + LOG="$WORK/old-nocreds.log" + (export HTTPS_PROXY=http://egress.test:3128 + run_install "$WORK/h-old-nocreds" "$FIXTURE/old" "$LOG") || fail "old daemon, proxy without credentials" + grep -F "skip the next line and use the pilot-sandbox" "$LOG" >/dev/null \ + || fail "no condition next to the start command (proxy without credentials)" + grep -E '^[[:space:]]*pilotctl daemon start --hostname' "$LOG" >/dev/null \ + || fail "the start command was dropped for a proxy that may not be the only way out" + LOG="$WORK/oldcmd.log" + # auto is not supported, so the daemon runs udp: the stock endpoints stay. [ "$(cfg_get "$WORK/h-oldcmd" registry)" = "34.71.57.205:9000" ] || fail "udp install lost the raw registry" if grep -F "$SECRET" "$LOG" >/dev/null || grep -rF "$SECRET" "$WORK/h-oldcmd/.pilot" >/dev/null; then @@ -333,6 +452,15 @@ ENV grep -F "installing as root (no systemd" "$LOG" >/dev/null || fail "no root note" [ -x "$WORK/h-root/.pilot/bin/pilotctl" ] || fail "root install did not install pilotctl" [ "$(cfg_get "$WORK/h-root" proxy_cmd)" = "$SANDBOX_CMD" ] || fail "root sandbox install did not save proxy_cmd" + + # sudo run by root itself (SUDO_UID=0) is still root's own install. + LOG="$WORK/root-sudo-root.log" + (export PILOT_TEST_UID=0 SUDO_USER=root SUDO_UID=0 + run_install "$WORK/h-root-sudo-root" "$FIXTURE/new" "$LOG") || fail "root via sudo from root was refused" + # PILOT_ALLOW_ROOT=1 still overrides the sudo refusal. + LOG="$WORK/root-sudo-allow.log" + (export PILOT_TEST_UID=0 SUDO_USER=agent SUDO_UID=1000 PILOT_ALLOW_ROOT=1 + run_install "$WORK/h-root-sudo-allow" "$FIXTURE/new" "$LOG") || fail "PILOT_ALLOW_ROOT=1 did not override the sudo refusal" else # 5b. Root on a host with systemd is still refused (PILOT_ALLOW_ROOT=1 overrides). LOG="$WORK/root.log" @@ -371,6 +499,13 @@ if grep -F "34.71.57.205" "$WORK/h-oldctl/.pilot/config.json" >/dev/null; then fail "old pilotctl compat install wrote a raw-IP endpoint" fi +# 9b. compat with a daemon that predates -transport: the summary does not +# claim compat. +run_install "$WORK/h-ancient" "$FIXTURE/ancient" "$WORK/ancient.log" --transport compat || fail "ancient daemon compat install" +grep -F "predates compat mode" "$WORK/ancient.log" >/dev/null || fail "ancient daemon: no compat warning" +grep -F "Transport: udp (compat is saved, but this pilot-daemon" "$WORK/ancient.log" >/dev/null \ + || fail "ancient daemon: the summary claims compat" + # 10. Root on macOS is refused like on any host with a service manager. LOG="$WORK/root-mac.log" if (export PILOT_TEST_UID=0 PILOT_TEST_UNAME=Darwin; run_install "$WORK/h-root-mac" "$FIXTURE/new" "$LOG"); then @@ -378,11 +513,49 @@ if (export PILOT_TEST_UID=0 PILOT_TEST_UNAME=Darwin; run_install "$WORK/h-root-m fi grep -F "refusing to install as root" "$LOG" >/dev/null || fail "no root refusal on macOS" +# 10b. Root through sudo/doas for a regular user is refused, with or without +# systemd: that user could not use a node installed for root. +for _elev in "SUDO_USER=agent SUDO_UID=1000" "SUDO_USER=agent" "DOAS_USER=agent"; do + LOG="$WORK/root-elev.log" + rm -rf "$WORK/h-root-elev" + # shellcheck disable=SC2086,SC2163 # intentional split into NAME=value words + if (export PILOT_TEST_UID=0 $_elev; run_install "$WORK/h-root-elev" "$FIXTURE/new" "$LOG"); then + fail "root install under '$_elev' was accepted" + fi + grep -F "refusing to install as root: this runs under" "$LOG" >/dev/null || fail "no sudo refusal ($_elev)" + grep -F "for agent" "$LOG" >/dev/null || fail "the sudo refusal does not name the user ($_elev)" + [ ! -e "$WORK/h-root-elev/.pilot" ] || fail "refused sudo install left ~/.pilot behind ($_elev)" +done + +# 10c. --version / --channel beta: a tag the manifest does not describe is +# checked against checksums.txt alone (previously: "integrity anchors +# disagree" for every tag but latest_stable). +for _mf in pinned pinned-nourl; do + run_install "$WORK/h-pin-$_mf" "$FIXTURE/$_mf" "$WORK/pin-$_mf.log" --version v9.9.8 --yes \ + || fail "--version v9.9.8 ($_mf manifest) was refused" + grep -F "Verified SHA-256 (checksums.txt)" "$WORK/pin-$_mf.log" >/dev/null \ + || fail "--version v9.9.8 ($_mf manifest): not verified against checksums.txt" + [ "$(cat "$WORK/h-pin-$_mf/.pilot/bin/.pilot-version")" = v9.9.8 ] || fail "--version v9.9.8 ($_mf): wrong version file" + run_install "$WORK/h-beta-$_mf" "$FIXTURE/$_mf" "$WORK/beta-$_mf.log" --channel beta \ + || fail "--channel beta ($_mf manifest) was refused" + grep -F "Downloading v9.9.10-rc.1" "$WORK/beta-$_mf.log" >/dev/null || fail "--channel beta did not resolve the beta tag" + # The tag the manifest describes still needs both anchors to agree. + if run_install "$WORK/h-latest-$_mf" "$FIXTURE/$_mf" "$WORK/latest-$_mf.log" --version v9.9.9; then + fail "a manifest hash that disagrees was ignored for latest_stable ($_mf)" + fi + grep -F "integrity anchors disagree" "$WORK/latest-$_mf.log" >/dev/null || fail "no anchor mismatch error ($_mf)" +done +# A manifest whose platform url names the pinned tag is an anchor for it. +if run_install "$WORK/h-pin-named" "$FIXTURE/pinned-named" "$WORK/pin-named.log" --version v9.9.8 --yes; then + fail "a manifest hash for the pinned tag that disagrees was ignored" +fi +grep -F "integrity anchors disagree" "$WORK/pin-named.log" >/dev/null || fail "no anchor mismatch error (url names the tag)" + # 11. --help prints the whole usage header and nothing past it. -sh "$ROOT/install.sh" --help > "$WORK/help.log" 2>&1 || fail "--help failed" +"${PILOT_TEST_SH:-sh}" "$ROOT/install.sh" --help > "$WORK/help.log" 2>&1 || fail "--help failed" grep -F -- "--transport " "$WORK/help.log" >/dev/null || fail "--help lacks --transport" grep -F "with a message, never fatal." "$WORK/help.log" >/dev/null || fail "--help cut the header short" if grep -F "WHAT THIS SCRIPT DOES" "$WORK/help.log" >/dev/null; then fail "--help printed past the usage header"; fi -rm -rf "$WORK" +if [ -n "${PILOT_TEST_KEEP:-}" ]; then echo "logs kept in $WORK"; else rm -rf "$WORK"; fi echo "proxy/transport installer contract: ok" From 662bdaa97830a37df04ec16dac8cf006dd0602fe Mon Sep 17 00:00:00 2001 From: Teodor Calin Date: Thu, 24 Sep 2026 12:02:27 +0300 Subject: [PATCH 5/5] installer: truthful proxy_cmd and restart advice (release#49 review round 2) - Save the sandbox proxy_cmd only when the proxy environment itself carries the credentials (read before PILOT_PROXY is copied into it for the downloads) and no explicit PILOT_PROXY is set. Credentials that arrive in PILOT_PROXY never reach a fresh shell, so the command printed nothing and the summary still claimed "rotation needs no restart"; next to an explicit PILOT_PROXY the command would have replaced it (pilotctl leaves it alone the same way). - Restart advice on a proxy-only host ("No managed service was running") now says to stop the running daemon first (`pilotctl daemon stop`) before the pilot-sandbox recipe starts one: the recipe never stops a daemon, and following the old advice ran two daemons with one identity. - macOS: the LaunchAgent start line goes through start_hint like every other start line (condition on PROXY_UNSUPPORTED, "Do not run" on PROXY_ONLY), and nothing points macOS at the Linux-root-only recipe: PROXY_REMEDY there is a re-run once a release whose pilot-daemon -h lists -proxy is out. - `--transport auto` with a daemon that predates auto no longer claims "keeps its default (udp)" while a saved compat stays in effect: it says the saved transport is kept and how to switch. tests/proxy-transport-install.sh covers each case (new darwin fixture with a fake launchctl); passes on macOS and in a Linux container as root, as a regular user and with /run/systemd/system present. shellcheck clean. Co-Authored-By: Claude Opus 5.5 (1M context) --- install.sh | 90 +++++++++++++++++++++++--------- tests/proxy-transport-install.sh | 83 +++++++++++++++++++++++++++-- 2 files changed, 145 insertions(+), 28 deletions(-) diff --git a/install.sh b/install.sh index 4e9b10b..7dc168c 100755 --- a/install.sh +++ b/install.sh @@ -438,6 +438,16 @@ EFFECTIVE_TRANSPORT="${TRANSPORT:-${CONFIG_TRANSPORT:-auto}}" # authenticating proxy is a credential. Nothing in this script writes a proxy # URL to disk. # +# ENV_PROXY_CREDS: the proxy environment itself carries credentials — read +# before PILOT_PROXY is copied into it below for this run's downloads. Only +# then can the sandbox proxy command (SANDBOX_PROXY_CMD), which prints what a +# fresh shell's $https_proxy / $HTTPS_PROXY hold, print them: credentials +# that arrive in PILOT_PROXY never reach a fresh shell. +ENV_PROXY_CREDS=false +case "${https_proxy:-}${HTTPS_PROXY:-}" in + *@*) ENV_PROXY_CREDS=true ;; +esac + # PILOT_PROXY is the daemon's own proxy setting; an http(s):// URL there # carries this run's downloads too when the environment names no proxy # (exported to this process and its children only). @@ -457,20 +467,23 @@ PILOT_PROXY_URL="${https_proxy:-${HTTPS_PROXY:-${all_proxy:-${ALL_PROXY:-}}}}" # keeps the ones it started with, and the proxy answers its next CONNECT with # 407. A fresh shell sees the current ones. PROXY_REFRESH_CMD prints the # current proxy URL: $PILOT_PROXY_CMD, else — in a Linux container/VM without -# systemd whose HTTPS_PROXY or https_proxy carries credentials — what a fresh -# bash has: whichever of $https_proxy and $HTTPS_PROXY carries credentials -# ($https_proxy when both do, the variable Meta Muse's guidance reads), else +# systemd whose HTTPS_PROXY or https_proxy carries credentials (ENV_PROXY_CREDS) +# and no explicit PILOT_PROXY is set — what a fresh bash has: whichever of +# $https_proxy and $HTTPS_PROXY carries credentials ($https_proxy when both do, the variable Meta Muse's guidance reads), else # ${HTTPS_PROXY:-$https_proxy}, so a URL with credentials is never traded for # one without (pilotctl uses the same command). It is saved as the daemon's # proxy_cmd further down, and pcurl uses it here to retry a download once # after the credentials rotated mid-install. # shellcheck disable=SC2016 # literal: the fresh bash expands it, not this shell SANDBOX_PROXY_CMD='bash -c '\''case $https_proxy in *@*) printf %s "$https_proxy";; *) printf %s "${HTTPS_PROXY:-$https_proxy}";; esac'\''' +# An explicit PILOT_PROXY is left alone, as pilotctl leaves it: pilot-daemon +# runs a proxy command in place of the URL it would use, so the sandbox +# command would replace that URL with the environment's proxy. PROXY_REFRESH_CMD="${PILOT_PROXY_CMD:-}" if [ -z "$PROXY_REFRESH_CMD" ] && [ "$SANDBOX_HOST" = true ] \ - && command -v bash >/dev/null 2>&1; then - case "${https_proxy:-}${HTTPS_PROXY:-}" in - *@*) PROXY_REFRESH_CMD="$SANDBOX_PROXY_CMD" ;; + && [ "$ENV_PROXY_CREDS" = true ] && command -v bash >/dev/null 2>&1; then + case "${PILOT_PROXY:-}" in + ""|auto|AUTO|Auto) PROXY_REFRESH_CMD="$SANDBOX_PROXY_CMD" ;; esac fi @@ -1367,7 +1380,16 @@ case "$TRANSPORT" in if [ "$DAEMON_HAS_AUTO" = true ]; then if [ -n "$CONFIG_TRANSPORT" ]; then TRANSPORT_CLEAR=true; fi else - echo " Note: this pilot-daemon (${TAG:-source}) predates -transport=auto; it keeps its default (udp)." + # Nothing is saved or removed: this release has no auto to go + # back to, and what it runs is what config.json already says. + case "$CONFIG_TRANSPORT" in + compat|udp) + echo " Note: this pilot-daemon (${TAG:-source}) predates -transport=auto; it keeps the" + echo " transport saved in config.json (${CONFIG_TRANSPORT}). Switch with --transport udp or" + echo " --transport compat." ;; + *) + echo " Note: this pilot-daemon (${TAG:-source}) predates -transport=auto; it keeps its default (udp)." ;; + esac fi ;; esac if [ "$CONFIG_TRANSPORT" = "auto" ] && [ "$DAEMON_HAS_AUTO" != true ] && [ -z "$TRANSPORT_TO_SAVE" ]; then @@ -1559,6 +1581,13 @@ fi # There every "start the daemon" instruction below is replaced by the # recipe; elsewhere it is kept, with the condition spelled out. SANDBOX_RECIPE_URL="https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" +# The recipe needs Linux (root and `unshare -m` for its SNI router): on any +# other OS the way forward is a release whose pilot-daemon has -proxy. +if [ "$OS" = "linux" ]; then + PROXY_REMEDY="the pilot-sandbox recipe (step 3): ${SANDBOX_RECIPE_URL}" +else + PROXY_REMEDY="re-run this installer once a Pilot release whose pilot-daemon -h lists -proxy is out" +fi PROXY_UNSUPPORTED=false PROXY_ONLY=false if [ -n "$PILOT_PROXY_URL" ] && [ "$DAEMON_HAS_PROXY" != true ]; then @@ -1575,14 +1604,13 @@ if [ -n "$PILOT_PROXY_URL" ] && [ "$DAEMON_HAS_PROXY" != true ]; then echo " WARNING: this host goes out through a proxy ($(redact_proxy "$PILOT_PROXY_URL")), and this" echo " pilot-daemon (${TAG:-source}) cannot use one: \`pilotctl daemon start\` would dial" echo " the registry directly, around the proxy, and never come online." - echo " Do not run it here. Bring the node online with the pilot-sandbox" - echo " recipe (step 3): ${SANDBOX_RECIPE_URL}" + echo " Do not start it here. What brings the node online: ${PROXY_REMEDY}" else echo " WARNING: a proxy is set ($(redact_proxy "$PILOT_PROXY_URL")), but this pilot-daemon" echo " (${TAG:-source}) cannot use one: it dials the registry directly. If the" echo " proxy is this host's only way out (UDP blocked, e.g. an agent sandbox)," - echo " \`pilotctl daemon start\` will not come online with this release: use the" - echo " pilot-sandbox recipe (step 3) instead: ${SANDBOX_RECIPE_URL}" + echo " the daemon will not come online with this release. What does then:" + echo " ${PROXY_REMEDY}" fi fi @@ -1598,18 +1626,25 @@ if [ "$EFFECTIVE_TRANSPORT" = "udp" ] && [ "$DAEMON_HAS_AUTO" != true ] \ UDP_ONLY_HINT=true fi -# start_hint PREFIX COMMAND [NAME] — print how to start the daemon: COMMAND, -# except where this daemon cannot use the proxy (the WARNING above). On a -# proxy-only host (PROXY_ONLY) COMMAND is not printed as something to run: -# only that NAME (default: COMMAND) must not be run there, and the recipe. +# start_hint PREFIX COMMAND [NAME [STOP]] — print how to start the daemon: +# COMMAND, except where this daemon cannot use the proxy (the WARNING above). +# On a proxy-only host (PROXY_ONLY) COMMAND is not printed as something to +# run: only that NAME (default: COMMAND) must not be run there, and +# PROXY_REMEDY. STOP (restart hints) is printed first there: the recipe starts +# a daemon but never stops one, and two daemons must not share an identity. start_hint() { if [ "$PROXY_ONLY" = true ]; then echo "${1}Do not run \`${3:-$2}\` on this host (see the WARNING above)." - echo "${1}Use the pilot-sandbox recipe (step 3): ${SANDBOX_RECIPE_URL}" + if [ -n "${4:-}" ]; then + echo "${1}Stop the running daemon first: ${4}" + echo "${1}then bring it back with ${PROXY_REMEDY}" + else + echo "${1}What brings the node online: ${PROXY_REMEDY}" + fi elif [ "$PROXY_UNSUPPORTED" = true ]; then echo "${1}${2}" - echo "${1}(if the proxy is this host's only way out, use the pilot-sandbox recipe" - echo "${1} instead, see the WARNING above)" + echo "${1}(if the proxy is this host's only way out, it will not come online with" + echo "${1} this release: see the WARNING above)" else echo "${1}${2}" fi @@ -2035,7 +2070,12 @@ UPLIST case " $RESTART_LAUNCHD " in *" network.pilotprotocol.pilot-daemon "*) ;; *) - echo " Start daemon: launchctl load -w $PLIST" + if [ "$PROXY_UNSUPPORTED" = true ]; then + echo " Start daemon:" + start_hint " " "launchctl load -w $PLIST" "launchctl load -w $PLIST" + else + echo " Start daemon: launchctl load -w $PLIST" + fi echo " Stop daemon: launchctl unload $PLIST" ;; esac @@ -2210,7 +2250,7 @@ if [ "$UPDATING" = true ]; then if [ -z "$RESTART_SYSTEMD" ] && [ -z "$RESTART_LAUNCHD" ]; then echo "No managed service was running. If you run the daemon yourself," echo "restart it to pick up the new version:" - start_hint " " "pilotctl daemon stop && pilotctl daemon start" "pilotctl daemon start" + start_hint " " "pilotctl daemon stop && pilotctl daemon start" "pilotctl daemon start" "pilotctl daemon stop" echo "" fi exit 0 @@ -2263,13 +2303,13 @@ echo " export PATH=\"${BIN_DIR}:\$PATH\" # only needed in THIS shell, befo # (see PROXY_UNSUPPORTED / PROXY_ONLY): a proxy-only host gets the recipe. if [ "$PROXY_ONLY" = true ]; then echo " # Do NOT run \`pilotctl daemon start\` on this host: this pilot-daemon (${TAG:-source})" - echo " # cannot use the proxy and would dial the registry directly. Bring the node" - echo " # online with the pilot-sandbox recipe (step 3), then check it here:" - echo " # ${SANDBOX_RECIPE_URL}" + echo " # cannot use the proxy and would dial the registry directly. What brings the" + echo " # node online (then check it here):" + echo " # ${PROXY_REMEDY}" elif [ "$PROXY_UNSUPPORTED" = true ]; then echo " # This pilot-daemon (${TAG:-source}) cannot use the proxy (see the WARNING above). If the" - echo " # proxy is this host's only way out, skip the next line and use the pilot-sandbox" - echo " # recipe (step 3) instead: ${SANDBOX_RECIPE_URL}" + echo " # proxy is this host's only way out, skip the next line; what works then:" + echo " # ${PROXY_REMEDY}" echo " pilotctl daemon start --hostname my-agent # blocks until registered; email already saved" else echo " pilotctl daemon start --hostname my-agent # blocks until registered; email already saved" diff --git a/tests/proxy-transport-install.sh b/tests/proxy-transport-install.sh index 3285370..bb9311d 100644 --- a/tests/proxy-transport-install.sh +++ b/tests/proxy-transport-install.sh @@ -20,6 +20,12 @@ # to run `pilotctl daemon start` where the proxy is the way out, and a # release without auto is not announced as auto (with the compat re-run # for UDP-blocked hosts); +# - the sandbox proxy_cmd is saved only for credentials a fresh shell sees +# (not for PILOT_PROXY's), and never over an explicit PILOT_PROXY; +# - restart advice on a proxy-only host stops the running daemon before the +# recipe starts one; macOS is never sent to the Linux-only recipe, and its +# LaunchAgent start line carries the same condition as the others; +# - --transport auto with a daemon that predates auto says what stays saved; # - --version / --channel beta install a tag the manifest does not describe # (checksums.txt is its anchor), while the manifest hash still has to # agree for the tag it describes. @@ -128,8 +134,9 @@ chmod 755 "$FIXTURE"/new/archive/* "$FIXTURE"/old/archive/* "$FIXTURE"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/ancient/a make_release() { # make_release [ [ []]] COPYFILE_DISABLE=1 tar -czf "$1/pilot-linux-amd64.tar.gz" -C "$1/archive" . + cp "$1/pilot-linux-amd64.tar.gz" "$1/pilot-darwin-amd64.tar.gz" # same fixture binaries _sha=$(shasum -a 256 "$1/pilot-linux-amd64.tar.gz" | awk '{print $1}') - printf '%s %s\n' "$_sha" pilot-linux-amd64.tar.gz > "$1/checksums.txt" + printf '%s %s\n%s %s\n' "$_sha" pilot-linux-amd64.tar.gz "$_sha" pilot-darwin-amd64.tar.gz > "$1/checksums.txt" _url="" if [ -n "${5:-}" ]; then _url="\"url\": \"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/pilot-protocol/pilotprotocol/releases/download/$5/pilot-linux-amd64.tar.gz\", " @@ -139,7 +146,7 @@ make_release() { # make_release [ [ [&2; exit 88 ;; esac @@ -216,6 +224,8 @@ cp "$src" "$output" SH # Never escalate on the machine running the test. printf '#!/bin/sh\nexit 1\n' > "$FAKEBIN/sudo" +# launchd (PILOT_TEST_UNAME=Darwin cases): nothing is loaded, nothing fails. +printf '#!/bin/sh\nexit 0\n' > "$FAKEBIN/launchctl" # `id -u` answers $PILOT_TEST_UID when set. REAL_ID=$(command -v id) cat > "$FAKEBIN/id" </dev/null \ + || fail "old daemon: --transport auto does not say the saved compat stays" +if grep -F "keeps its default (udp)" "$WORK/compat-old2.log" >/dev/null; then + fail "old daemon: --transport auto claims udp while compat stays saved" +fi +grep -F "Transport: compat" "$WORK/compat-old2.log" >/dev/null || fail "old daemon: summary does not report the saved compat" + # 4. Sandbox (Linux without systemd) with a credential-bearing HTTPS_PROXY: # proxy_cmd is saved and the credentials never reach the output. if [ ! -d /run/systemd/system ]; then @@ -369,6 +392,11 @@ if [ ! -d /run/systemd/system ]; then no_start_command "$LOG" "old daemon sandbox re-run" grep -F "https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse" "$LOG" >/dev/null \ || fail "the re-run does not point at the recipe" + # The recipe starts a daemon but never stops one: the restart advice + # must stop the running one first, or following it runs two daemons + # with one identity. + grep -F "Stop the running daemon first: pilotctl daemon stop" "$LOG" >/dev/null \ + || fail "the re-run advice dropped \`pilotctl daemon stop\` before the recipe" LOG="$WORK/oldcmd.log" # --transport compat on the same host: same story. LOG="$WORK/oldcmd-compat.log" @@ -382,7 +410,7 @@ if [ ! -d /run/systemd/system ]; then LOG="$WORK/old-nocreds.log" (export HTTPS_PROXY=http://egress.test:3128 run_install "$WORK/h-old-nocreds" "$FIXTURE/old" "$LOG") || fail "old daemon, proxy without credentials" - grep -F "skip the next line and use the pilot-sandbox" "$LOG" >/dev/null \ + grep -F "skip the next line; what works then:" "$LOG" >/dev/null \ || fail "no condition next to the start command (proxy without credentials)" grep -E '^[[:space:]]*pilotctl daemon start --hostname' "$LOG" >/dev/null \ || fail "the start command was dropped for a proxy that may not be the only way out" @@ -445,6 +473,27 @@ ENV (export PILOT_PROXY=http://relay.test:3128 PILOT_TEST_EXPECT_PROXY=http://relay.test:3128 run_install "$WORK/h-pilot-proxy" "$FIXTURE/new" "$LOG") || fail "downloads did not use PILOT_PROXY" + # 4d. Credentials that come from PILOT_PROXY never reach a fresh shell, + # so the sandbox proxy command (which prints a fresh shell's + # $https_proxy / $HTTPS_PROXY) is not saved and rotation is not + # claimed; nor does it replace an explicit PILOT_PROXY next to a + # credential-bearing HTTPS_PROXY (the daemon runs a proxy command in + # place of the URL it would use). + LOG="$WORK/pilot-proxy-creds.log" + (export PILOT_PROXY="$PROXY" + run_install "$WORK/h-pilot-proxy-creds" "$FIXTURE/new" "$LOG") || fail "PILOT_PROXY with credentials" + [ -z "$(cfg_get "$WORK/h-pilot-proxy-creds" proxy_cmd)" ] || fail "sandbox proxy_cmd saved for credentials that come from PILOT_PROXY" + if grep -F -e "re-read by the daemon" -e "rotation needs no restart" "$LOG" >/dev/null; then + fail "rotation claimed for credentials that come from PILOT_PROXY" + fi + if grep -F "$SECRET" "$LOG" >/dev/null || grep -rF "$SECRET" "$WORK/h-pilot-proxy-creds/.pilot" >/dev/null; then + fail "proxy credentials leaked (PILOT_PROXY)" + fi + LOG="$WORK/pilot-proxy-explicit.log" + (export PILOT_PROXY=http://relay.test:3128 HTTPS_PROXY="$PROXY" + run_install "$WORK/h-pilot-proxy-explicit" "$FIXTURE/new" "$LOG") || fail "explicit PILOT_PROXY next to HTTPS_PROXY" + [ -z "$(cfg_get "$WORK/h-pilot-proxy-explicit" proxy_cmd)" ] || fail "sandbox proxy_cmd saved over an explicit PILOT_PROXY" + # 5. Root: allowed in a Linux container/VM without systemd. LOG="$WORK/root.log" (export PILOT_TEST_UID=0 HTTPS_PROXY="$PROXY" @@ -513,6 +562,34 @@ if (export PILOT_TEST_UID=0 PILOT_TEST_UNAME=Darwin; run_install "$WORK/h-root-m fi grep -F "refusing to install as root" "$LOG" >/dev/null || fail "no root refusal on macOS" +# 10a. macOS with a proxy this release cannot use: the LaunchAgent start +# line carries the same condition as every other start line, and +# nothing points at the Linux-root-only sandbox recipe. +LOG="$WORK/mac-proxy.log" +(export PILOT_TEST_UNAME=Darwin HTTPS_PROXY="$PROXY" + run_install "$WORK/h-mac-proxy" "$FIXTURE/old" "$LOG") || fail "macOS install behind a proxy (old daemon)" +grep -F "cannot use one" "$LOG" >/dev/null || fail "macOS: no warning about a proxy the old daemon cannot use" +if grep -F "pilot-sandbox recipe (step 3)" "$LOG" >/dev/null; then + fail "macOS was sent to the Linux-only pilot-sandbox recipe" +fi +if grep -E '^[[:space:]]*Start daemon: launchctl load' "$LOG" >/dev/null; then + fail "macOS: the LaunchAgent start line has no condition next to it" +fi +grep -F "it will not come online with" "$LOG" >/dev/null || fail "macOS: no condition next to the LaunchAgent start line" +grep -F "lists -proxy" "$LOG" >/dev/null || fail "macOS: no way forward named" +LOG="$WORK/mac-proxy-cmd.log" +(export PILOT_TEST_UNAME=Darwin HTTPS_PROXY="$PROXY" PILOT_PROXY_CMD='cat /run/proxy-url' + run_install "$WORK/h-mac-proxy-cmd" "$FIXTURE/old" "$LOG") || fail "macOS install, PILOT_PROXY_CMD (old daemon)" +if grep -E '^[[:space:]]*(launchctl load|Start daemon: launchctl load)' "$LOG" >/dev/null; then + fail "macOS proxy-only: still told to load the LaunchAgent" +fi +# shellcheck disable=SC2016 # literal backquotes +grep -F 'Do not run `launchctl load -w' "$LOG" >/dev/null || fail "macOS proxy-only: no warning against loading the LaunchAgent" +if grep -F "pilot-sandbox recipe (step 3)" "$LOG" >/dev/null; then + fail "macOS proxy-only was sent to the Linux-only pilot-sandbox recipe" +fi +if grep -F "$SECRET" "$LOG" >/dev/null; then fail "proxy credentials leaked (macOS)"; fi + # 10b. Root through sudo/doas for a regular user is refused, with or without # systemd: that user could not use a node installed for root. for _elev in "SUDO_USER=agent SUDO_UID=1000" "SUDO_USER=agent" "DOAS_USER=agent"; do