diff --git a/.github/workflows/publish-manifest.yml b/.github/workflows/publish-manifest.yml new file mode 100644 index 00000000..268e2ec6 --- /dev/null +++ b/.github/workflows/publish-manifest.yml @@ -0,0 +1,113 @@ +name: publish-manifest + +# Receives a `repository_dispatch` event of type `manifest-update` from +# `web4/.github/workflows/release.yml` after a tag ships. The event carries +# the rendered manifest in `client_payload.manifest`; this workflow merges it +# into `public/.well-known/latest.json` and commits to main, which triggers +# the Cloudflare Pages deploy. +# +# Merge rules: +# - `latest_stable` is only advanced when the incoming manifest is for a +# non-prerelease tag (`is_prerelease == false`). RC tags update only +# `latest_prerelease` and `channels.edge`. +# - `platforms` is replaced wholesale with the incoming map for the tag. +# - `updated_at` is taken from the incoming manifest. +# +# Manual trigger (`workflow_dispatch`) is available for the case where the +# dispatch from web4 was dropped (missing token, transient 5xx) and the +# operator needs to nudge the manifest forward by hand. + +on: + repository_dispatch: + types: [manifest-update] + workflow_dispatch: + inputs: + manifest: + description: 'Inline manifest JSON (overrides the dispatch payload path)' + required: true + +permissions: + contents: write + +jobs: + merge-and-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + + - name: Resolve incoming manifest + id: incoming + env: + DISPATCH_MANIFEST: ${{ toJson(github.event.client_payload.manifest) }} + MANUAL_MANIFEST: ${{ inputs.manifest }} + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "$MANUAL_MANIFEST" > /tmp/incoming.json + else + echo "$DISPATCH_MANIFEST" > /tmp/incoming.json + fi + jq . /tmp/incoming.json > /tmp/incoming.pretty.json + cp /tmp/incoming.pretty.json /tmp/incoming.json + echo "incoming manifest:" + cat /tmp/incoming.json + + - name: Merge into public/.well-known/latest.json + run: | + INCOMING=/tmp/incoming.json + CURRENT=public/.well-known/latest.json + mkdir -p public/.well-known + if [ ! -f "$CURRENT" ]; then + # First-ever publish — start from the incoming manifest, but + # rewrite the prerelease-aware fields into their final names. + jq '{ + "$schema": "https://pilotprotocol.network/.well-known/latest.schema.json", + "updated_at": .updated_at, + "latest_stable": (.proposed_stable // ""), + "latest_prerelease": (.proposed_edge // .tag), + "channels": { + "stable": (.proposed_stable // ""), + "edge": (.proposed_edge // .tag) + }, + "release_notes_url": .release_notes_url, + "homebrew_formula_url": .homebrew_formula_url, + "platforms": .platforms + }' "$INCOMING" > "$CURRENT" + else + jq --slurpfile inc "$INCOMING" ' + . as $cur | + $inc[0] as $i | + { + "$schema": ($cur["$schema"] // "https://pilotprotocol.network/.well-known/latest.schema.json"), + "updated_at": $i.updated_at, + "latest_stable": (if $i.is_prerelease then $cur.latest_stable else $i.proposed_stable end), + "latest_prerelease": ($i.proposed_edge // $i.tag), + "channels": { + "stable": (if $i.is_prerelease then $cur.channels.stable else $i.proposed_stable end), + "edge": ($i.proposed_edge // $i.tag) + }, + "release_notes_url": $i.release_notes_url, + "homebrew_formula_url": $i.homebrew_formula_url, + "platforms": $i.platforms + } + ' "$CURRENT" > "$CURRENT.new" + mv "$CURRENT.new" "$CURRENT" + fi + echo "merged manifest:" + cat "$CURRENT" + + - name: Commit and push + run: | + if git diff --quiet public/.well-known/latest.json; then + echo "manifest unchanged — nothing to commit." + exit 0 + fi + git config user.name "pilot-release-bot" + git config user.email "release-bot@pilotprotocol.network" + TAG=$(jq -r '.latest_prerelease // .latest_stable // "unknown"' public/.well-known/latest.json) + git add public/.well-known/latest.json + git commit -m "manifest: bump to ${TAG}" \ + -m "Triggered by upstream release workflow." + git push origin main diff --git a/public/.well-known/latest.json b/public/.well-known/latest.json new file mode 100644 index 00000000..5f987262 --- /dev/null +++ b/public/.well-known/latest.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://pilotprotocol.network/.well-known/latest.schema.json", + "updated_at": "2026-05-28T14:00:00Z", + "latest_stable": "v1.10.5", + "latest_prerelease": "v1.10.5", + "channels": { + "stable": "v1.10.5", + "edge": "v1.10.5" + }, + "release_notes_url": "https://github.com/TeoSlayer/pilotprotocol/releases/tag/v1.10.5", + "homebrew_formula_url": "https://github.com/TeoSlayer/homebrew-pilot/raw/main/Formula/pilotprotocol.rb", + "platforms": { + "darwin-amd64": { + "url": "https://github.com/TeoSlayer/pilotprotocol/releases/download/v1.10.5/pilot-darwin-amd64.tar.gz", + "sha256": "eab41da1108068e965e4c500bfae6f5aa1b6a21d02b7feb3942fd2b6434fc3be" + }, + "darwin-arm64": { + "url": "https://github.com/TeoSlayer/pilotprotocol/releases/download/v1.10.5/pilot-darwin-arm64.tar.gz", + "sha256": "125fe7c8b83ce89f53487ec0ac1fe22decd4defd4af2b42559809042e2083597" + }, + "linux-amd64": { + "url": "https://github.com/TeoSlayer/pilotprotocol/releases/download/v1.10.5/pilot-linux-amd64.tar.gz", + "sha256": "b062e355f24d85b884345de396398cd95d5c70fd44d23a9633c1bbcd3003022b" + }, + "linux-arm64": { + "url": "https://github.com/TeoSlayer/pilotprotocol/releases/download/v1.10.5/pilot-linux-arm64.tar.gz", + "sha256": "37428a4b08e6880cffca5049df872746358c41f9d63c25d5896446c48aa7e165" + } + } +} diff --git a/public/install.sh b/public/install.sh index c1d1e48f..4d6f11d4 100755 --- a/public/install.sh +++ b/public/install.sh @@ -6,9 +6,20 @@ set -e # Hosted at: https://pilotprotocol.network/install.sh # # Usage: -# Install: curl -fsSL https://pilotprotocol.network/install.sh | sh -# RC build: PILOT_RC=1 curl -fsSL https://pilotprotocol.network/install.sh | sh -# Uninstall: curl -fsSL https://pilotprotocol.network/install.sh | sh -s uninstall +# Install: curl -fsSL https://pilotprotocol.network/install.sh | sh +# Pin a version: curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --version v1.10.5 +# Edge channel: curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --channel edge +# Uninstall: curl -fsSL https://pilotprotocol.network/install.sh | sh -s uninstall +# +# Flags: +# --version Install a specific tag. Warns when older than latest stable. +# --channel stable (default) or edge. Edge tracks the newest prerelease. +# --yes / -y Skip the older-version confirmation prompt. +# --no-warn Suppress the older-version warning entirely. +# +# Legacy env vars (still honored, lower precedence than flags): +# PILOT_RELEASE_TAG=vX.Y.Z Same as --version. +# PILOT_RC=1 Same as --channel edge. # # WHAT THIS SCRIPT DOES (read before piping to sh): # 1. Detects OS/arch (Linux/Darwin × amd64/arm64) @@ -62,6 +73,64 @@ BEACON="${PILOT_BEACON:-34.71.57.205:9001}" PILOT_DIR="$HOME/.pilot" BIN_DIR="$PILOT_DIR/bin" +# Canonical manifest URL — the single source of truth for "what version is +# current". Republished by web4 release.yml on every tag. Override only for +# testing PR-preview manifests. +MANIFEST_URL="${PILOT_MANIFEST_URL:-https://pilotprotocol.network/.well-known/latest.json}" + +# --- Parse CLI flags --- +# Flags are parsed BEFORE the root check so that `install.sh --yes uninstall` +# (and similar combinations) still recognize the `uninstall` positional. +PILOT_REQUESTED_VERSION="" +PILOT_REQUESTED_CHANNEL="" +PILOT_YES=0 +PILOT_NO_WARN=0 +PILOT_POSITIONAL="" + +while [ $# -gt 0 ]; do + case "$1" in + --version) + if [ $# -lt 2 ]; then echo "Error: --version requires a value" >&2; exit 2; fi + PILOT_REQUESTED_VERSION="$2"; shift 2 ;; + --version=*) + PILOT_REQUESTED_VERSION="${1#--version=}"; shift ;; + --channel) + if [ $# -lt 2 ]; then echo "Error: --channel requires a value" >&2; exit 2; fi + PILOT_REQUESTED_CHANNEL="$2"; shift 2 ;; + --channel=*) + PILOT_REQUESTED_CHANNEL="${1#--channel=}"; shift ;; + --yes|-y) + PILOT_YES=1; shift ;; + --no-warn) + PILOT_NO_WARN=1; shift ;; + -h|--help) + sed -n '4,21p' "$0" 2>/dev/null || echo "See https://pilotprotocol.network/install.sh" + exit 0 ;; + --) + shift + while [ $# -gt 0 ]; do PILOT_POSITIONAL="$PILOT_POSITIONAL $1"; shift; done + break ;; + -*) + echo "Error: unknown flag: $1" >&2 + echo " Run with --help for usage." >&2 + exit 2 ;; + *) + PILOT_POSITIONAL="$PILOT_POSITIONAL $1"; shift ;; + esac +done + +# Validate channel value early so we fail fast. +if [ -n "$PILOT_REQUESTED_CHANNEL" ] \ + && [ "$PILOT_REQUESTED_CHANNEL" != "stable" ] \ + && [ "$PILOT_REQUESTED_CHANNEL" != "edge" ]; then + echo "Error: --channel must be 'stable' or 'edge' (got: $PILOT_REQUESTED_CHANNEL)" >&2 + exit 2 +fi + +# 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. if [ "${1:-}" != "uninstall" ] && [ "$(id -u)" = "0" ] && [ -z "${PILOT_ALLOW_ROOT:-}" ]; then @@ -71,6 +140,58 @@ if [ "${1:-}" != "uninstall" ] && [ "$(id -u)" = "0" ] && [ -z "${PILOT_ALLOW_RO exit 1 fi +# --- Manifest + version helpers --- + +# fetch_manifest writes the manifest JSON to $1 and returns 0 on success. +# Soft-fails (returns 1) so callers fall back to the GitHub-redirect path +# when the manifest host is unreachable. +fetch_manifest() { + curl -fsSL --max-time 10 "$MANIFEST_URL" -o "$1" 2>/dev/null +} + +# manifest_field "" "" extracts a string field. Supports nested +# paths like "channels.stable" with a one-level sed slice — POSIX shell only, +# no jq dependency. Returns empty if the field is absent. +manifest_field() { + _mf_field="$1"; _mf_file="$2" + case "$_mf_field" in + *.*) + _mf_outer="${_mf_field%%.*}" + _mf_inner="${_mf_field#*.}" + sed -n "/\"${_mf_outer}\"[[:space:]]*:[[:space:]]*{/,/^[[:space:]]*}/p" "$_mf_file" \ + | grep "\"${_mf_inner}\"" | head -1 \ + | sed -E "s/.*\"${_mf_inner}\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\\1/" + ;; + *) + grep "\"${_mf_field}\"" "$_mf_file" | head -1 \ + | sed -E "s/.*\"${_mf_field}\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\\1/" + ;; + esac +} + +# 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 +# suffixes, so we split on "-" and compare the base versions first, then +# break ties on the prerelease suffix. +version_compare() { + _vc_a="${1#v}"; _vc_b="${2#v}" + if [ "$_vc_a" = "$_vc_b" ]; then echo 0; return; fi + _vc_a_base="${_vc_a%%-*}"; _vc_b_base="${_vc_b%%-*}" + if [ "$_vc_a_base" = "$_vc_b_base" ]; then + _vc_a_pre=0; case "$_vc_a" in *-*) _vc_a_pre=1 ;; esac + _vc_b_pre=0; case "$_vc_b" in *-*) _vc_b_pre=1 ;; esac + if [ "$_vc_a_pre" = "1" ] && [ "$_vc_b_pre" = "0" ]; then printf '%s\n' -1; return; fi + if [ "$_vc_a_pre" = "0" ] && [ "$_vc_b_pre" = "1" ]; then echo 1; return; fi + _vc_a_suf="${_vc_a#*-}"; _vc_b_suf="${_vc_b#*-}" + _vc_older=$(printf '%s\n%s\n' "$_vc_a_suf" "$_vc_b_suf" | sort -V | head -1) + if [ "$_vc_older" = "$_vc_a_suf" ]; then printf '%s\n' -1; else echo 1; fi + return + fi + _vc_older=$(printf '%s\n%s\n' "$_vc_a_base" "$_vc_b_base" | sort -V | head -1) + if [ "$_vc_older" = "$_vc_a_base" ]; then printf '%s\n' -1; else echo 1; fi +} + # --- Uninstall --- if [ "${1}" = "uninstall" ]; then @@ -194,26 +315,38 @@ trap 'rm -rf "$TMPDIR"' EXIT ARCHIVE="pilot-${OS}-${ARCH}.tar.gz" -# Resolve the release tag. -# - PILOT_RELEASE_TAG=v1.2.3 : explicit override, no network round-trip. -# - PILOT_RC=1 : list releases via api.github.com and pick the -# newest (incl. pre-releases). 403 rate-limited -# output is detected and reported instead of -# silently falling through to source-build with -# an unstamped version. -# - default : follow the /releases/latest/download/ redirect, -# which uses the unauthenticated CDN and is not -# subject to the 60/hr api.github.com rate limit. -if [ -n "${PILOT_RELEASE_TAG:-}" ]; then +# Resolve the release tag. Precedence (highest to lowest): +# 1. --version explicit pin via flag +# 2. PILOT_RELEASE_TAG env legacy explicit pin (back-compat) +# 3. --channel manifest channel lookup +# 4. PILOT_RC=1 env legacy "edge" channel (back-compat) +# 5. Manifest "latest_stable" preferred for the default install +# 6. GitHub /releases/latest redirect — fallback when the manifest host +# is unreachable. Unauthenticated CDN, not subject to the 60/hr +# api.github.com rate limit. +MANIFEST_FILE="$TMPDIR/manifest.json" +HAVE_MANIFEST=0 +if fetch_manifest "$MANIFEST_FILE"; then + HAVE_MANIFEST=1 +fi + +if [ -n "$PILOT_REQUESTED_VERSION" ]; then + TAG="$PILOT_REQUESTED_VERSION" +elif [ -n "${PILOT_RELEASE_TAG:-}" ]; then TAG="$PILOT_RELEASE_TAG" +elif [ -n "$PILOT_REQUESTED_CHANNEL" ] && [ "$HAVE_MANIFEST" = "1" ]; then + TAG=$(manifest_field "channels.${PILOT_REQUESTED_CHANNEL}" "$MANIFEST_FILE") +elif [ "${PILOT_RC:-}" = "1" ] && [ "$HAVE_MANIFEST" = "1" ]; then + TAG=$(manifest_field "channels.edge" "$MANIFEST_FILE") elif [ "${PILOT_RC:-}" = "1" ]; then + # Manifest unreachable; fall back to api.github.com for the newest tag. API_BODY="$TMPDIR/releases.json" API_CODE=$(curl -sSL -o "$API_BODY" -w '%{http_code}' "https://api.github.com/repos/${REPO}/releases" 2>/dev/null || echo "000") if [ "$API_CODE" = "403" ]; then echo "Error: GitHub API rate-limited (403) while resolving the latest pre-release." >&2 echo " Workarounds:" >&2 echo " - retry in ~1 hour, OR" >&2 - echo " - pin the tag: PILOT_RELEASE_TAG=vX.Y.Z-rcN curl ... | sh" >&2 + echo " - pin the tag: --version vX.Y.Z-rcN" >&2 echo " Refusing to silently source-build an unstamped binary." >&2 exit 1 fi @@ -221,6 +354,8 @@ elif [ "${PILOT_RC:-}" = "1" ]; then TAG=$(grep '"tag_name"' "$API_BODY" | head -1 | cut -d'"' -f4 || true) fi rm -f "$API_BODY" +elif [ "$HAVE_MANIFEST" = "1" ]; then + TAG=$(manifest_field "latest_stable" "$MANIFEST_FILE") else TAG=$(curl -fsSI "https://github.com/${REPO}/releases/latest/download/${ARCHIVE}" 2>/dev/null \ | grep -i '^location:' \ @@ -228,6 +363,32 @@ else | tr -d '\r' | head -1) fi +# Warn when the resolved tag is older than the manifest's latest_stable. +# A confirmation prompt fires only when stdin is a TTY *and* --yes was not +# passed; non-interactive pipes (curl | sh) get the warning text without a +# prompt and proceed, so existing automation does not break. +if [ -n "$TAG" ] && [ "$HAVE_MANIFEST" = "1" ] && [ "$PILOT_NO_WARN" = "0" ]; then + LATEST_STABLE=$(manifest_field "latest_stable" "$MANIFEST_FILE") + if [ -n "$LATEST_STABLE" ] && [ "$TAG" != "$LATEST_STABLE" ]; then + CMP=$(version_compare "$TAG" "$LATEST_STABLE") + if [ "$CMP" = "-1" ]; then + echo "" >&2 + echo "Warning: ${TAG} is older than the latest stable release (${LATEST_STABLE})." >&2 + echo " Older versions miss security fixes. To install the latest stable," >&2 + echo " re-run without --version, or pass --version ${LATEST_STABLE}." >&2 + if [ "$PILOT_YES" != "1" ] && [ -t 0 ]; then + printf "Continue installing %s anyway? [y/N] " "$TAG" >&2 + read -r _confirm + case "$_confirm" in + y|Y|yes|YES) ;; + *) echo "Aborted." >&2; exit 1 ;; + esac + fi + echo "" >&2 + fi + fi +fi + if [ -n "$TAG" ]; then URL="https://github.com/${REPO}/releases/download/${TAG}/${ARCHIVE}" CHECKSUMS_URL="https://github.com/${REPO}/releases/download/${TAG}/checksums.txt" diff --git a/src/pages/blog/enterprise-phase-3-rbac-policies-audit-fleet.astro b/src/pages/blog/enterprise-phase-3-rbac-policies-audit-fleet.astro index 680796c1..ef28634f 100644 --- a/src/pages/blog/enterprise-phase-3-rbac-policies-audit-fleet.astro +++ b/src/pages/blog/enterprise-phase-3-rbac-policies-audit-fleet.astro @@ -215,8 +215,11 @@ pilotctl network reject 1

