This document is the read-before-edit plan for wiring the app store into the pilot daemon. No web4 files have been modified yet — the changes below are proposed.
Across ~/Development/web4/app-store/ and ~/Development/web4-apps/wallet/,
the following components are built and tested (210+ tests, all green):
-
Hook/extension protocol (
pkg/extend/) — open-namespace pre/post hooks on any command, runtime registration gated by manifest declarations, provenance metadata (WireMeta) that follows every hooked message so recipients without the right apps installed see a clean "install X" message rather than a decode failure. -
Payment-capability framework (
pkg/payment/) —Method,Escrow,Sealinterfaces with chacha20-poly1305 default seal. Wallets implement these slots; other apps can plug their own implementations alongside. -
Real x402 wallet (
web4-apps/wallet/):pkg/evm/— secp256k1 keys, EIP-712 typed-data hashing for EIP-3009 transferWithAuthorization, on-chain-verifiable signatures (validated against the canonical Vitalik test vector), JSON-RPC client forbalanceOf+sendRawTransaction, broadcast helper that emits transferWithAuthorization calldata for relayer / recipient submission.- Method id
io.pilot.wallet/v1produces real USDC-on-Base receipts. - Method id
io.pilot.wallet-mock/v1is the offline Ed25519 internal-ledger test wallet (lives alongside the EVM one, no collision). - Hooks
wallet.hookPreSendMessage(adds--paywalltosend-message) andwallet.hookPostRecvMessage(detects sealed envelopes on receive). - IPC surface:
wallet.balance/address/request/pay/verify/settle/topup/historypluswallet.evm.address/balance/satisfy/verifywhen EVM is enabled.
-
Plugin shim + integration adapter (
app-store/plugin/appstore/,app-store/integration/) —*Serviceimplements the daemon'scoreapi.Serviceinterface via a compile-time-checked Adapter; supervisor spawns child app processes with sha256 verification; tested end-to-end spawning the real wallet binary.
After integration, a user can:
pilotctl appstore install io.pilot.wallet # fetch + verify + pin manifest, place binary, ask user for grants
pilotctl appstore list # see installed apps + their state
pilotctl appstore uninstall io.pilot.wallet
…and the daemon, on start, automatically supervises every installed app
(spawns its binary, brokers ipc.call:<app>.<method> from peer apps,
handles graceful shutdown).
rt.Register(integration.New(appstore.NewService(appstore.Config{
InstallRoot: cfg.AppStoreInstallRoot, // default: ~/.pilot/apps
CatalogPubkey: appstore.EmbeddedCatalogPubkey,
})))The integration.Adapter wrapper is what actually satisfies coreapi.Service
— it maps the real coreapi.Deps into the shim's structurally-typed bag.
A compile-time assertion in app-store/integration/adapter.go guarantees
the wrapper still satisfies the interface; if you see a build break in web4
after a coreapi rev, the assertion will tell you exactly which field drifted.
"github.com/pilot-protocol/app-store/integration"
"github.com/pilot-protocol/app-store/plugin/appstore"case "appstore":
cmdAppStore(cmdArgs)…with a new file cmd/pilotctl/appstore.go that implements the subcommands.
Sketch in app-store/plugin/pilotctl/.
AppStoreInstallRoot string // default ~/.pilot/apps
EVMRPCEndpoint string // optional, used by io.pilot.wallet for x402
EVMChainID uint64 // default 8453 (Base mainnet); 84532 for Base Sepolia devThe EVM fields are forwarded to the wallet's per-install config (via
<InstallRoot>/io.pilot.wallet/config.json at install time) so the wallet
binary doesn't need them on its CLI in production.
That is the complete web4 footprint. Everything else lives in app-store/ and in the wallet's own module.
appstore.Service implements coreapi.Service:
| Method | Behavior |
|---|---|
Name() |
"appstore" |
Order() |
120 (application-layer; after trust at 50, before sidecars at 200) |
Start(ctx, deps) |
Scan InstallRoot/*/manifest.json, verify sha256s, spawn each app's binary under a child supervisor, register their methods in a deps.Streams listener so peer-app calls route correctly |
Stop(ctx) |
SIGTERM every child, wait 5s, SIGKILL stragglers, close all sockets |
For each installed app the supervisor maintains:
~/.pilot/apps/<app_id>/
├── manifest.json # pinned manifest the user consented to
├── binary # pinned executable, sha256-checked at every start
├── data.db # the app's own sqlite (wallet ledger / memories index / ...)
├── identity.json # the app's signing key (ed25519 seed, 0600)
├── app.sock # unix socket the daemon dials to talk to the app
└── audit.log # signed event log per the prim_audit primitive
The supervisor:
- Verifies
sha256(binary) == manifest.binary.sha256. Mismatch → abort, log, do not start. - Spawns the binary with flags:
--addr=<daemon's pilot addr>,--db=<data.db>,--socket=<app.sock>,--identity=<identity.json>. - Waits for the socket to appear (poll up to 2s).
- Dials the socket and parks a long-lived IPC conn — used to forward
ipc.call:<app>.<method>requests from other apps. - Restarts the process on crash (exponential backoff, capped at 30s) unless explicitly suspended.
appstore install <app_id> flow:
- Fetch
catalog.head.jsonfromcatalog_repo. - Verify the signature against
EmbeddedCatalogPubkey. - Fetch
manifests/<app_id>.jsonand the binary URL listed in it. - Re-compute canonical-JSON sha256 of the manifest; verify Merkle proof from leaf to signed root.
- Compute
sha256(binary)and check it matchesmanifest.binary.sha256. - Show the user the grant list and the depends list; require explicit accept.
- Pin
(manifest, binary, manifest_version, manifest_hash)to disk under<InstallRoot>/<app_id>/.
That's the literal "pilotctl embeds store pubkey → store signs manifest → manifest pins binary.sha256 → daemon re-verifies on every launch" trust chain the architecture graph asserts.
Grants live in <InstallRoot>/<app_id>/grants.db (separate sqlite from the
app's own data). The daemon's broker checks this file on every ipc.call
into the app. Manifest_version bump on the publisher's side triggers re-consent
via pilotctl before the new manifest activates.
-
Catalog pubkey source. Compile-time-embedded constant in app-store (
EmbeddedCatalogPubkey), or read from~/.pilot/config.json? Compile-time is the architecture's stated trust anchor; config-file is easier for testing. Recommend compile-time, override only with a-dev-catalog-keydebug flag. -
Wallet auto-install for dev. Should the first daemon start auto-install
io.pilot.walletfrom a local catalog under~/Development/web4-apps/so we can iterate without pushing to a real catalog? Recommend yes, gated by a-dev-appsflag. -
Crash-loop policy. A misbehaving app could spin its restart budget forever. Cap at N restarts in M seconds, then mark suspended; user resurrects with
pilotctl appstore restart <id>.
app-store/plugin/appstore/shim implementscoreapi.Serviceagainst a fakecoreapi.Deps(defined in app-store, mirrors the real one). Unit tests cover Start/Stop, sha256 verification, spawn/respawn.- A second test crate
app-store/integration/addsreplace github.com/web4/pilot => ../../web4so the shim compiles against the realcoreapi.Serviceinterface. CI runs both. - Once both pass, the web4 edit is the eight lines above — no risk surface.
app-store/INTEGRATION.md(this file)app-store/plugin/appstore/service.go— the shimapp-store/plugin/appstore/supervisor.go— child-process supervisor (sketch)app-store/plugin/appstore/install.go— install-flow stubs
cmd/daemon/main.go— one import, three lines in the register blockcmd/pilotctl/appstore.go— pilotctl subcommands (new file)cmd/pilotctl/main.go— one case in the switchpkg/config/config.go— three optional fields (install root + EVM RPC + chain id)
When agent A's wallet produces a wallet.evm.satisfy receipt for a payment.Contract,
the receipt is a self-contained EIP-3009 transferWithAuthorization signed by A.
Agent B (the recipient / payee) takes it on-chain in one of three ways:
-
Direct submission via the wallet's broadcast helper. B's daemon calls
evm.Broadcaster.CalldataForReceipt(receipt)to get(token_addr, calldata_hex), builds a transaction (B sets the from-address to B's own EVM key, sets to=token_addr, sets data=calldata_hex, sets gas/nonce/fees), signs it with B's wallet, and broadcasts viaSubmitTransferWithAuthorization. B pays the gas; the USDC contract movesvalueUSDC from A's address to B's address. This is the simplest path for a recipient that wants direct settlement. -
Meta-transaction relayer. B forwards (token_addr, calldata, signature) to a relayer service (Gelato, ERC-4337 bundler, internal infra). The relayer pays gas and either bills B off-chain or takes a cut of the transfer. B receives the USDC without ever paying gas.
-
Hold for later batched settlement. B accumulates receipts and submits them in a batch when gas is cheap or volume justifies a single tx with multiple authorizations. This is a niche path; the wallet's receipt format is the same.
All three paths use the same receipt payload — the only difference is who
calls transferWithAuthorization and when. The wallet's signature is valid
on-chain regardless of who submits it.
| Method ID | Backend | Use |
|---|---|---|
io.pilot.wallet/v1 |
EVM secp256k1 + EIP-3009 | Real x402 USDC payments; signatures verifiable on Base / Ethereum |
io.pilot.wallet-mock/v1 |
Ed25519 internal ledger | Offline tests, dev mode, contract-shape verification without touching a chain |
Both coexist on a single wallet instance. The contract's accepted_methods
field decides which one runs. For real-world send: list only the EVM id.
For testing: list the mock id. For interop between agents at different
trust levels: list both, and whichever the payer has installed satisfies.