App Store
Installable agent apps that run locally on your daemon as typed IPC services - JSON in, JSON out, auto-spawned on install. Discover, install, call.
On this page
Overview
Where list-agents is the phonebook for live data on the overlay, the App Store is for installable capability apps. An app is a small binary plus a signed manifest.json. The daemon fetches it from the catalogue, verifies it, and supervises it: it spawns the binary, hands it a unix socket, and brokers IPC calls to it. Each app method is a typed call - JSON in, JSON out.
Apps are:
- Local - they run on your own daemon, one process per host. The heavy lifting (an index, a chain, an LLM) lives wherever the app's backend is; the installed binary is a thin, stateless adapter.
- Typed - every method maps to a JSON request/response. No browser, no REST plumbing.
- Signature-verified - the manifest pins the binary's sha256 and carries an ed25519 signature; the daemon re-checks both on every spawn.
- Grant-scoped - the manifest declares exactly what the app may do (network, file I/O); the user accepts at install time. No ambient authority.
- Auto-spawned - once installed, the daemon's supervisor keeps the app running. No manual start.
The whole loop an agent runs is: discover → install → call.
Using apps
Discovery and install go through the catalogue - a signed list the daemon fetches. Install verifies the bundle and the daemon auto-spawns it; call is then the workhorse.
# 1. Discover what's installable
pilotctl appstore catalogue
# 2. Install by id - fetch + verify sha + install; the daemon auto-spawns it
pilotctl appstore install io.pilot.cosift
# 3. Confirm it's ready (lists installed apps + the methods each exposes)
pilotctl appstore list
pilotctl appstore status io.pilot.cosift
# 4. Call a method - JSON in, JSON out on stdout
pilotctl appstore call io.pilot.cosift cosift.search '{"q":"raft consensus","k":"5"}'
No config step. A well-built app ships with sane defaults, so install then call is all an agent needs. Apps may read an optional config.json next to their manifest for overrides (e.g. a self-hosted backend).
Discovery & the help convention
pilotctl appstore list and status surface a flat list of method names - enough to know what exists, not how to call it. The convention for richer discovery is a <app>.help method: a single local call (no backend round-trip) that returns every method with its parameters, a kind (utility / status / meta), and an expected-latency class.
pilotctl appstore call io.pilot.cosift cosift.help '{}'
The latency class lets an agent pick the cheapest method for its need before spending a slow one:
- fast - under ~1s: status or cheap retrieval.
- med - ~1-5s: an LLM rerank or a single-pass synthesis.
- slow - ~5-30s: multi-step work (e.g. research that plans, retrieves, and synthesizes).
Each method entry also carries a measured, warm round-trip estimate, so an agent can budget a call end-to-end (agent → daemon → app → backend → back).
Lifecycle
pilotctl appstore restart io.pilot.cosift # respawn (e.g. after writing a config.json)
pilotctl appstore audit io.pilot.cosift # supervisor log: spawn / exit / verify-fail
pilotctl appstore install io.pilot.cosift --force # upgrade to a new version
pilotctl appstore uninstall io.pilot.cosift --yes
Upgrades key on the version. The supervisor respawns an app when its app_version changes. Bump the version for every new build, or a re-release of the same version won't roll running nodes onto the new binary.
Building an app
An app is a binary that listens on the socket the daemon hands it and speaks the app-store IPC protocol. The manifest declares its identity, the methods it exposes, the pinned binary, and the grants it needs.
{
"id": "io.pilot.cosift",
"app_version": "0.1.2",
"manifest_version": 1,
"binary": { "runtime": "go", "path": "bin/cosift-app", "sha256": "<pinned>" },
"exposes": ["cosift.search", "cosift.answer", "cosift.research",
"cosift.stats", "cosift.health", "cosift.help"],
"grants": [
{ "cap": "net.dial", "target": "cosift.pilotprotocol.network",
"if": { "kind": "rate", "params": { "per": "min", "limit": 120 } } },
{ "cap": "fs.read", "target": "$APP/config.json" },
{ "cap": "audit.log", "target": "*" }
],
"protection": "shareable",
"store": { "publisher": "ed25519:...", "signature": "..." }
}
The binary registers one handler per method and serves them over the socket. In Go, that's the app-store/pkg/ipc contract:
d := ipc.NewDispatcher()
d.Register("cosift.search", func(ctx, req) (json.RawMessage, error) { ... })
// ... one Register per exposed method ...
ipc.Serve(ctx, conn, d) // on the --socket the daemon supplies
The daemon spawns the binary with a fixed set of lifecycle flags (--socket, --manifest, --addr, --db, --identity, --cap-state). An app must accept all of them (even if it ignores most) or it will fail to start. Method names in the code must match the manifest's exposes list, or the daemon won't broker them.
Publishing an app
Three steps: sign, release, and add one catalogue entry by PR.
# 1. One-time: generate a publisher keypair (keep the private key safe)
pilotctl appstore gen-key publisher.key
# 2. Sign the manifest (after pinning binary.sha256) and package the bundle
pilotctl appstore sign --key publisher.key bundle/manifest.json
tar -czf io.pilot.cosift-0.1.2.tar.gz -C bundle .
# 3. Attach the tarball to a GitHub release
gh release create cosift-v0.1.2 io.pilot.cosift-0.1.2.tar.gz
Then add one entry to catalogue.json (pinning the tarball's sha256) and open a PR. Once merged, pilotctl appstore install <id> resolves it everywhere:
{
"id": "io.pilot.cosift",
"version": "0.1.2",
"description": "cosift search / answer / research over the public web corpus.",
"bundle_url": "https://github.com/.../releases/download/cosift-v0.1.2/io.pilot.cosift-0.1.2.tar.gz",
"bundle_sha256": "<sha256 of the tarball>"
}
Two integrity layers protect every install, both re-checked at each spawn: the catalogue pins the tarball sha256 (a swapped CDN byte fails), and the manifest pins the binary sha256 under an ed25519 signature.
Catalogue vs sideload
There are two install paths, with different trust:
- Catalogue (
install <id>) - the reviewed path. The bundle is signature-verified and installs with the grants its manifest declares, includingnet.dial. This is how any app that needs the network is distributed. - Sideload (
install <dir> --local) - for local development. The manifest is clamped to a small sandbox:fs.read/fs.writeunder$APPandaudit.logonly. Nonet.dial, no inter-app calls, no hooks. A net-using app must go through the catalogue.
To stage a release locally before publishing, point $PILOT_APPSTORE_CATALOG_URL at a file:// catalogue and install by id - the same code path as production, with your own tarball.
Worked example: io.pilot.cosift
The cosift app is a stateless adapter to a search / answer / research API over a multi-million-document web corpus. It exposes three utility methods and several status/discovery ones:
# Discover the surface + latencies
pilotctl appstore call io.pilot.cosift cosift.help '{}'
# search (fast) - ranked URLs + excerpts
pilotctl appstore call io.pilot.cosift cosift.search '{"q":"raft leader election","retriever":"hybrid","rerank":"true","k":"5"}'
# answer / chat (med) - grounded synthesis with citations
pilotctl appstore call io.pilot.cosift cosift.answer '{"q":"What is HNSW?"}'
# research (slow) - plan -> multi-retrieval -> report
pilotctl appstore call io.pilot.cosift cosift.research '{"q":"compare raft and paxos"}'
Its source is a public reference for app authors: a tiny adapter, a manifest, and the publish flow above.