From 9dfe67d498396cd345e36cba5a35c8413728f547 Mon Sep 17 00:00:00 2001 From: Teo Calin Date: Thu, 8 Oct 2026 17:27:11 +0300 Subject: [PATCH] manifest: pilotctl that deletes app state on upgrade refuses this version (0.4.1) pilotctl v1.13.0-v1.13.9 delete an app's dir on `appstore upgrade`, which the updater runs hourly as `upgrade --all`. Publishing 0.4.0 would have deleted 0.3.3 wallets' EVM keys on those nodes before 0.4.0 ever ran, so there is no copy to restore (tested with real builds of v1.13.0, v1.13.5 and v1.13.9). The manifest now declares identity.verify, which app-store v1.0.3 (pilotctl v1.13.10, the first that carries state forward) added. Older validators reject it, so those pilotctl versions refuse the upgrade before touching the install and the key stays on disk; v1.13.10 and later upgrade and keep it. The wallet does not use the capability. TestManifestRefusedByPilotctlThatDeletesState keeps the grant in. Co-Authored-By: Claude Opus 5.5 --- cmd/wallet/main.go | 2 +- manifest.json | 3 ++- manifest_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/cmd/wallet/main.go b/cmd/wallet/main.go index f2314d3..2fa33ca 100644 --- a/cmd/wallet/main.go +++ b/cmd/wallet/main.go @@ -51,7 +51,7 @@ const shutdownGrace = 5 * time.Second // Version is the wallet binary's release tag. Kept in sync with the // app_version field in manifest.json — `manifest_test.go` cross-checks // they agree, so a release without bumping both fails CI. -const Version = "0.4.0" +const Version = "0.4.1" func main() { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) diff --git a/manifest.json b/manifest.json index 301099d..ba2aa6e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "id": "io.pilot.wallet", - "app_version": "0.4.0", + "app_version": "0.4.1", "manifest_version": 2, "binary": { "runtime": "go", @@ -47,6 +47,7 @@ {"cap": "fs.read", "target": "$HOME/.pilot/apps/io.pilot.wallet.previous/*"}, {"cap": "fs.read", "target": "$PILOT_APPSTORE_BACKUP_ROOT/io.pilot.wallet/*/*"}, {"cap": "audit.log","target": "*"}, + {"cap": "identity.verify", "target": "*"}, {"cap": "net.dial", "target": "*.pilot", "if": {"kind": "rate", "params": {"per": "min", "limit": 100}}}, {"cap": "net.dial", "target": "*.base.org", diff --git a/manifest_test.go b/manifest_test.go index 4b1250a..132176a 100644 --- a/manifest_test.go +++ b/manifest_test.go @@ -151,6 +151,34 @@ func TestManifestDeclaresEIP3009Grant(t *testing.T) { } } +// capsKnownToStateDeletingPilotctl is app-store v1.0.2's KnownCaps: the +// manifest validator in pilotctl v1.13.0-v1.13.9, whose `appstore upgrade` +// (run hourly by the updater as `upgrade --all`) deletes the app's dir, and +// with it identity-evm.json, before the new version ever runs. pilotctl +// v1.13.10 (app-store v1.0.3) carries the state forward and added +// identity.verify. +var capsKnownToStateDeletingPilotctl = map[string]bool{ + "fs.read": true, "fs.write": true, "fs.append": true, "fs.delete": true, + "net.dial": true, "net.call": true, "ipc.call": true, "key.sign": true, + "audit.log": true, "proc.exec": true, +} + +// TestManifestRefusedByPilotctlThatDeletesState: the manifest must declare a +// capability those pilotctl versions do not know, so their upgrade fails +// validation before it touches the install and the existing key stays on disk. +// The wallet does not use identity.verify; it is there for this. Drop it only +// once no node runs pilotctl v1.13.0-v1.13.9. +func TestManifestRefusedByPilotctlThatDeletesState(t *testing.T) { + raw, _ := os.ReadFile(manifestPath(t)) + m, _ := manifest.Parse(raw) + for _, g := range m.Grants { + if !capsKnownToStateDeletingPilotctl[g.Cap] { + return + } + } + t.Error("every grant's cap is known to pilotctl v1.13.0-v1.13.9: their upgrade would install this version and delete the wallet's key; keep the identity.verify grant") +} + // TestShippedManifestSpendCapsParse round-trips the real wallet // manifest through ParseSpendCapsFromManifest and asserts both // declared caps come through. Without this, a manifest edit that