diff --git a/src/pages/docs/app-store.astro b/src/pages/docs/app-store.astro index 184bc92..48d86f6 100644 --- a/src/pages/docs/app-store.astro +++ b/src/pages/docs/app-store.astro @@ -13,6 +13,7 @@ const bodyContent = `
upgrade re-runs the same verified install (catalogue signature → tarball sha256 → manifest signature → publisher trust anchor), then the supervisor applies the version change and restarts the app, refusing any downgrade. outdated flags an app not only on a version bump but also as rebuilt when a same-version bundle was republished — so a fix shipped without a version change is still surfaced.
Auto-update. pilotctl's own updater — pilotctl update status|enable|disable — keeps the CLI and daemon current (automatic updates are off by default). With auto-update enabled, the updater also keeps installed app adapters current automatically, so you won't need to run upgrade by hand.
Auto-update. pilotctl's own updater — pilotctl update status|enable|disable — keeps the CLI and daemon current (automatic updates are off by default). With auto-update enabled, the updater also keeps installed app adapters current automatically, so you won't need to run upgrade by hand. To keep apps pinned instead, see Opting out of app auto-updates below.
App auto-update is on by default: while pilotctl update is enabled, the pilot-updater service periodically runs appstore upgrade --all so installed apps stay current. To keep your apps pinned at the versions you installed — for example on a locked-down or reproducible host — opt out with the PILOT_APP_UPDATE_OPT_OUT environment variable. It is false by default (app updates on).
Set it in the updater's service environment, then restart the updater so it takes effect:
+ +# 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 checks for or installs app updates — your installed apps stay exactly where they are. Pilot's own daemon and CLI binaries keep updating as normal; this switch only affects app-store apps.
Opting back in is the reverse: remove PILOT_APP_UPDATE_OPT_OUT from the updater's environment (or set it to false, the default) and restart the updater. The next cycle resumes appstore upgrade --all and brings your apps current. You can always upgrade by hand with pilotctl appstore upgrade <id> or upgrade --all, regardless of the setting.
Truthy values. PILOT_APP_UPDATE_OPT_OUT reads 1, true, yes, or on (case-insensitive) as opted-out; anything else — including unset — means opted-in. The value is read when the updater starts, so restart it after a change. (The legacy PILOT_UPDATER_NO_APP_UPGRADE variable still works as an alias.)
The App Store is for installable agent apps that run locally on a daemon as typed IPC services. Apps are discovered, installed, and called.
+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.
An app is a binary plus a signed manifest.json. The pilotctl tool fetches the bundle from a catalogue, verifies its checksum and manifest, 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 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 execution loop is: discover, install, then call.
+The agent loop is: discover, install, call.
Discovery and installation use a signed catalogue fetched by the daemon. Installation verifies the bundle, and the daemon auto-spawns it. The `call` command is used for execution.
+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
@@ -40,35 +40,32 @@ 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"}'
-`view <id>` shows an app's details, whether installed or not. It displays the description, vendor, changelog, size, source URL, license, methods, and requested permissions. Use `--all-changelog` for the full history.
-Apps ship with default configurations. An optional `config.json` file can be placed next to the manifest for overrides.
+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.
`pilotctl appstore list` and `status` show a list of method names. For richer discovery, a convention is to provide 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 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 helps an agent choose a method:
+The latency class lets an agent pick the cheapest method for its need.
Each method entry also includes a warm round-trip estimate published by the app.
+Each method entry also carries a warm round-trip estimate the app publishes.
Two surfaces put the right next command in front of an agent at the two moments it is most likely to stall: right after install, and after every call. Both are authored once by the publisher and need no runtime code.
-When `pilotctl appstore install io.pilot.<app>` finishes, its last step is a compact, example-first "Full usage demo": when to reach for the app, the one call to run first, and a handful of worked examples (with per-call cost for metered apps). The same demo renders as the Full usage demo section on the app's store page.
-After every `pilotctl appstore call`, `pilotctl` prints a short next-steps block to stderr — the recommended commands for where the agent now stands, on success and on failure. stdout stays pure JSON, so an agent can still pipe the result into `jq`; the hints never touch it. A missing param renders a corrected, runnable call; a needs-signup state names the signup command; a 402 points at the balance check. For example, a `sqlite.query` with no `database`:
+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
-Turn it off with `PILOT_NEXT_STEPS=off`. With `--json`, the hint arrives as a structured `next_steps` object instead of prose. A hint is never load-bearing: an absent, malformed, or unmatched graph prints nothing and leaves the call itself unchanged.
-The hints are driven by a small graph the app ships in the catalogue. Agents do not reinstall to get them: `pilotctl` lazily refreshes each app's cached graph from the catalogue in the background — a bounded check, at most about every 12 hours per app. So an app installed before its graph shipped starts showing hints on its next call, and a republished graph propagates within the day. No user action.
+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.
pilotctl appstore restart io.pilot.cosift # respawn (e.g. after writing a config.json)
@@ -77,19 +74,35 @@ pilotctl appstore audit io.pilot.cosift # supervisor log: spawn / exit / ve
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 manifest spend caps and usage. `audit` tails the supervisor lifecycle log for an app. `actions` is the install/uninstall log, which persists after removal. Both `audit` and `actions` accept `--tail <n>` and `--event <name>`. `audit` also accepts `--since <dur>`.
-Upgrades are keyed on the version. The supervisor respawns an app when its `app_version` changes. The version must be bumped for each new build.
+`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.
Apps are versioned in the catalogue. Two commands find and apply new versions:
+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 same verified install (catalogue signature, tarball sha256, manifest signature, publisher trust anchor), then the supervisor applies the version change and restarts the app, refusing any downgrade. `outdated` flags an app not only on a version bump but also as rebuilt when a same-version bundle was republished, so a fix shipped without a version change is still surfaced.
-Auto-update: `pilotctl`'s own updater — `pilotctl update status|enable|disable` — keeps the CLI and daemon current (automatic updates are off by default). With auto-update enabled, the updater also keeps installed app adapters current automatically, so you won't need to run `upgrade` by hand.
+`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.
+`pilotctl`'s own updater (`pilotctl update status|enable|disable`) keeps the CLI and daemon current. With auto-update enabled, it also keeps installed apps current automatically.
+ +App auto-update is on by default when `pilotctl update` is enabled. To pin app versions, set the `PILOT_APP_UPDATE_OPT_OUT` environment variable to `true`. The default is `false`.
+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.
An app is a binary that listens on a socket provided by the daemon and uses the app-store IPC protocol. The manifest declares its identity, methods, pinned binary, and required grants.
+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",
@@ -106,15 +119,15 @@ pilotctl appstore upgrade --all # bring every installed app up to
"protection": "shareable",
"store": { "publisher": "ed25519:...", "signature": "..." }
}
-The binary registers a handler for each method. In Go, this uses the `app-store/pkg/ipc` package.
+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 flags: `--socket`, `--manifest`, `--addr`, `--db`, `--identity`, `--cap-state`. An app must accept all flags to start. Method names in the code must match the manifest's `exposes` list.
+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 involves three steps: signing the manifest, creating a release bundle, and adding an entry to the catalogue via a pull request.
+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
@@ -124,7 +137,7 @@ 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
-Next, add an entry to `catalogue.json` with the tarball's sha256 and open a pull request.
+Then add one entry to `catalogue.json`, pinning the tarball's sha256, and open a PR.
{
"id": "io.pilot.cosift",
"version": "0.1.2",
@@ -132,14 +145,14 @@ gh release create cosift-v0.1.2 io.pilot.cosift-0.1.2.tar.gz
"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, re-sign it.
+The catalogue itself is signed. After editing `catalogue.json`, re-sign it.
pilotctl appstore sign-catalogue --key catalog-signing.key catalogue/catalogue.json
-This creates a detached `catalogue.json.sig` file. `pilotctl` verifies this signature before trusting any entry. Three integrity layers protect installs: the signed catalogue, the tarball sha256 pinned in the catalogue, and the binary sha256 pinned and signed in the manifest. The binary sha256 is re-checked before every spawn.
+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.
-The two runtime surfaces above (the install demo and the per-call next-steps) are authored once in your app's submission and validated at submit time — no runtime code, and both are carried verbatim into the catalogue.
-Product demo: a `product_demo` block is the example-first usage guide that renders both at install and as the store page's Full usage demo — a one-sentence `when_to_use`, a `quickstart` call, 2–6 worked `examples`, and, for metered apps, a cost table.
-Next-steps graph: a `next_steps` graph is a flat list of edges. Each edge answers one question — the agent just ran `from` and it went `on`; what now? — and names 1–3 recommended commands.
+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",
@@ -157,48 +170,55 @@ gh release create cosift-v0.1.2 io.pilot.cosift-0.1.2.tar.gz
}
]
}
-Edge fields:
The most important case is often not an error: an app with a mandatory signup soft-fails with exit 0 and a `"needs_signup": true` body, so its gateway edge matches on `on:"ok"`. The best-matching edge wins on specificity, not file order. Authoring rules: recommended, not exhaustive (a graph is not `<app>.help`); every step needs a `why`; cover the failures that end sessions (needs-signup, 402 budget, 429 quota, missing/invalid param, server-not-started) each with a corrected, runnable command; every command must actually run; and if your app has a mandatory first method, a `from:"*"` edge must route a cold agent to it. Because the graph lives in the catalogue metadata rather than the bundle, rewording or extending it is a metadata republish — no app rebuild — and existing installs pick it up automatically.
+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.
There are two installation paths with different trust models:
+There are two installation paths with different trust models.
To stage a release locally, point the `$PILOT_APPSTORE_CATALOG_URL` environment variable at a `file://` catalogue and install by ID.
-To check a bundle without installing, `pilotctl appstore verify <bundle-dir>` validates the manifest and its pinned binary sha256.
+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.
The app store is deny-by-default. Trust flows from a signed catalogue to a signed manifest to a resource-limited child process.
-The catalogue is signed with an ed25519 key whose public half is compiled into `pilotctl` and the daemon. `pilotctl` fetches `catalogue.json` and `catalogue.json.sig` and verifies the signature before trusting any entry. An invalid signature results in refusal. The signing key can be rotated at build time.
+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:
+All calls go through the daemon's broker, which enforces two gates before dispatch.
The supervisor that spawns apps applies several hardening measures:
+The supervisor that spawns and watches each app applies several defenses.
Apps may register hooks on daemon primitives, as declared in the manifest. The hook surface is bounded by per-app rate limiting and a cap on the number of dynamic hook registrations.
+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.
The cosift app is a stateless adapter to a search, answer, and research API. It exposes utility methods plus status and discovery methods.
+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 '{}'
@@ -210,12 +230,6 @@ 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 same adapter, signed-manifest, and publishing flow.
- -The cosift backend is open source. The app follows the publishing flow described above.