[ Switch to styled version → ]


← Docs index

App Store

The App Store provides installable agent apps. Apps run locally on the daemon as typed IPC services, consuming and producing JSON. They are automatically spawned on install.

Overview

The App Store is for installable capability apps. An app consists of a binary and a signed manifest.json. pilotctl fetches the bundle, verifies its SHA and signature, and extracts it. The daemon supervises the app, spawning the binary, providing a unix socket, and brokering IPC calls. Each app method is a typed call with JSON input and output.

The agent loop is: discover, install, call.

Using apps

Discovery and installation use the catalogue, a signed list fetched by the daemon. Installation verifies the bundle, and the daemon auto-spawns it. The `call` command is used for execution.

# 1. Discover what's installable
pilotctl appstore catalogue

# 2. Inspect before committing - description, vendor, changelog, size, source, methods, permissions
pilotctl appstore view io.pilot.cosift [--all-changelog]

# 3. Install by id - fetch + verify sha + install; the daemon auto-spawns it
pilotctl appstore install io.pilot.cosift

# 4. Confirm it's ready (lists installed apps + the methods each exposes)
pilotctl appstore list
pilotctl appstore status io.pilot.cosift

# 5. Call a method - JSON in, JSON out on stdout
pilotctl appstore call io.pilot.cosift cosift.search '{"q":"raft consensus","k":"5"}'

The `view <id>` command shows an app's details, including description, vendor, changelog, size, source URL, license, methods, and requested permissions. This works for both installed and uninstalled apps.

Apps ship with default configurations. An optional `config.json` can be used for overrides.

Discovery & the help convention

`pilotctl appstore list` and `status` show method names. A convention for richer discovery is a `<app>.help` method. This is a local call that returns every method with its parameters, a `kind`, 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.

Each method entry also carries a warm round-trip estimate the app publishes.

Dynamic context on every call

Two surfaces provide context to agents: a product demo at install and next-steps after every call. These are authored by the publisher.

When `pilotctl appstore install` finishes, it displays a "Full usage demo" with examples. This demo also renders on the app's store page.

After every `pilotctl appstore call`, a next-steps block is printed to stderr, suggesting commands for success or failure states. stdout remains pure JSON.

# stdout is the pure JSON result (here an error envelope) — the hints go to stderr:
pilotctl appstore call io.pilot.sqlite sqlite.query '{"sql":"SELECT 42"}'
error: ipc: server error: backend: missing required param(s): database
next:  sqlite needs an explicit database — there is no default
  1. pilotctl appstore call io.pilot.sqlite sqlite.query '{"database":":memory:","sql":"SELECT 42 AS a"}'
     why: pass database (:memory: for a scratch db) — fixes the error above

This feature can be disabled with `PILOT_NEXT_STEPS=off`. With the `--json` flag, the hint is a structured `next_steps` object. A malformed hint does not cause a working call to fail.

Hints are driven by a graph shipped in the catalogue. `pilotctl` lazily refreshes this graph from the catalogue approximately every 12 hours per app. No re-installation is needed to receive updated hints.

Lifecycle

pilotctl appstore restart io.pilot.cosift    # respawn (e.g. after writing a config.json)
pilotctl appstore caps io.pilot.cosift       # spend caps + current rolling-window usage
pilotctl appstore audit io.pilot.cosift      # supervisor log: spawn / exit / verify-fail
pilotctl appstore actions                    # pilotctl-side install/uninstall log (survives removal)
pilotctl appstore install io.pilot.cosift --force  # upgrade to a new version
pilotctl appstore uninstall io.pilot.cosift --yes

`caps` reports spend caps and usage. `audit` tails the supervisor log for an app. `actions` is the pilotctl-side install/uninstall log. Both `audit` and `actions` accept `--tail <n>` and `--event <name>`, and `audit` accepts `--since <dur>`.

The supervisor respawns an app when its `app_version` changes.

Keeping apps current

Apps are versioned in the catalogue. Two commands find and apply new versions.

pilotctl appstore outdated                    # installed apps with a newer — or rebuilt — catalogue version
pilotctl appstore upgrade io.pilot.cosift      # re-install one app: verified, then respawned
pilotctl appstore upgrade --all               # bring every installed app up to date

`upgrade` re-runs the verified install process and restarts the app, refusing downgrades. `outdated` flags an app on a version bump or when a same-version bundle was republished.

