Documentation
¶
Overview ¶
Package appstore is the coreapi.Service shim that hosts the app store inside the pilot daemon. It implements coreapi.Service (Name/Order/Start/ Stop) and wraps the supervisor that spawns + brokers every installed app.
The shim does not import the daemon's coreapi package directly, so the app-store module stays self-buildable. The Service type's method shapes are nominally identical to coreapi.Service; the daemon's main.go can register *Service against the interface without an explicit adapter as long as the type signatures stay in sync. (See INTEGRATION.md for the contract.)
Index ¶
- Variables
- type AppInfo
- type Config
- type Deps
- type Service
- func (s *Service) Apps() []AppInfo
- func (s *Service) Call(ctx context.Context, appID, method string, args, out any) error
- func (s *Service) CallFrom(ctx context.Context, callerID, appID, method string, args, out any) error
- func (s *Service) Name() string
- func (s *Service) Order() int
- func (s *Service) Start(ctx context.Context, deps Deps) error
- func (s *Service) Stop(ctx context.Context) error
- type TelemetryEmitter
- type TelemetryEvent
Constants ¶
This section is empty.
Variables ¶
var EmbeddedCatalogPubkey = make([]byte, 32)
EmbeddedCatalogPubkey is the production trust anchor for the catalog. REPLACE with the real key before release; the placeholder here is the all-zeros key so a misconfigured build refuses to verify anything (any real signature will fail against zero).
var ErrAppNotInstalled = errors.New("appstore: app not installed")
ErrAppNotInstalled is returned by Call/Apps lookups for unknown app IDs.
var ErrAppNotReady = errors.New("appstore: app not ready")
ErrAppNotReady is returned by Call when the app's socket hasn't appeared yet (still starting up, or it crashed and hasn't respawned).
var ErrGrantMissing = errors.New("appstore: caller lacks ipc.call grant for target")
ErrGrantMissing is returned by CallFrom when a cross-app caller does not hold an `ipc.call` grant whose target matches "<app>.<method>". Deny-by-default: an app may only reach methods it declared at install time, and the user reviewed, in its manifest grants.
var ErrMethodNotExposed = errors.New("appstore: method not exposed by app")
ErrMethodNotExposed is returned by Call/CallFrom when the target app's manifest does not list the requested method in its `exposes` set. The exposes list is the app's entire broker surface, so calling anything else is denied — even for trusted (daemon/pilotctl) callers.
Functions ¶
This section is empty.
Types ¶
type AppInfo ¶
type AppInfo struct {
ID string `json:"id"`
AppVersion string `json:"app_version"`
ManifestVersion int `json:"manifest_version"`
Methods []string `json:"methods"`
Protection string `json:"protection"`
Ready bool `json:"ready"`
Suspended bool `json:"suspended,omitempty"` // crash-loop budget spent
}
AppInfo is the public summary of one installed app, exposed via Service.Apps. Strips internal paths the daemon shouldn't expose.
type Config ¶
type Config struct {
// InstallRoot is where the app store keeps each installed app's
// pinned manifest, binary, data, identity, and audit log.
// Default: ~/.pilot/apps.
InstallRoot string
// CatalogPubkey is the ed25519 public key the app store uses to
// verify the signed catalog Merkle root. In production this is
// compile-time-embedded into the daemon binary (see
// EmbeddedCatalogPubkey). For dev mode, leave zero and pass a
// dev key via NewServiceWithKey.
CatalogPubkey []byte
// CataloguePublisher returns the publisher key ("ed25519:<base64>") that the
// release-signed catalogue pins for appID, and whether appID is pinned at
// all. It is the trust anchor for non-sideloaded apps: before spawning a
// catalogue app the supervisor confirms the installed manifest's publisher
// matches this pin (manifest.VerifyTrustAnchor). The daemon supplies it from
// the catalogue it has signature-verified with CatalogPubkey.
//
// When nil (or it reports an app as not pinned), non-sideloaded apps
// fail closed — they are not spawned. Sideloaded apps bypass this and are
// clamped to the safe grant subset instead.
CataloguePublisher func(appID string) (publisher string, pinned bool)
// Logger optionally redirects internal messages. When nil the
// service logs via the standard log package.
Logger *log.Logger
// RescanInterval controls how often the supervisor re-walks
// InstallRoot looking for newly-landed apps (e.g. dropped by
// `pilotctl appstore install` while the daemon is running).
// Zero defaults to 30s. Set very short (e.g. 100ms) in tests.
RescanInterval time.Duration
// AuditLogMaxBytes is the per-app supervisor.log size threshold
// at which rotation fires (active → .1). Zero defaults to
// maxAuditLogSize (10MB). Tests set this low to exercise the
// rotation path without writing megabytes.
AuditLogMaxBytes int64
// AuditLogMaxBackups is the number of rotated generations the
// supervisor keeps (supervisor.log.1 .. .N). Zero defaults to
// defaultAuditLogBackups (3). Worst-case per-app footprint is
// (AuditLogMaxBackups+1) × AuditLogMaxBytes.
AuditLogMaxBackups int
// ChildMemoryLimitBytes caps the virtual address space (RLIMIT_AS)
// of each spawned app process on Linux. Zero defaults to
// defaultChildAddressSpaceLimit (4 GiB). No-op on non-Linux
// platforms. Set generously — RLIMIT_AS bounds address space, not
// RSS, and runtime-managed languages reserve far more virtual
// memory than they use.
ChildMemoryLimitBytes uint64
}
Config carries the integration-time settings. Passed by the daemon's main.go composition root; defaults are picked up when fields are zero.
type Deps ¶
type Deps struct {
Streams any // coreapi.Streams — Dial, Listen, SendDatagram
Identity any // coreapi.Identity — NodeID, Address, PublicKey, Sign
Resolver any
Events any // coreapi.EventBus — Publish, Subscribe
Logger any
Trust any
Telemetry TelemetryEmitter // optional; no-op when nil
}
Deps is the duck-typed shape of coreapi.Deps the daemon hands plugins. Kept independent of the real coreapi package so app-store stays self-buildable; the field names + signatures must match pkg/coreapi/lifecycle.go exactly.
When the daemon's main.go registers *Service it passes the real coreapi.Deps; Go's structural typing makes this work as long as the methods used here are present on the real types.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service implements coreapi.Service for the app store.
func NewService ¶
NewService constructs a Service using cfg. Defaults are filled in.
func (*Service) Apps ¶
Apps returns a summary of every installed app the service is currently supervising. Returns nil before Start has run.
func (*Service) Call ¶
Call dispatches method+args into the named installed app via its app.sock. This is the broker entry point that lets other plugins, the daemon, or pilotctl talk to any installed app's IPC surface without dialing the socket directly.
Returns ErrAppNotInstalled when the app id is unknown, ErrAppNotReady when the spawned process hasn't bound its socket yet, or whatever error the app's own IPC handler returned.
func (*Service) CallFrom ¶ added in v1.0.1
func (s *Service) CallFrom(ctx context.Context, callerID, appID, method string, args, out any) error
CallFrom is the cross-app broker entry point. callerID identifies the installed app making the request; the supervisor authorizes the call against that app's manifest grants (it must declare an `ipc.call` grant targeting "<appID>.<method>"). Pass an empty callerID for trusted daemon/pilotctl calls — see Call.
Returns ErrAppNotInstalled, ErrMethodNotExposed, ErrGrantMissing, or ErrAppNotReady for the gate failures; otherwise the app's IPC response.
func (*Service) Order ¶
Order returns the lifecycle priority. 120 = application layer, started after foundation (10-49) and trust (50-79), before sidecars (200+). Stop runs in reverse, so by the time we stop, peer plugins still up.
func (*Service) Start ¶
Start scans InstallRoot for installed apps, verifies each binary's pinned sha256, spawns the child processes, and parks an IPC connection per app for broker forwarding. Returns the first hard failure that would leave the daemon in an unhealthy state; per-app failures only log and continue (one bad app shouldn't sink the daemon).
In tick 5 this is a skeleton: directory walk + log lines, no actual spawn. The supervisor wire-up happens in tick 6 when we have a real integration test against the daemon's coreapi.
type TelemetryEmitter ¶ added in v1.0.1
type TelemetryEmitter interface {
Emit(event TelemetryEvent)
}
TelemetryEmitter is the interface the supervisor calls to emit a usage event. The daemon wires a real implementation; the no-op default is set in newSupervisor so the supervisor never has to nil-check.
type TelemetryEvent ¶ added in v1.0.1
type TelemetryEvent struct {
AppID string `json:"app_id"`
Method string `json:"method"`
CallerID string `json:"caller_id,omitempty"`
OK bool `json:"ok"`
DurMs int64 `json:"dur_ms"`
ErrMsg string `json:"err_msg,omitempty"`
}
TelemetryEvent captures one app-usage event that the supervisor emits after every successful (or failed) IPC call through callFrom. The daemon's telemetry client converts these to signed HTTP POSTs.
Fields match what the telemetry endpoint expects for "app_usage" kind events. CallerID is empty for trusted daemon/pilotctl calls and non-empty for cross-app calls.