All enterprise features are in v1.5.0-rc1, available now via the install script:

-
# Install release candidate
-PILOT_RC=1 curl -fsSL https://pilotprotocol.network/install.sh | sh
+
# Install the latest release candidate (edge channel)
+curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --channel edge
+
+# Or pin to a specific tag
+curl -fsSL https://pilotprotocol.network/install.sh | sh -s -- --version v1.5.0-rc1
 
 # Or wait for the stable release
 curl -fsSL https://pilotprotocol.network/install.sh | sh
diff --git a/src/pages/blog/private-agent-network-company.astro b/src/pages/blog/private-agent-network-company.astro index 36ff33cd..b5c2fdba 100644 --- a/src/pages/blog/private-agent-network-company.astro +++ b/src/pages/blog/private-agent-network-company.astro @@ -72,13 +72,12 @@ const bodyContent = `

Your company is deploying AI agents. They process custom

The rendezvous server runs the registry and beacon as a single process. Choose a machine with a static IP that all agents can reach. This can be a dedicated VM, a bare-metal server, or even a container -- the process uses minimal resources (under 20 MB RSS).

-
# Download the latest release
-wget https://github.com/TeoSlayer/pilotprotocol/releases/latest/download/pilot-rendezvous-linux-amd64
-chmod +x pilot-rendezvous-linux-amd64
+  
# Install all Pilot binaries (~/.pilot/bin/{daemon,pilotctl,rendezvous,...})
+curl -fsSL https://pilotprotocol.network/install.sh | sh
 
 # Start the rendezvous server
 # Registry listens on :9000, beacon on :9001
-./pilot-rendezvous-linux-amd64 -registry-addr :9000 -beacon-addr :9001
+~/.pilot/bin/rendezvous -registry-addr :9000 -beacon-addr :9001

For production, run the rendezvous server as a systemd service:

@@ -113,13 +112,11 @@ WantedBy=multi-user.target

On each machine that will run an agent, deploy the Pilot daemon. The daemon handles tunnel management, encryption, and communication with the rendezvous server.

-
# Download the daemon and CLI
-wget https://github.com/TeoSlayer/pilotprotocol/releases/latest/download/pilot-daemon-linux-amd64
-wget https://github.com/TeoSlayer/pilotprotocol/releases/latest/download/pilotctl-linux-amd64
-chmod +x pilot-daemon-linux-amd64 pilotctl-linux-amd64
+  
# Install the daemon, CLI, and other Pilot binaries
+curl -fsSL https://pilotprotocol.network/install.sh | sh
 
 # Start the daemon, pointing to your private rendezvous server
-./pilot-daemon-linux-amd64 -registry 10.0.1.10:9000 -beacon 10.0.1.10:9001
+~/.pilot/bin/daemon -registry 10.0.1.10:9000 -beacon 10.0.1.10:9001

The daemon generates a cryptographic identity (Ed25519 keypair) on first run and stores it at ~/.pilot/identity.json. This identity is permanent -- it persists across restarts and is used for all trust operations. Back up this file; losing it means the agent gets a new identity and must re-establish all trust relationships.

diff --git a/src/pages/for/compatibility.astro b/src/pages/for/compatibility.astro index d761298a..c83f5898 100644 --- a/src/pages/for/compatibility.astro +++ b/src/pages/for/compatibility.astro @@ -301,8 +301,8 @@ const egress = [