The updater service is off by default. After you explicitly enable `pilotctl update`, it keeps the CLI and daemon current and includes installed app-adapter updates by default.

Opting out of app auto-updates

On an updater-enabled host, set `PILOT_APP_UPDATE_OPT_OUT=true` to keep app adapters pinned. This does not enable the updater service itself.

Set this variable in the updater's service environment and restart the updater.

# Linux (systemd): set the env var on the pilot-updater service
sudo systemctl edit pilot-updater
#   [Service]
#   Environment=PILOT_APP_UPDATE_OPT_OUT=true
sudo systemctl restart pilot-updater

# macOS (launchd): add PILOT_APP_UPDATE_OPT_OUT to the plist's EnvironmentVariables, then reload
launchctl unload ~/Library/LaunchAgents/network.pilotprotocol.pilot-updater.plist
launchctl load   ~/Library/LaunchAgents/network.pilotprotocol.pilot-updater.plist

With `PILOT_APP_UPDATE_OPT_OUT=true`, the updater no longer updates apps. Pilot's daemon and CLI binaries continue to update. Manual upgrades are still possible.

To re-enable, remove `PILOT_APP_UPDATE_OPT_OUT` from the updater's environment or set it to `false`, then restart the updater.

`PILOT_APP_UPDATE_OPT_OUT` reads `1`, `true`, `yes`, or `on` (case-insensitive) as opted-out. The value is read when the updater starts. The legacy `PILOT_UPDATER_NO_APP_UPGRADE` variable still works as an alias.

Building an app

An app is a binary that communicates over a socket using the app-store IPC protocol. The manifest declares its identity, methods, binary hash, and required grants.

{
  "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 a handler for each method. In Go, this uses 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. Method names in the code must match the manifest's `exposes` list.

Publishing an app

Publishing involves three steps: signing, releasing, and adding a catalogue entry via a pull request.

# 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.

{
  "id": "io.pilot.cosift",
  "version": "0.1.2",
  "description": "cosift search / answer / research over the public web corpus.",
  "bundle_url": "https://github.com/<org>/<repo>/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.

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

This writes a detached `catalogue.json.sig`. `pilotctl` verifies it against an embedded key. Three integrity layers protect every install: the catalogue signature, the catalogue's tarball sha256 pin, and the manifest's binary sha256 pin. The binary sha256 is re-checked before every spawn.

Authoring dynamic context

The product demo and next-steps graph are authored once in the app's submission and are carried into the catalogue.

A `product_demo` block is a usage guide that renders at install and on the store page. It includes a `when_to_use` sentence, a `quickstart` call, 2–6 worked `examples`, and a cost table for metered apps.

A `next_steps` graph is a flat list of edges. Each edge defines recommended commands based on the outcome of a previous command.

{
  "schema": 1,
  "app": "io.pilot.example",
  "edges": [
    {
      "from": "*",
      "on": "err",
      "code": 402,
      "why": "budget exhausted",
      "then": [
        { "cmd": "pilotctl appstore call io.pilot.example example.balance '{}'",
          "why": "check what's left before spending again",
          "kind": "recovery" }
      ]
    }
  ]
}

The best-matching edge wins on specificity. A recommendation is a pure function of `(method, outcome, payload)`.

Because the graph lives in the catalogue metadata, updates do not require an app rebuild and are picked up automatically by existing installs.

Catalogue vs sideload

There are two installation paths with different trust models.

To stage a release locally, `PILOT_APPSTORE_CATALOG_URL` can be pointed at a `file://` catalogue.

`pilotctl appstore verify <bundle-dir>` validates a bundle without installing it.

Security model & hardening

The app store is deny-by-default. Trust flows from a signed catalogue, through a signed manifest, to a resource-limited child process.

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 `catalogue.json.sig` and verifies the signature before trusting any entry. An unsigned or tampered catalogue is refused. The signing key can be rotated at build time.

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

All calls go through the daemon's broker, which enforces two gates before dispatch.

The supervisor that spawns and watches each app applies several defenses.

Apps may register hooks on daemon primitives. The hook surface is bounded by per-app rate limiting and a cap on the number of dynamic hook registrations.

Worked example: io.pilot.cosift

The cosift app is a stateless adapter to a search, answer, and research API over a web corpus. It exposes several utility methods plus status and discovery methods.

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

The cosift backend is open source. The app follows the publishing flow described above.