Skip to content

Commit 1b66469

Browse files
TeoSlayerteovl
andauthored
feat(daemon): always-on app-store supervisor (#231)
Pilotctl's appstore subcommands (list, install, call, uninstall, ...) all assume the supervisor is running. Until now the daemon never loaded the app-store plugin, so those commands silently did nothing useful on any host. This wires appstore.NewService unconditionally into the daemon, pointed at <home>/.pilot/apps, 2s rescan interval. No flag, no config to enable — apps are still installed individually via pilotctl appstore install, but the supervisor is part of every daemon process from now on. The integration adapter from app-store/integration/ is a separate go module which web4 doesn't otherwise depend on. Rather than pull in the whole module for a 4-method shim, inline the adapter at cmd/daemon/appstore_adapter.go. A compile-time `var _ coreapi.Service = (*appstoreAdapter)(nil)` assertion breaks the build loudly if coreapi.Service ever drifts away from the shape the shim expects. Verified end-to-end via scripts/smoke-test-appstore.sh (lands in follow-up PR): daemon starts cleanly, supervisor logs "starting: install_root=<home>/.pilot/apps", picks up a freshly- installed wallet within the rescan window, spawns it, exposes its IPC socket. Uninstall via pilotctl propagates: daemon logs "removed from disk; supervise goroutine canceled". Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
1 parent 2488eb2 commit 1b66469

4 files changed

Lines changed: 78 additions & 11 deletions

File tree

cmd/daemon/appstore_adapter.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
//
3+
// Inline adapter from *appstore.Service to coreapi.Service.
4+
//
5+
// The app-store module (separate go.mod) ships an
6+
// integration.Adapter in its own integration/ package, but we don't
7+
// want web4 to pull in that whole separate module — the adapter is
8+
// 4 method shims. Duplicating them here keeps web4 self-buildable.
9+
//
10+
// Lockstep contract: if app-store/integration/adapter.go ever needs
11+
// a field added to its Start translation (new coreapi.Deps field
12+
// propagating into appstore.Deps), this file must follow.
13+
14+
package main
15+
16+
import (
17+
"context"
18+
19+
"github.com/pilot-protocol/app-store/plugin/appstore"
20+
"github.com/pilot-protocol/common/coreapi"
21+
)
22+
23+
type appstoreAdapter struct {
24+
svc *appstore.Service
25+
}
26+
27+
func (a *appstoreAdapter) Name() string { return a.svc.Name() }
28+
func (a *appstoreAdapter) Order() int { return a.svc.Order() }
29+
func (a *appstoreAdapter) Start(ctx context.Context, deps coreapi.Deps) error {
30+
return a.svc.Start(ctx, appstore.Deps{
31+
Streams: deps.Streams,
32+
Identity: deps.Identity,
33+
Resolver: deps.Resolver,
34+
Events: deps.Events,
35+
Logger: deps.Logger,
36+
Trust: deps.Trust,
37+
})
38+
}
39+
func (a *appstoreAdapter) Stop(ctx context.Context) error { return a.svc.Stop(ctx) }
40+
41+
// Compile-time check — the build breaks loudly if coreapi.Service ever
42+
// drifts away from this shape.
43+
var _ coreapi.Service = (*appstoreAdapter)(nil)

cmd/daemon/main.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"log/slog"
1111
"os"
1212
"os/signal"
13+
"path/filepath"
1314
"strconv"
1415
"strings"
1516
"syscall"
@@ -23,6 +24,7 @@ import (
2324
// L11 plugin imports — cmd/daemon (L12) is the only place these
2425
// are allowed. The daemon proper imports only pkg/coreapi
2526
// interfaces.
27+
"github.com/pilot-protocol/app-store/plugin/appstore"
2628
"github.com/pilot-protocol/dataexchange"
2729
"github.com/pilot-protocol/eventstream"
2830
"github.com/pilot-protocol/handshake"
@@ -259,6 +261,28 @@ func main() {
259261
}
260262
d.RegisterWebhookManager(webhookManagerAdapter{svc: webhookSvc})
261263

264+
// App store — ALWAYS on. The supervisor scans <home>/.pilot/apps
265+
// every 2s, verifies each installed bundle's manifest signature
266+
// against its embedded publisher, and spawns the app's binary under
267+
// a child supervisor with an IPC socket the daemon brokers to. No
268+
// flag gates this: apps are installed individually (via
269+
// `pilotctl appstore install`), but the supervisor is part of every
270+
// daemon process from now on.
271+
//
272+
// Default-on rationale: pilotctl appstore install/list/call all
273+
// assume the supervisor is running. Gating it behind a flag would
274+
// silently break those commands on hosts that forgot to enable it.
275+
appstoreInstallRoot := ""
276+
if home, herr := os.UserHomeDir(); herr == nil {
277+
appstoreInstallRoot = filepath.Join(home, ".pilot", "apps")
278+
}
279+
if err := rt.Register(&appstoreAdapter{svc: appstore.NewService(appstore.Config{
280+
InstallRoot: appstoreInstallRoot,
281+
RescanInterval: 2 * time.Second,
282+
})}); err != nil {
283+
log.Fatalf("register appstore: %v", err)
284+
}
285+
262286
// T4.1: subscribe plugins to the bus BEFORE Daemon.Start so the
263287
// webhook plugin captures the node.registered / agent.registered
264288
// events published from registerWithRegistry inside d.Start.

pkg/daemon/daemon.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ var (
7171
type Config struct {
7272
RegistryAddr string
7373
BeaconAddr string
74-
ListenAddr string // UDP listen address for tunnel traffic
75-
SocketPath string // Unix socket path for IPC
74+
ListenAddr string // UDP listen address for tunnel traffic
75+
SocketPath string // Unix socket path for IPC
7676
IPCWhitelist []string // process names (comm) trusted to bypass per-client dial quota (PILOT-346)
77-
Encrypt bool // enable tunnel-layer encryption (X25519 + AES-256-GCM)
78-
RegistryTLS bool // use TLS for registry connection
79-
RegistryFingerprint string // hex SHA-256 fingerprint for TLS cert pinning
77+
Encrypt bool // enable tunnel-layer encryption (X25519 + AES-256-GCM)
78+
RegistryTLS bool // use TLS for registry connection
79+
RegistryFingerprint string // hex SHA-256 fingerprint for TLS cert pinning
8080
// RegistryTrust selects which trust store verifies the registry's
8181
// certificate when RegistryTLS=true. "pinned" (default) requires
8282
// RegistryFingerprint and is what production deploys use today.
@@ -251,11 +251,11 @@ type Daemon struct {
251251
// rotation eliminates the shared-buffer hazard without changing the
252252
// existing RLock/RUnlock pattern for non-rotating Sign callers.
253253
rotateKeyMu sync.Mutex
254-
regConn *registry.Client
255-
tunnels *TunnelManager
256-
ports *PortManager
257-
ipc *IPCServer
258-
handshakes HandshakeService
254+
regConn *registry.Client
255+
tunnels *TunnelManager
256+
ports *PortManager
257+
ipc *IPCServer
258+
handshakes HandshakeService
259259

260260
// handshakeInFlight tracks peers with an outbound trust-handshake
261261
// currently in progress on this daemon. Per-peer keyed

pkg/daemon/ipc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ type ipcConn struct {
151151

152152
// peerPID is the PID of the connected process (Linux SO_PEERCRED,
153153
// 0 on Darwin/other). Used for IPC whitelist matching (PILOT-346).
154-
peerPID int32
154+
peerPID int32
155155
whitelisted bool // bypasses per-client dial quota (PILOT-346)
156156

157157
// dialCancels holds cancel funcs for in-flight DialConnection calls

0 commit comments

Comments
 (0)