Flow

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.

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:

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:

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>"
}

The catalogue itself is signed. After editing catalogue.json, re-sign it so the daemon and pilotctl will trust it:

pilotctl appstore sign-catalogue --key catalog-signing.key catalogue/catalogue.json

This writes a detached catalogue.json.sig (commit both). pilotctl verifies it against the embedded catalogue key before trusting any entry - see Security model.

Three integrity layers protect every install: the catalogue carries a detached ed25519 signature (a substituted app list fails), the catalogue pins each tarball sha256 (a swapped CDN byte fails), and the manifest pins the binary sha256 under an ed25519 signature - the last two re-checked at every spawn.

Catalogue vs sideload

There are two install paths, with different trust:

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.

Security model & hardening

The app store is deny-by-default at every layer. Trust flows from a signed catalogue, through a signed manifest, to a sandboxed and continuously-verified child process.

Signed catalogue (fail-closed)

The catalogue is signed with a dedicated ed25519 key whose public half is compiled into pilotctl and the daemon. pilotctl fetches both catalogue.json and a detached catalogue.json.sig and verifies the signature before trusting any entry. An unsigned, missing-signature, or tampered catalogue is refused - a compromised host or CDN cannot point installs at hostile bundle URLs without forging the signature. The signing key can be rotated at build time:

go build -ldflags   "-X .../internal/catalogtrust.publicKeyB64=<new-b64-pubkey>"   ./cmd/pilotctl ./cmd/daemon

Broker authorization (exposes + grants)

Apps never dial each other's sockets directly - every call goes through the daemon's broker, which enforces two gates before any dispatch:

Supervisor hardening

The supervisor that spawns and watches each app applies defence-in-depth:

Extension hooks

Apps may register hooks on daemon primitives (declared in the manifest), but the hook surface is bounded: per-app rate limiting caps how often the daemon will dispatch into an app's hooks, and the number of dynamic hook registrations per app is capped - so a hostile hook can't become a DoS amplifier.

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.