diff --git a/go.mod b/go.mod index 943ee32..8851def 100644 --- a/go.mod +++ b/go.mod @@ -5,3 +5,5 @@ go 1.25.0 require golang.org/x/crypto v0.51.0 require golang.org/x/sys v0.44.0 // indirect + +replace github.com/pilot-protocol/common => ../common diff --git a/integration/adapter.go b/integration/adapter.go index 74cf374..4aba790 100644 --- a/integration/adapter.go +++ b/integration/adapter.go @@ -1,6 +1,6 @@ // Package integration is the glue layer between the app-store shim and // the pilot daemon. It imports both modules and provides an Adapter that -// satisfies github.com/TeoSlayer/pilotprotocol/pkg/coreapi.Service by +// satisfies github.com/pilot-protocol/common/coreapi.Service by // forwarding to *appstore.Service. // // This package is intentionally *not* part of the main app-store module's @@ -13,7 +13,7 @@ package integration import ( "context" - "github.com/TeoSlayer/pilotprotocol/pkg/coreapi" + "github.com/pilot-protocol/common/coreapi" "github.com/pilot-protocol/app-store/plugin/appstore" ) diff --git a/integration/adapter_test.go b/integration/adapter_test.go index e3891f3..5415434 100644 --- a/integration/adapter_test.go +++ b/integration/adapter_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/TeoSlayer/pilotprotocol/pkg/coreapi" + "github.com/pilot-protocol/common/coreapi" "github.com/pilot-protocol/app-store/plugin/appstore" ) diff --git a/integration/broker_spawn_test.go b/integration/broker_spawn_test.go index 52f1eb8..5eb6fec 100644 --- a/integration/broker_spawn_test.go +++ b/integration/broker_spawn_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/TeoSlayer/pilotprotocol/pkg/coreapi" + "github.com/pilot-protocol/common/coreapi" "github.com/pilot-protocol/app-store/plugin/appstore" ) diff --git a/integration/go.mod b/integration/go.mod index dfec9d4..41ddcd0 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -1,12 +1,10 @@ module github.com/pilot-protocol/app-store/integration -go 1.25.3 +go 1.25.10 require ( - github.com/TeoSlayer/pilotprotocol v0.0.0 - github.com/pilot-protocol/app-store v0.0.0 + github.com/pilot-protocol/app-store v0.1.0 + github.com/pilot-protocol/common v0.2.0 ) -replace github.com/pilot-protocol/app-store => .. - -replace github.com/TeoSlayer/pilotprotocol => /Users/calinteodor/Development/web4 +replace github.com/pilot-protocol/common => ../../common diff --git a/integration/go.sum b/integration/go.sum new file mode 100644 index 0000000..0753f72 --- /dev/null +++ b/integration/go.sum @@ -0,0 +1,2 @@ +github.com/pilot-protocol/app-store v0.1.0 h1:mMEbr04GURXWuFd4kQBONZZK+AMrXxdVt+IujeySfo8= +github.com/pilot-protocol/app-store v0.1.0/go.mod h1:0fo1XjzzLHmRMGuTc22aOLAseQzms7qM4QXfGilmMWY= diff --git a/integration/spawn_test.go b/integration/spawn_test.go index 689beee..b5f53b6 100644 --- a/integration/spawn_test.go +++ b/integration/spawn_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "github.com/TeoSlayer/pilotprotocol/pkg/coreapi" + "github.com/pilot-protocol/common/coreapi" "github.com/pilot-protocol/app-store/pkg/ipc" "github.com/pilot-protocol/app-store/plugin/appstore" ) @@ -21,7 +21,7 @@ import ( // walletSourceDir is the path to the wallet module relative to the dev // layout. If absent (e.g. CI running on a checkout without sibling apps), // spawn-tests t.Skip rather than fail. -const walletSourceDir = "/Users/calinteodor/Development/web4-apps/wallet" +const walletSourceDir = "/Users/calinteodor/Development/pilot-protocol/wallet" // TestSupervisorSpawnsAndServesWallet builds the wallet binary, pins it // into a fake install root with a real manifest, starts the supervisor diff --git a/plugin/appstore/supervisor.go b/plugin/appstore/supervisor.go index 939948a..161f765 100644 --- a/plugin/appstore/supervisor.go +++ b/plugin/appstore/supervisor.go @@ -434,6 +434,12 @@ func (s *supervisor) rescanForNew() []*installedApp { delete(s.ready, a.Manifest.ID) s.logger.Printf("rescan: version upgrade detected: app=%s %s → %s — restarting", a.Manifest.ID, existing.Manifest.AppVersion, a.Manifest.AppVersion) + s.writeAuditLine(a, auditEvent{ + Event: "upgrade-applied", + Reason: fmt.Sprintf("rescan: %s → %s", existing.Manifest.AppVersion, a.Manifest.AppVersion), + SHA256: a.Manifest.Binary.SHA256, + BinaryAt: a.BinaryPath, + }) } s.installed[a.Manifest.ID] = a fresh = append(fresh, a) diff --git a/plugin/appstore/zz4_downgrade_test.go b/plugin/appstore/zz4_downgrade_test.go index 610dbd5..0de4a9a 100644 --- a/plugin/appstore/zz4_downgrade_test.go +++ b/plugin/appstore/zz4_downgrade_test.go @@ -1,11 +1,18 @@ package appstore import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" + + "github.com/pilot-protocol/app-store/pkg/manifest" ) // TestCompareVersions exercises the semver comparison helper. @@ -135,7 +142,9 @@ func TestRegisterSameVersionIsIdempotent(t *testing.T) { } // writeAppDirWithVersion creates a valid app dir with a manifest -// carrying the given version, and a stub binary. +// carrying the given version, and a stub binary. The manifest is +// signed with a fresh ed25519 keypair so scanInstalled's signature +// verification (PILOT-98) accepts it. func writeAppDirWithVersion(t *testing.T, root, id, version string) string { t.Helper() dir := filepath.Join(root, id) @@ -149,11 +158,33 @@ func writeAppDirWithVersion(t *testing.T, root, id, version string) string { if err := os.WriteFile(filepath.Join(binDir, "x"), []byte("#!/bin/sh\necho ok"), 0o755); err != nil { t.Fatal(err) } - raw := fmt.Sprintf( - `{"id":%q,"app_version":%q,"manifest_version":1,"binary":{"runtime":"go","path":"bin/x","sha256":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},"grants":[{"cap":"net.dial","target":"*"}],"store":{"publisher":"ed25519:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","signature":"sig"}}`, - id, version, + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("generate key: %v", err) + } + pubB64 := base64.StdEncoding.EncodeToString(pub) + + template := fmt.Sprintf( + `{"id":%q,"app_version":%q,"manifest_version":1,"binary":{"runtime":"go","path":"bin/x","sha256":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},"grants":[{"cap":"net.dial","target":"*"}],"store":{"publisher":"ed25519:%s","signature":""}}`, + id, version, pubB64, ) - if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(raw), 0o644); err != nil { + m, err := manifest.Parse([]byte(template)) + if err != nil { + t.Fatalf("parse template: %v", err) + } + grantsJSON, _ := json.Marshal(m.Grants) + grantsHash := sha256.Sum256(grantsJSON) + payload := fmt.Sprintf("%s:%s:%d:%s:%x", + m.Store.Publisher, m.ID, m.ManifestVersion, m.Binary.SHA256, grantsHash) + sig := ed25519.Sign(priv, []byte(payload)) + m.Store.Signature = base64.StdEncoding.EncodeToString(sig) + + raw, err := json.Marshal(m) + if err != nil { + t.Fatalf("marshal signed manifest: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "manifest.json"), raw, 0o644); err != nil { t.Fatal(err) } return dir @@ -232,6 +263,46 @@ func TestRescanAllowsUpgradeMidRun(t *testing.T) { } } +// TestRescanAuditLogsUpgradeApplied confirms the supervisor writes an +// "upgrade-applied" audit event when an in-place version swap lands +// (symmetric counterpart to "downgrade-refused"). +func TestRescanAuditLogsUpgradeApplied(t *testing.T) { + root := t.TempDir() + appDir := writeAppDirWithVersion(t, root, "io.test.app", "1.0.0") + + sup := newSupervisor(Config{ + InstallRoot: root, + RescanInterval: 20 * 1e6, + }, Deps{}, newQuietLogger(t)) + + entry := &installedApp{ + Dir: appDir, + Manifest: parseDummyManifest(t, "io.test.app"), + BinaryPath: filepath.Join(appDir, "bin/x"), + } + entry.Manifest.AppVersion = "1.0.0" + sup.mu.Lock() + sup.installed["io.test.app"] = entry + sup.mu.Unlock() + + writeAppDirWithVersion(t, root, "io.test.app", "2.0.0") + if fresh := sup.rescanForNew(); len(fresh) != 1 { + t.Fatalf("upgrade should be accepted, got fresh=%d", len(fresh)) + } + + data, err := os.ReadFile(filepath.Join(appDir, supervisorLogName)) + if err != nil { + t.Fatalf("read audit log: %v", err) + } + body := string(data) + if !strings.Contains(body, "upgrade-applied") { + t.Errorf("audit log missing upgrade-applied event:\n%s", body) + } + if !strings.Contains(body, "1.0.0") || !strings.Contains(body, "2.0.0") { + t.Errorf("audit log missing version transition 1.0.0 → 2.0.0:\n%s", body) + } +} + // TestRescanAuditLogsDowngradeRefusal confirms the supervisor writes an // audit event when refusing a downgrade during rescan. func TestRescanAuditLogsDowngradeRefusal(t *testing.T) {