diff --git a/CHANGELOG.md b/CHANGELOG.md index 23fc66f3..83b6b90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ Detailed per-release notes are on the ## [Unreleased] +### Added +- **Opt out of automatic app-store updates with `PILOT_APP_UPDATE_OPT_OUT`.** The + `pilot-updater` keeps installed apps current by periodically running + `pilotctl appstore upgrade --all`. Set `PILOT_APP_UPDATE_OPT_OUT=true` in the + updater's environment and it stops checking for and installing app updates — + installed apps stay at the version you installed. Unset it or set it to + `false` (the default) to switch app auto-updates back on. Pilot daemon/CLI + binary updates are never affected. Honors the existing + `PILOT_UPDATER_NO_APP_UPGRADE` as a back-compat alias. + +### Fixed +- **The `pilotctl skills disable` opt-out now survives updates and explicit + reconciles.** A forced reconcile — `pilotctl skills check`, `pilotctl update`, + or an installer re-run — bypassed the disabled flag and re-injected skills a + user had turned off. The opt-out is now a hard gate on every write path; only + the read-only `pilotctl skills` status still previews. (skillinject) + ## [1.12.8] - 2026-07-16 Reliable P2P data transfer across NAT, plus cold-start onboarding fixes: agents diff --git a/README.md b/README.md index 0be73b17..08818a30 100644 --- a/README.md +++ b/README.md @@ -480,6 +480,7 @@ Most daemon flags have an environment variable equivalent. Useful for containeri | `PILOT_REPLY_WHITELIST` | `-reply-whitelist` | Nodes exempt from reply rate limit | | `PILOT_REKEY_WHITELIST` | `-rekey-whitelist` | Nodes exempt from rekey rate limit | | `PILOT_FLAG_` | — | Feature flag override (`true`/`false`) | +| `PILOT_APP_UPDATE_OPT_OUT` | — | Opt out of automatic **app-store** updates. Set to `true` and the `pilot-updater` stops checking for and installing app updates — installed apps stay at their current version. Unset or `false` (the default) keeps app auto-updates on. Pilot daemon/CLI binary updates are unaffected. Read by `pilot-updater` at startup, so set it in the updater's service environment and restart the updater to change it. (Legacy alias: `PILOT_UPDATER_NO_APP_UPGRADE`.) | --- diff --git a/cmd/updater/main.go b/cmd/updater/main.go index 37fdec58..49cd7099 100644 --- a/cmd/updater/main.go +++ b/cmd/updater/main.go @@ -45,6 +45,22 @@ func envBool(name string) bool { } } +// appAutoUpgradeEnabled reports whether the updater should run the app +// auto-upgrade loop (the periodic `pilotctl appstore upgrade --all`). +// +// This is the app-update opt-out gate. By default (PILOT_APP_UPDATE_OPT_OUT unset or +// "false") the updater checks for and installs app updates for all installed +// apps. Set PILOT_APP_UPDATE_OPT_OUT=true to stop it — installed apps then stay at +// the version you installed until you opt back in (unset the variable or set it +// to false) and restart the updater. Binary updates to the pilot daemon/CLI are +// never affected by this gate. +// +// PILOT_UPDATER_NO_APP_UPGRADE is honored as a back-compat alias for the same +// opt-out. Either variable being truthy disables the loop. +func appAutoUpgradeEnabled() bool { + return !envBool("PILOT_APP_UPDATE_OPT_OUT") && !envBool("PILOT_UPDATER_NO_APP_UPGRADE") +} + func main() { installDir := flag.String("install-dir", "", "directory containing pilot binaries (required)") repo := flag.String("repo", "pilot-protocol/pilotprotocol", "GitHub owner/repo for releases") @@ -100,10 +116,13 @@ func main() { // fleet without anyone running upgrade by hand. Each upgrade re-runs the full // catalogue-signature + manifest-signature + trust-anchor gate that install // does, so this adds automation, not trust. Opt out with - // PILOT_UPDATER_NO_APP_UPGRADE for hosts that want binary-only updates. - if !envBool("PILOT_UPDATER_NO_APP_UPGRADE") { + // PILOT_APP_UPDATE_OPT_OUT=true for hosts that want binary-only updates; see + // appAutoUpgradeEnabled. The pilot daemon/CLI binaries keep updating. + if appAutoUpgradeEnabled() { go appUpgradeLoop(*installDir, *statePath, *interval) slog.Info("app auto-upgrade loop started", "interval", interval.String()) + } else { + slog.Info("app auto-upgrade opted out (PILOT_APP_UPDATE_OPT_OUT); apps stay at their installed versions, pilot binaries still update") } if *pin != "" { slog.Info("version pinned", "tag", *pin) diff --git a/cmd/updater/zz_appupgrade_test.go b/cmd/updater/zz_appupgrade_test.go index fc446adc..8dda5202 100644 --- a/cmd/updater/zz_appupgrade_test.go +++ b/cmd/updater/zz_appupgrade_test.go @@ -33,3 +33,38 @@ func TestAutoUpdateEnabledGate(t *testing.T) { t.Fatal("enabled:true must read as enabled") } } + +// TestAppAutoUpgradeOptOut pins the PILOT_APP_UPDATE_OPT_OUT gate: app auto-upgrade is +// ON by default, off when the operator opts out, and back on when they opt in +// again. PILOT_UPDATER_NO_APP_UPGRADE is honored as a back-compat alias. +func TestAppAutoUpgradeOptOut(t *testing.T) { + // Default: both unset → enabled. + t.Setenv("PILOT_APP_UPDATE_OPT_OUT", "") + t.Setenv("PILOT_UPDATER_NO_APP_UPGRADE", "") + if !appAutoUpgradeEnabled() { + t.Fatal("default (both env vars unset) must enable app auto-upgrade") + } + + // Opt out via PILOT_APP_UPDATE_OPT_OUT (each truthy spelling). + for _, v := range []string{"true", "1", "yes", "on", "TRUE"} { + t.Setenv("PILOT_APP_UPDATE_OPT_OUT", v) + if appAutoUpgradeEnabled() { + t.Errorf("PILOT_APP_UPDATE_OPT_OUT=%q must disable app auto-upgrade", v) + } + } + + // Explicit opt-in values keep it enabled — this is what "switch back on". + for _, v := range []string{"false", "0", "", "no"} { + t.Setenv("PILOT_APP_UPDATE_OPT_OUT", v) + if !appAutoUpgradeEnabled() { + t.Errorf("PILOT_APP_UPDATE_OPT_OUT=%q must keep app auto-upgrade enabled", v) + } + } + + // Back-compat alias still disables. + t.Setenv("PILOT_APP_UPDATE_OPT_OUT", "false") + t.Setenv("PILOT_UPDATER_NO_APP_UPGRADE", "1") + if appAutoUpgradeEnabled() { + t.Error("legacy PILOT_UPDATER_NO_APP_UPGRADE=1 must still disable app auto-upgrade") + } +} diff --git a/go.mod b/go.mod index c0373000..11a58e3b 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/pilot-protocol/policy v0.2.2 github.com/pilot-protocol/rendezvous v0.2.5 github.com/pilot-protocol/runtime v0.3.1 - github.com/pilot-protocol/skillinject v0.2.4-0.20260713095857-dc5109d7a33a + github.com/pilot-protocol/skillinject v0.2.4-0.20260717204101-902f745138da github.com/pilot-protocol/trustedagents v0.2.4 github.com/pilot-protocol/updater v0.2.3 github.com/pilot-protocol/webhook v0.2.0 diff --git a/go.sum b/go.sum index 85a924da..7bd08b4e 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/pilot-protocol/rendezvous v0.2.5 h1:PvApwKHU2DnvK8pA6K9RAohUomZNu6vX6 github.com/pilot-protocol/rendezvous v0.2.5/go.mod h1:nUPZaM1R1x0iEKbTp1TuYqro2wgBArwT0E4wauR8I2E= github.com/pilot-protocol/runtime v0.3.1 h1:+W9ww0dZY/FgOBtCmIOV3w5L5Z4Upt/RIsrYElXZ1zs= github.com/pilot-protocol/runtime v0.3.1/go.mod h1:GfFEIji0w7H9SSNR9Wl2q72pd2OYN3PHY9Qhcbvyrqk= -github.com/pilot-protocol/skillinject v0.2.4-0.20260713095857-dc5109d7a33a h1:MDblMoSuIXZ84um+AJt4vW3ebfBH2SG08b5o1SdGb1g= -github.com/pilot-protocol/skillinject v0.2.4-0.20260713095857-dc5109d7a33a/go.mod h1:+jEW+uCFkA6TnmZc41Ev5oczXCbOHD0NqVWG8RzjwXQ= +github.com/pilot-protocol/skillinject v0.2.4-0.20260717204101-902f745138da h1:supBO6UzykRn5Ta5pCLXjTvAPqTKOgfr2ZpZQugBNuI= +github.com/pilot-protocol/skillinject v0.2.4-0.20260717204101-902f745138da/go.mod h1:+jEW+uCFkA6TnmZc41Ev5oczXCbOHD0NqVWG8RzjwXQ= github.com/pilot-protocol/trustedagents v0.2.4 h1:NqYjU3eoxBzyzzlhTQY2vV4q0l5+4eaADAhrOD1OkQU= github.com/pilot-protocol/trustedagents v0.2.4/go.mod h1:Y3Eq/IOZqAUIbtzVBcxW4epkciaFAFAYkEyu/E7DXe4= github.com/pilot-protocol/updater v0.2.3 h1:9+Y2XvyEnfxjbguho8AIKLdEK+Gzj2I7lkad2qIu6JY=