diff --git a/catalogue/README.md b/catalogue/README.md index d9e22c4f..75481398 100644 --- a/catalogue/README.md +++ b/catalogue/README.md @@ -47,7 +47,10 @@ ignores the v2 fields. **Always set `"version": 2` when using any v2 field.** "license": "", "metadata_url": "https:///apps//metadata.json", - "metadata_sha256": "" + "metadata_sha256": "", + + "renamed_to": ", optional", + "hidden": false } ] } @@ -58,6 +61,26 @@ fields stay required. `pilotctl` decodes the index directly into `catalogueEntry` in `cmd/pilotctl/appstore_catalogue.go` — any field added here must also land there. +### Renaming an app (`renamed_to` + `hidden`) + +Both are optional v2 fields (**keep `"version": 2`**). To rename an app id +`old → new` without breaking existing installs, do NOT delete the old entry — +the daemon supervisor pins each installed app's publisher key from its +catalogue entry and **fail-closes (stops) an installed app whose id has no +pin**. Instead, replace the old entry's body with a **tombstone**: keep `id` +and `publisher` (so existing installs keep their pin and keep running), set +`"renamed_to": ""`, and set `"hidden": true`. Drop `bundle_url` / +`bundles` / `metadata_url` (the tombstone is not installable) and delete the old +`apps//` detail dir. Then add the full new entry under the new id and +re-sign. + +A bundles-aware `pilotctl` then, for the old id: omits it from `catalogue`, +and on `install`/`view`/`call` prints a deprecation warning and routes to +`renamed_to`. `hidden` alone (without `renamed_to`) just omits an entry from the +listing while keeping it resolvable. Older clients ignore both fields (they see +a normal, pin-only entry). One hop only — a `renamed_to` that points at another +tombstone is a bug and is not chased. + `bundles` is the per-platform map keyed by `"os/arch"`. It is an **optional v2 field — keep `"version": 2`, do NOT bump to 3.** `loadCatalogue` fail-closes on any version other than 1 or 2, so a version-3 catalogue is rejected wholesale by diff --git a/catalogue/apps/io.pilot.didit/metadata.json b/catalogue/apps/io.pilot.didit/metadata.json new file mode 100644 index 00000000..20ad0045 --- /dev/null +++ b/catalogue/apps/io.pilot.didit/metadata.json @@ -0,0 +1,230 @@ +{ + "schema_version": 1, + "id": "io.pilot.didit", + "display_name": "Didit", + "tagline": "One API for identity and fraud — KYC, liveness, face match, AML, and more, with a no-broker key you mint in one call.", + "description_md": "**Didit is one API for identity and fraud** — KYC/ID verification, liveness, face match, AML screening, proof of address, database validation, and email/phone OTP, wrapped as a single Pilot app. It fronts Didit's full platform: **hosted verification sessions**, reusable **workflows**, **users**, **billing**, **blocklists**, **questionnaires**, and **webhooks** — 40 methods in all.\n\n## Your own key, minted in one call — no email, no code\n\nThe hard part of using an identity provider is usually onboarding: signing up, confirming an email code, and wiring the key. This app removes all of it. **`didit.signup` takes no arguments** and returns a working key:\n\n- It signs a keyless request (your Pilot identity) to Pilot's Didit broker. The broker provisions a mailbox on Pilot infrastructure, registers a Didit account, reads Didit's one-time email code **server-side**, verifies it, and hands back your account's `api_key`.\n- The adapter caches `{email, api_key}` to `$APP/secrets.json`. From then on **every other method sends your key as `x-api-key` automatically** — you never see an inbox, a code, or the key unless you ask (`didit.account`).\n- **Idempotent:** the broker mints at most one Didit account per Pilot identity, so a repeat call — or a fresh install on another machine — returns the *same* account. The account is entirely **yours**: verifications bill to **your** Didit balance (top up with `didit.billing_topup`), and Pilot adds no markup. Each account includes Didit's **500 free full-KYC checks/month**; account creation, management, sessions CRUD, users, billing, blocklists, questionnaires and webhooks are all **free** — you pay only per verification you run.\n\n## The fast path\n\n1. `didit.signup {}` → your key is cached (one call, ~5s, no email).\n2. `didit.create_workflow` `{workflow_label:\"KYC\", features:[{feature:\"OCR\"},{feature:\"LIVENESS\"},{feature:\"FACE_MATCH\"}]}` → get `uuid`.\n3. `didit.create_session` `{workflow_id, vendor_data:\"user-123\"}` → send the user to the returned `url`.\n4. `didit.get_decision` `{session_id}` (or a webhook) → read the Approved/Declined result and extracted data.\n\n`didit.account` returns your provisioned email + key any time.\n\n## What each area does\n\n- **Sessions** — hosted flows where the user completes verification at a Didit URL, so you never handle document images: `create_session`, `get_decision`, `list_sessions`, `update_session_status` (approve/decline/resubmit), `delete_session`, `batch_delete_sessions`, `share_session` / `import_session` (B2B KYC reuse), `list_reviews`, `create_review`.\n- **Workflows** — templates built from an ordered `features` array (`OCR`, `LIVENESS`, `FACE_MATCH`, `AML`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, `EMAIL_VERIFICATION`, `DATABASE_VALIDATION`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `NFC`, `QUESTIONNAIRE`, `KYB_*`), each with an optional per-feature `config`: `create_workflow`, `list_workflows`, `get_workflow`, `update_workflow`, `delete_workflow`.\n- **Standalone checks (JSON, no session)** — `aml` (sanctions/PEP/adverse-media, $0.20), `database_validation` (gov sources, from $0.05).\n- **Contact** — `email_send`/`email_check` ($0.03) and `phone_send`/`phone_check` (from $0.03) OTP verification.\n- **Billing** — `billing_balance`, `billing_topup` (Stripe checkout URL).\n- **Governance** — `blocklist_*` (auto-flag repeat faces/docs/phones/emails), `questionnaire_*` (custom forms), `users_*` (people grouped by your `vendor_data`), `get_webhook`/`update_webhook` (set + rotate the HMAC secret programmatically).\n\n## Pricing\n\nPay-per-check on **your** Didit balance — no Pilot markup. See the full rate card in `didit.help`. Highlights: full KYC bundle **$0.33/check** (first **500/month free**), ID verification $0.15, passive liveness $0.10, face match $0.05, **face search free**, AML $0.20, PoA $0.20, email/phone from $0.03. Image-upload APIs (direct ID scan, liveness, face match, face search, age estimation, PoA) run through the **hosted session** flow rather than as direct methods.\n\n## Notes\n\n- The adapter dials exactly two hosts: Pilot's broker (`broker.pilotprotocol.network`) for the one-call `didit.signup`, and Didit (`verification.didit.me`) for every operational call with your cached key. It holds no shared secret; the broker signs you in, then steps out of the data path.\n- Plain request/response REST — no websockets, no async jobs. Rate limits: ~600 session-creates/min, 300/min per other method; the account OTP register is 5/IP/hour.\n- Errors surface verbatim: `401` (run `didit.signup` first), `403` (top up credits), `429` (back off).\n", + "vendor": { + "name": "Didit", + "url": "https://didit.me", + "contact": "hello@didit.me", + "publisher_pubkey": "ed25519:ii6QAc8qZB4NvbOXqnLfnv+OhPqepuK0BISAv9jM5x0=" + }, + "homepage": "https://didit.me", + "source_url": "https://github.com/didit-protocol/skills", + "license": "Proprietary", + "categories": [ + "identity", + "verification", + "compliance", + "security", + "kyc" + ], + "keywords": [ + "kyc", + "identity", + "verification", + "aml", + "liveness", + "face-match", + "biometrics", + "proof-of-address", + "sanctions", + "pep", + "onboarding", + "fraud", + "kyb", + "otp", + "didit" + ], + "size": { + "bundle_bytes": 5130067, + "installed_bytes": 9324763 + }, + "compat": { + "min_pilot_version": "1.0.0", + "runtimes": [ + "go" + ] + }, + "methods": [ + { + "name": "didit.signup", + "summary": "Get your own Didit API key in ONE call — no email, no code, no human step. This signs a keyless request to Pilot's Didit broker, which provisions a mailbox on Pilot infrastructure, registers a Didit account, reads the emailed one-time code server-side, verifies it, and returns your account's api_key. The adapter caches {email, api_key} to $APP/secrets.json, and from then on EVERY other didit.* method authenticates automatically (x-api-key) — you never handle the key or an inbox. Idempotent: the broker mints at most one account per Pilot identity, so a repeat call (or a fresh install) returns the SAME account. Run this ONCE before any other method. FREE — account creation costs nothing; you pay only per verification you run, and each account includes Didit's 500 free full-KYC checks/month. The account (email + key) is retrievable any time via didit.account. Takes no arguments." + }, + { + "name": "didit.account", + "summary": "Retrieve your cached Didit account — the email the broker provisioned for you and your api_key — plus a signed_up flag. Local, instant, FREE (reads $APP/secrets.json; no backend call). Use it to confirm you're signed up or to read your key. If signed_up is false, call didit.signup first." + }, + { + "name": "didit.billing_balance", + "summary": "Check your remaining Didit credit balance (and auto-refill settings). FREE. Returns {balance, auto_refill_enabled, auto_refill_amount, auto_refill_threshold}. Verifications draw down this balance; check it before a batch of checks." + }, + { + "name": "didit.billing_topup", + "summary": "Add credit to your Didit balance. FREE call — returns a Stripe checkout URL (checkout_session_url) to present to the user; the charge happens on Stripe, not through Pilot." + }, + { + "name": "didit.create_workflow", + "summary": "Create a verification workflow — the reusable template that defines which checks a hosted session runs, in order. FREE to create; you're billed per feature only when a session actually runs it. Returns {uuid} — pass it as workflow_id to didit.create_session. The v3 API takes a `features` ARRAY (in the order users complete them); each item is {feature, config?} where feature is one of OCR, NFC, LIVENESS, FACE_MATCH, PROOF_OF_ADDRESS, QUESTIONNAIRE, DOCUMENT_AI, PHONE_VERIFICATION, EMAIL_VERIFICATION, DATABASE_VALIDATION, AML, IP_ANALYSIS, AGE_ESTIMATION, KYB_REGISTRY, KYB_DOCUMENTS, KYB_KEY_PEOPLE. Example: [{\"feature\":\"OCR\"},{\"feature\":\"LIVENESS\",\"config\":{\"face_liveness_method\":\"PASSIVE\"}},{\"feature\":\"FACE_MATCH\"}]. The API uses a strict field whitelist — any undeclared key (e.g. workflow_type) is a 400. Max 50 workflows per account." + }, + { + "name": "didit.list_workflows", + "summary": "List your verification workflows with their features and total_price. FREE." + }, + { + "name": "didit.get_workflow", + "summary": "Get one workflow by id. FREE." + }, + { + "name": "didit.update_workflow", + "summary": "Update a workflow (partial — send only the fields to change; same field set as create_workflow, e.g. a replacement `features` array, workflow_label, status, is_default). FREE." + }, + { + "name": "didit.delete_workflow", + "summary": "Delete a workflow. FREE. Existing sessions are unaffected. Returns 204." + }, + { + "name": "didit.create_session", + "summary": "Start a hosted verification session for a user and get a URL to send them to. This is Didit's recommended path for ID/liveness/face-match/AML/PoA/etc. — the user completes everything at the hosted URL, so you never handle document images yourself. COST is the sum of the features the workflow enables (e.g. a full KYC bundle ≈ $0.33/check; 500 full-KYC checks/month are free), charged to your Didit balance when the session runs. Returns {session_id, session_token, url, status}. Poll didit.get_decision or set a webhook for the result. Nested objects (contact_details, expected_details) are passed as JSON objects." + }, + { + "name": "didit.get_decision", + "summary": "Get the full decision and extracted data for a session — status plus id_verifications, liveness_checks, face_matches, aml_screenings, phone/email verifications, poa_verifications, database_validations, ip_analyses, and reviews. FREE (reading results). Image URLs in the response expire after 60 minutes. Statuses: Not Started | In Progress | In Review | Approved | Declined | Abandoned | Expired | Resubmitted." + }, + { + "name": "didit.list_sessions", + "summary": "List/filter your sessions (paginated). FREE." + }, + { + "name": "didit.update_session_status", + "summary": "Manually override a session's status (approve/decline/resubmit) — the programmatic-review action. FREE. For Resubmitted, pass nodes_to_resubmit; the session must be Declined, In Review, or Abandoned." + }, + { + "name": "didit.delete_session", + "summary": "Permanently delete a session and all its data. FREE. Returns 204." + }, + { + "name": "didit.batch_delete_sessions", + "summary": "Delete many sessions at once by number (or all). FREE." + }, + { + "name": "didit.share_session", + "summary": "Generate a share_token so a partner can import a finished session (B2B KYC reuse). FREE. Works only for finished sessions." + }, + { + "name": "didit.import_session", + "summary": "Import a session shared by a partner via its share_token. FREE." + }, + { + "name": "didit.list_reviews", + "summary": "List the manual-review activity for a session (status changes, notes). FREE." + }, + { + "name": "didit.create_review", + "summary": "Add a manual review decision to a session (Approved/Declined/In Review). FREE." + }, + { + "name": "didit.aml", + "summary": "Screen a person or company against sanctions, PEP, and adverse-media watchlists (standalone, no session). COST $0.20/check on your Didit balance. Returns matches with scores and categories." + }, + { + "name": "didit.database_validation", + "summary": "Cross-check identity fields against government / authoritative databases (standalone). COST from $0.05/check (1x1, single source) to $0.30 (2x2, two-source cross-validation); varies by country/source. Covers 1,000+ sources across 18+ countries." + }, + { + "name": "didit.email_send", + "summary": "Send a one-time verification code to an email address. Part of email verification ($0.03 per completed verification, charged to your Didit balance)." + }, + { + "name": "didit.email_check", + "summary": "Verify the email OTP the user received. Completes an email verification ($0.03)." + }, + { + "name": "didit.phone_send", + "summary": "Send a one-time code by SMS / WhatsApp / Telegram. Part of phone verification (from $0.03 per completed verification, varies by channel)." + }, + { + "name": "didit.phone_check", + "summary": "Verify the phone OTP the user received. Completes a phone verification (from $0.03)." + }, + { + "name": "didit.blocklist_add", + "summary": "Add a session's face/document/phone/email to your blocklist so future matches auto-flag (FACE_IN_BLOCKLIST, etc.). FREE." + }, + { + "name": "didit.blocklist_remove", + "summary": "Remove items from your blocklist. FREE." + }, + { + "name": "didit.blocklist_list", + "summary": "List your blocklisted items. FREE." + }, + { + "name": "didit.create_questionnaire", + "summary": "Create a custom form (7 element types) to attach to a questionnaire_verification workflow. FREE." + }, + { + "name": "didit.list_questionnaires", + "summary": "List your questionnaires. FREE." + }, + { + "name": "didit.get_questionnaire", + "summary": "Get one questionnaire. FREE." + }, + { + "name": "didit.update_questionnaire", + "summary": "Update a questionnaire (partial). FREE." + }, + { + "name": "didit.delete_questionnaire", + "summary": "Delete a questionnaire. FREE. Returns 204." + }, + { + "name": "didit.list_users", + "summary": "List verified individuals (grouped by vendor_data) with status and session counts. FREE." + }, + { + "name": "didit.get_user", + "summary": "Get one user by your vendor_data. FREE." + }, + { + "name": "didit.update_user", + "summary": "Update a user's display name / manual status / metadata. FREE." + }, + { + "name": "didit.batch_delete_users", + "summary": "Delete many users by vendor_data (or all). FREE." + }, + { + "name": "didit.get_webhook", + "summary": "Get your webhook configuration (url, version, HMAC secret_shared_key, capture_method). FREE." + }, + { + "name": "didit.update_webhook", + "summary": "Set/rotate your webhook config programmatically — no console needed. FREE." + }, + { + "name": "didit.help", + "summary": "Discovery: every method with params, kind, and latency class." + } + ], + "changelog": [ + { + "version": "1.0.0", + "notes": [ + "Released v1.0.0" + ] + } + ], + "links": [ + { + "label": "Source", + "url": "https://github.com/didit-protocol/skills" + }, + { + "label": "Website", + "url": "https://didit.me" + } + ] +} diff --git a/catalogue/apps/io.pilot.mysql/metadata.json b/catalogue/apps/io.pilot.mysql/metadata.json new file mode 100644 index 00000000..fd9dffd7 --- /dev/null +++ b/catalogue/apps/io.pilot.mysql/metadata.json @@ -0,0 +1,119 @@ +{ + "schema_version": 1, + "id": "io.pilot.mysql", + "display_name": "MySQL", + "tagline": "Run a real MySQL server from an agent — full client/server SQL, no cloud account, no provisioning", + "description_md": "# MySQL — the world's most popular open-source SQL database, native CLI for agents\n\nThis app installs the official **MySQL 9.7.1** server (`mysqld`) **and** client tools (`mysql`,\n`mysqladmin`, `mysqldump`, `mysqlshow`) on the host and fronts them as typed methods. The bundle is the\nupstream MySQL suite (sha-pinned per OS/arch, fetched from the Pilot artifact registry at install) plus a\ntiny `mysqlctl` dispatcher that makes the server relocatable — it finds its plugins and error messages\nwherever the bundle is staged.\n\nMySQL is a **real client/server RDBMS** — the database behind a huge share of the web. Unlike a hosted\nMySQL, there is **no cloud account, no credentials to provision, and no network egress**: the agent runs\n`mysql.initialize` once to lay down a data directory, `mysql.start` to bring up a server on\n`127.0.0.1:\u003cport\u003e`, and then creates databases and runs SQL — all locally, in its own sandbox. It is the\ntransactional, server-grade complement to the embedded `io.pilot.sqlite` and the analytics-focused\n`io.pilot.duckdb`, and a sibling to `io.pilot.postgres`.\n\n## Why an agent wants this\n\n- **A full local MySQL, zero provisioning.** No RDS, no `docker run`, no credentials. `initialize` →\n `start` → `createdb` → `query`, all on `127.0.0.1`.\n- **Real server semantics.** Transactions, InnoDB, foreign keys, triggers, stored routines, JSON columns,\n window functions, full-text indexes — the actual MySQL engine, not an emulation.\n- **The MySQL dialect + wire protocol.** Test and run exactly what your production MySQL will see: same SQL,\n same `mysqldump` format, same client tools.\n- **Durable + portable.** The data directory is a real MySQL instance the agent can stop, restart, back up\n with `mysql.dump`, or hand off.\n- **Agent-friendly output.** `mysql.query` returns an aligned table; `mysql.query_tsv` returns tab-separated\n values for clean parsing.\n\n## Methods\n\n- `mysql.initialize` — create a new data directory (system tables + insecure `root`). Run once.\n- `mysql.start` / `mysql.stop` — bring a server on `127.0.0.1:\u003cport\u003e` up (detached) / down (clean shutdown).\n- `mysql.ping` — is the server accepting connections?\n- `mysql.createdb` — create a database.\n- `mysql.query` — run SQL, get an aligned table. `mysql.query_tsv` — same, as TSV.\n- `mysql.databases` / `mysql.tables` — list databases / tables.\n- `mysql.dump` — mysqldump a database to portable SQL.\n- `mysql.exec` — run any bundled tool with a verbatim argv (+ optional stdin) for anything the curated\n methods don't cover (`--vertical`, `mysqlshow`, `SOURCE script.sql`, a different output mode, …).\n- `mysql.mysql_help` — the full `mysql` client help. `mysql.version` — the delivered version.\n `mysql.help` — the self-describing method list.\n\n## How to use it (typical flow)\n\n1. **Initialize once:** `mysql.initialize` `{ \"datadir\": \"/work/mysql-data\" }`.\n2. **Start the server:** `mysql.start` `{ \"datadir\": \"/work/mysql-data\", \"port\": \"13306\" }` (then `mysql.ping`).\n3. **Create a database:** `mysql.createdb` `{ \"port\": \"13306\", \"dbname\": \"app\" }`.\n4. **Run SQL:** `mysql.query` `{ \"port\": \"13306\", \"database\": \"app\", \"sql\": \"CREATE TABLE t(id INT PRIMARY KEY, v TEXT); INSERT INTO t VALUES (1,'hi'); SELECT * FROM t;\" }`.\n5. **Back up / stop:** `mysql.dump` `{ \"port\": \"13306\", \"database\": \"app\" }`, then `mysql.stop` `{ \"port\": \"13306\" }`.\n\n## Configuration \u0026 connection\n\n- **Connection** is always local: `127.0.0.1:\u003cport\u003e`, user `root`. After `initialize` the root password is\n empty; set one with `mysql.exec` (`ALTER USER 'root'@'localhost' IDENTIFIED BY '…'`) and pass it to later\n calls via the **`MYSQL_PWD`** environment variable (opted into `env_passthrough`).\n- **Data directory** — each `datadir` is an independent MySQL instance; run several on different ports.\n- **X protocol** is disabled (`--mysqlx=OFF`) for a lean local footprint; the classic protocol is on.\n- **Anything else** — `mysql.exec` gives you the raw tools: `--vertical`/`--html`/`--xml` output, `mysqlshow`,\n `SOURCE` a script over stdin, or `mysqld`/`mysqladmin` flags the curated methods don't expose.\n\n## Good to know\n\n- On a non-zero exit (SQL error) the reply is `{stdout, stderr, exit}` so the caller sees everything the\n tool produced.\n- Runs on **macOS and Linux** (arm64 + amd64); binaries come from the Pilot artifact registry, sha-pinned on\n install. The server must not run as OS root — the Pilot daemon runs unprivileged, which is exactly right.\n- MySQL Community Server is **GPL-2.0** licensed. `mysql.help` lists every method with its latency class.\n\n## MySQL client help (`mysql.mysql_help`)\n```\n/tmp/mysql-reloc/mysql-9.7.1-darwin-arm64/bin/mysql Ver 9.7.1 for macos15.7 on arm64 (conda-forge)\nCopyright (c) 2000, 2026, Oracle and/or its affiliates.\n\nOracle is a registered trademark of Oracle Corporation and/or its\naffiliates. Other names may be trademarks of their respective\nowners.\n\nUsage: /tmp/mysql-reloc/mysql-9.7.1-darwin-arm64/bin/mysql [OPTIONS] [database]\n -?, --help Display this help and exit.\n -I, --help Synonym for -?\n --auto-rehash Enable automatic rehashing. One doesn't need to use\n 'rehash' to get table and field completion, but startup\n and reconnecting may take a longer time. Disable with\n --disable-auto-rehash.\n (Defaults to on; use --skip-auto-rehash to disable.)\n -A, --no-auto-rehash \n No automatic rehashing. One has to use 'rehash' to get\n table and field completion. This gives a quicker start of\n mysql and disables rehashing on reconnect.\n --auto-vertical-output \n Automatically switch to vertical output mode if the\n result is wider than the terminal width.\n -B, --batch Don't use history file. Disable interactive behavior.\n (Enables --silent.)\n --bind-address=name IP address to bind to.\n --binary-as-hex Print binary data as hex. Enabled by default for\n interactive terminals.\n --character-sets-dir=name \n Directory for character set files.\n --column-type-info Display column type information.\n --commands Enable or disable processing of local mysql commands.\n -c, --comments Preserve comments. Send comments to the server. The\n default is --comments (keep comments), disable with\n --skip-comments.\n (Defaults to on; use --skip-comments to disable.)\n -C, --compress Use compression in server/client protocol.\n -#, --debug[=#] This is a non-debug version. Catch this and exit.\n --debug-check This is a non-debug version. Catch this and exit.\n -T, --debug-info This is a non-debug version. Catch this and exit.\n -D, --database=name Database to use.\n --default-character-set=name \n Set the default character set.\n --delimiter=name Delimiter to be used.\n --enable-cleartext-plugin \n Enable/disable the clear text authentication plugin.\n -e, --execute=name Execute command and quit. (Disables --force and history\n file.)\n -E, --vertical Print the output of a query (rows) vertically.\n -f, --force Continue even if we get an SQL error.\n --histignore=name A colon-separated list of patterns to keep statements\n from getting logged into syslog and mysql history.\n -G, --named-commands \n Enable named commands. Named commands mean this program's\n internal commands; see mysql\u003e help . When enabled, the\n named commands can be used from any line of the query,\n otherwise only from the first line, before an enter.\n Disable with --disable-named-commands. This option is\n disabled by default.\n -i, --ignore-spaces Ignore space after function names.\n --init-command=name Single SQL Command to execute when connecting to MySQL\n```\n", + "vendor": { + "name": "Pilot Protocol", + "url": "https://pilotprotocol.network", + "contact": "apps@pilotprotocol.network", + "publisher_pubkey": "ed25519:cTZr4unmAHK/r8fS+TVQ4QiCIORimYe+y+bIdiUUfAY=" + }, + "homepage": "https://www.mysql.com", + "source_url": "https://github.com/mysql/mysql-server", + "license": "GPL-2.0", + "categories": [ + "database", + "data", + "sql" + ], + "keywords": [ + "mysql", + "sql", + "database", + "rdbms", + "oltp", + "innodb", + "server", + "query", + "transactional", + "relational" + ], + "size": { + "bundle_bytes": 5359647, + "installed_bytes": 9634438 + }, + "compat": { + "min_pilot_version": "1.0.0", + "runtimes": [ + "go" + ] + }, + "methods": [ + { + "name": "mysql.initialize", + "summary": "Initialize a new MySQL data directory (system tables + an insecure `root@localhost` with an empty password) — the one-time setup before the first start. This is `mysqld --initialize-insecure --datadir=\u003cdatadir\u003e`. The bundle's basedir/plugin-dir/error-messages are wired in automatically." + }, + { + "name": "mysql.start", + "summary": "Start a local MySQL server from a data directory, listening on 127.0.0.1:\u003cport\u003e. Runs detached (`--daemonize`) and returns once the server is accepting connections. This is `mysqld --daemonize --datadir=\u003cdatadir\u003e --socket=\u003cdatadir\u003e/mysql.sock --port=\u003cport\u003e --bind-address=127.0.0.1 --mysqlx=OFF --pid-file=\u003cdatadir\u003e/mysqld.pid --log-error=\u003cdatadir\u003e/mysqld.err`." + }, + { + "name": "mysql.stop", + "summary": "Stop the local server listening on \u003cport\u003e (a clean shutdown). This is `mysqladmin -h 127.0.0.1 -P \u003cport\u003e -u root shutdown`." + }, + { + "name": "mysql.ping", + "summary": "Check whether the server on \u003cport\u003e is up and accepting connections (liveness probe). This is `mysqladmin -h 127.0.0.1 -P \u003cport\u003e -u root ping`." + }, + { + "name": "mysql.createdb", + "summary": "Create a database on the running server (no-op if it already exists). This is `mysql -h 127.0.0.1 -P \u003cport\u003e -u root -e \"CREATE DATABASE IF NOT EXISTS \u003cdbname\u003e\"`." + }, + { + "name": "mysql.query", + "summary": "Run SQL against a database and return an aligned ASCII table — the default, human-readable shape. May contain multiple `;`-separated statements. This is `mysql -h 127.0.0.1 -P \u003cport\u003e -u root -D \u003cdatabase\u003e --table -e \u003csql\u003e`." + }, + { + "name": "mysql.query_tsv", + "summary": "Same as mysql.query but returns tab-separated values (header + rows) via `--batch` — the machine-parseable shape. This is `mysql -h 127.0.0.1 -P \u003cport\u003e -u root -D \u003cdatabase\u003e --batch -e \u003csql\u003e`." + }, + { + "name": "mysql.databases", + "summary": "List the databases on the server (`SHOW DATABASES`). This is `mysql -h 127.0.0.1 -P \u003cport\u003e -u root --table -e \"SHOW DATABASES\"`." + }, + { + "name": "mysql.tables", + "summary": "List the tables in a database (`SHOW TABLES`). This is `mysql -h 127.0.0.1 -P \u003cport\u003e -u root -D \u003cdatabase\u003e --table -e \"SHOW TABLES\"`." + }, + { + "name": "mysql.dump", + "summary": "Dump a database as SQL (schema + data) with mysqldump — a portable logical backup the agent can save or replay. This is `mysqldump -h 127.0.0.1 -P \u003cport\u003e -u root --no-tablespaces \u003cdatabase\u003e`." + }, + { + "name": "mysql.exec", + "summary": "Run any bundled MySQL tool with a verbatim argv — the full surface beyond the curated methods. Payload is {\"args\":[tool, ...]} where tool is one of `mysql`, `mysqld`, `mysqladmin`, `mysqldump`, `mysqlshow`, plus optional {\"stdin\":\"...\"} piped to the process. Examples: {\"args\":[\"mysql\",\"--protocol=TCP\",\"-h\",\"127.0.0.1\",\"-P\",\"13306\",\"-u\",\"root\",\"-D\",\"app\",\"--vertical\",\"-e\",\"SELECT * FROM t\\\\G\"]}; {\"args\":[\"mysqlshow\",\"--protocol=TCP\",\"-h\",\"127.0.0.1\",\"-P\",\"13306\",\"-u\",\"root\",\"app\"]}; {\"args\":[\"mysql\",\"--protocol=TCP\",\"-h\",\"127.0.0.1\",\"-P\",\"13306\",\"-u\",\"root\",\"-D\",\"app\"],\"stdin\":\"SOURCE /work/schema.sql;\"}." + }, + { + "name": "mysql.mysql_help", + "summary": "Return the full `mysql` client help — every command-line option — captured from the delivered binary. The reference for what mysql.query / mysql.exec accept." + }, + { + "name": "mysql.version", + "summary": "Print the delivered MySQL version, e.g. \"mysql Ver 9.7.1 for … (conda-forge)\". This is `mysql --version`." + }, + { + "name": "mysql.help", + "summary": "Discovery: every method with params, kind, and latency class." + } + ], + "changelog": [ + { + "version": "9.7.1", + "notes": [ + "Released v9.7.1" + ] + } + ], + "links": [ + { + "label": "Source", + "url": "https://github.com/mysql/mysql-server" + }, + { + "label": "Website", + "url": "https://www.mysql.com" + } + ] +} diff --git a/catalogue/apps/io.pilot.orthogonal/metadata.json b/catalogue/apps/io.pilot.orthogonal/metadata.json index 2482f43f..872c4e85 100644 --- a/catalogue/apps/io.pilot.orthogonal/metadata.json +++ b/catalogue/apps/io.pilot.orthogonal/metadata.json @@ -3,7 +3,7 @@ "id": "io.pilot.orthogonal", "display_name": "Orthogonal", "tagline": "One key, 851 paid APIs — described in English, metered per user", - "description_md": "# Orthogonal — a catalog of paid tools and APIs, for your agent\n\nOrthogonal is an **API marketplace / meta-API**: a single key fronts **58\nthird-party APIs across 851 endpoints** — lead \u0026 contact enrichment, work-email\nand phone finding, web \u0026 social scraping, AI web search, company / people /\njobs data, weather, voice/phone, email inboxes, and more. You never sign up for\nthe underlying providers and you never juggle their keys — you describe what you\nneed, and pay Orthogonal per call. This Pilot app wraps Orthogonal's control\nplane behind the managed-key broker, so **your agent gets one metered, keyless\nsurface** and a **per-user $5 budget**.\n\n## The workflow: discover → price → execute\n\n1. **Discover — `orthogonal.search`** (the natural-language router ⭐).\n Describe the task in plain English, e.g. *\"find the work email and phone for\n a person given their name and company\"*, and get back the ranked APIs and\n endpoints that can do it, grouped by API with a relevance score. This is the\n \"which API do I need?\" endpoint — you don't have to know the catalog.\n2. **Price — `orthogonal.details`.** Pass an `{api, path}` and get the full\n request schema (every path/query/body param, with types and required flags)\n **and the exact price in dollars**. This is the authoritative price source —\n search and list return `null` prices.\n3. **Execute — `orthogonal.run`.** Call `{api, path, body?, query?}` and\n Orthogonal dispatches to the provider, returns the provider's data, and\n reports the exact `priceCents` charged. This is the **only call that costs\n money**.\n\n`orthogonal.integrate` and `orthogonal.list` round out discovery (code snippets\nand full-catalog browse), and `orthogonal.balance` / `.usage` / `.transactions`\n/ `.check` expose the account ledger — all free.\n\n## What you can do (representative)\n\n- **Contact \u0026 lead enrichment** — apollo, contactout, company-enrich,\n peopledatalabs, coresignal, aviato, crustdata, ocean-io, influencers-club.\n- **Work-email \u0026 phone finding** — tomba, icypeas, contactout, company-enrich.\n- **Web \u0026 AI search** — serper, linkup, tavily, exa/perplexity (unverified set).\n- **Web \u0026 social scraping** — olostep, serper-scrape, scrapecreators (107\n endpoints), scrapegraphai, fiber (92 endpoints).\n- **Company / firmographic / jobs data** — predictleads, fantastic-jobs,\n openfunnel, edges, context-dev, brand-dev.\n- **Weather, voice/phone, email inboxes** — precip, agentphone, agentmail.\n\nRun `orthogonal.search` (or `orthogonal.list`) for the live, complete set.\n\n## Pricing — how you're billed (true to real usage)\n\n- **Only `orthogonal.run` costs money.** Every discovery, pricing, and account\n call is **free**.\n- Each run is billed the **target endpoint's real price**, debited from your\n **$5 per-user budget** in micro-dollars. The `priceCents` in the run response\n is the exact amount charged (real cents); `X-Pilot-Credits-Remaining` on every\n response is your remaining budget in micro-dollars ($5 = 5000000).\n- Prices span **$0.001 – $3.50**. Distribution across the 851 endpoints: **11\n free, ~612 fixed-price, 104 \"dynamic\"** (priced only after the call). Common\n tiers: Basic $0.001–0.01, Standard $0.01–0.10, Premium $0.10–1.00. Cheap\n endpoints to start with: serper ($0.002), olostep / tomba / linkup ($0.01).\n- For a **known** cost up front, call `orthogonal.details` first. For\n **\"dynamic\"** endpoints the price is only knowable from the run response — so\n metering is done on the actual charged amount, and a single call may spend the\n last of your budget; after that, `orthogonal.run` returns **402** (the free\n discovery calls keep working).\n\n## Per-user budget \u0026 fair use\n\nEach Pilot user is seeded **$5 of credit** on first use, metered by the broker\nagainst their signed pilot identity. When the budget is exhausted, billable runs\nreturn 402. To keep the shared master account fair, the broker also enforces a\n**per-IP identity cap**: a small number of distinct pilot identities may claim a\nfresh $5 from any one network, so a depleted user can't farm new budgets by\nminting new identities. The Orthogonal account itself is the ultimate backstop —\nif it runs dry, runs return 402 until it's topped up.\n\n## Good to know\n\n- **Auth is fully managed.** The `orth_live_` master key lives only in the\n broker; the installed adapter is keyless and signs each request with your\n pilot identity. Nothing to configure.\n- **The account is shared** across Pilot users (no per-user sub-accounts on\n Orthogonal), so `orthogonal.balance` / `.usage` / `.transactions` report the\n **account-wide** figures — your **personal** remaining budget is the\n `X-Pilot-Credits-Remaining` header.\n- Errors surface verbatim: 402 insufficient credit, 404 unknown api/path, 5xx\n upstream provider error, 429 rate-limited (back off).\n- `orthogonal.help` is the self-describing discovery contract: it lists every\n method with params, cost note, and latency class.\n", + "description_md": "# Orthogonal — a catalog of paid tools and APIs, for your agent\n\nOrthogonal is an **API marketplace / meta-API**: a single key fronts **58 third-party APIs across 851 endpoints** — lead \u0026 contact enrichment, work-email and phone finding, web \u0026 social scraping, AI web search, company / people / jobs data, weather, voice/phone, email inboxes, and more. You never sign up for the underlying providers and you never juggle their keys — you describe what you need, and pay Orthogonal per call. This Pilot app wraps Orthogonal's control plane behind the managed-key broker, so **your agent gets one metered, keyless surface** and a **per-user $5 budget**.\n\n## The workflow: discover → price → execute\n\n1. **Discover — `orthogonal.search`** (the natural-language router ★). Describe the task in plain English, e.g. *\"find the work email and phone for a person given their name and company\"*, and get back the ranked APIs and endpoints that can do it, grouped by API with a relevance score. This is the \"which API do I need?\" endpoint — you don't have to know the catalog.\n2. **Price — `orthogonal.details`.** Pass an `{api, path}` and get the full request schema (every path/query/body param, with types and required flags) **and the exact price in dollars**. This is the authoritative price source — search and list return `null` prices.\n3. **Execute — `orthogonal.run`.** Call `{api, path, body?, query?}` and Orthogonal dispatches to the provider, returns the provider's data, and reports the exact `priceCents` charged. This is the **only call that costs money**.\n\n`orthogonal.integrate` and `orthogonal.list` round out discovery (code snippets and full-catalog browse), and `orthogonal.balance` shows YOUR remaining per-user budget — all free.\n\n## What you can do (representative)\n\n- **Contact \u0026 lead enrichment** — apollo, contactout, company-enrich, peopledatalabs, coresignal, aviato, crustdata, ocean-io, influencers-club.\n- **Work-email \u0026 phone finding** — tomba, icypeas, contactout, company-enrich, hunter.\n- **Web \u0026 AI search** — serper, linkup, tavily, exa/perplexity.\n- **Web \u0026 social scraping** — olostep, serper-scrape, scrapecreators (107 endpoints), scrapegraphai, fiber (92 endpoints).\n- **Company / firmographic / jobs data** — predictleads, fantastic-jobs, openfunnel, edges, context-dev, brand-dev.\n- **Weather, voice/phone, email inboxes** — precip, agentphone, agentmail.\n\nRun `orthogonal.search` (or `orthogonal.list`) for the live, complete set.\n\n## Pricing — how you're billed (true to real usage)\n\n- **Only `orthogonal.run` costs money.** Every discovery, pricing, and account call is **free**.\n- Each run is billed the **target endpoint's real price**, debited from your **$5 per-user budget** in micro-dollars. The `priceCents` in the run response is the exact amount charged (real cents); `X-Pilot-Credits-Remaining` on every response is your remaining budget in micro-dollars ($5 = 5000000).\n- Prices span **$0.001 – $3.50**. Distribution across the 851 endpoints: **11 free, ~612 fixed-price, 104 \"dynamic\"** (priced only after the call). Common tiers: Basic $0.001–0.01, Standard $0.01–0.10, Premium $0.10–1.00. Cheap endpoints to start with: serper ($0.002), olostep / tomba / linkup ($0.01).\n- For a **known** cost up front, call `orthogonal.details` first. For **\"dynamic\"** endpoints the price is only knowable from the run response — so metering is done on the actual charged amount, and a single call may spend the last of your budget; after that, `orthogonal.run` returns **402** (the free discovery calls keep working).\n\n## Per-user budget \u0026 fair use\n\nEach Pilot user is seeded **$5 of credit** on first use, metered by the broker against their signed pilot identity. When the budget is exhausted, billable runs return 402. To keep the shared master account fair, the broker also enforces a **per-IP identity cap**: a small number of distinct pilot identities may claim a fresh $5 from any one network, so a depleted user can't farm new budgets by minting new identities. The Orthogonal account itself is the ultimate backstop — if it runs dry, runs return 402 until it's topped up.\n\n## Good to know\n\n- **Auth is fully managed.** The `orth_live_` master key lives only in the broker; the installed adapter is keyless and signs each request with your pilot identity. Nothing to configure.\n- **You only ever see your own budget.** The provider account is shared (no per-user sub-accounts on Orthogonal), so the broker deliberately does **not** expose the account-wide balance/usage/ledger. `orthogonal.balance` is answered by the broker from its own per-user ledger — you get your personal remaining budget (also on the `X-Pilot-Credits-Remaining` header), never the pooled account total.\n- Errors surface verbatim: 402 insufficient credit, 404 unknown api/path, 5xx upstream provider error, 429 rate-limited (back off).\n- `orthogonal.help` is the self-describing discovery contract: it lists every method with params, cost note, and latency class.", "vendor": { "name": "Orthogonal", "url": "https://orthogonal.com", @@ -11,7 +11,7 @@ "publisher_pubkey": "ed25519:8zcpGtp5sUw3sjLptDR9W/btumEaOv3DO3tTWN/+PCk=" }, "homepage": "https://orthogonal.com", - "source_url": "https://github.com/pilot-protocol/app-template", + "source_url": "https://github.com/pilot-protocol/app-template/tree/main/submissions/io.pilot.orthogonal", "license": "MIT", "categories": [ "data", @@ -34,8 +34,8 @@ "natural-language" ], "size": { - "bundle_bytes": 5076601, - "installed_bytes": 9137279 + "bundle_bytes": 5073695, + "installed_bytes": 9123313 }, "compat": { "min_pilot_version": "1.10.0", @@ -62,23 +62,11 @@ }, { "name": "orthogonal.run", - "summary": "★ Execute any of the 851 provider endpoints via {api, path, body?, query?} (the HTTP method is chosen automatically). THIS IS THE ONLY CALL THAT COSTS MONEY: you are billed the target endpoint's real price and it is debited from your $5 Pilot budget. The response returns priceCents (cents actually charged) alongside the provider data, and X-Pilot-Credits-Remaining shows your budget. Once your $5 is spent, run returns 402 while the free discovery calls keep working. Prices range $0.001–$3.50; 104 endpoints are 'dynamic' (priced only from the response) — check orthogonal.details first when you need the cost up front." + "summary": "★ Execute any of the 851 provider endpoints via a JSON payload {api, path, body?, query?} (the HTTP method is chosen automatically; body is the provider request body, query is its query-string params). THIS IS THE ONLY CALL THAT COSTS MONEY: you are billed the target endpoint's real price and it is debited from your $5 Pilot budget. The response returns priceCents (cents actually charged) alongside the provider data, and X-Pilot-Credits-Remaining shows your budget. Once your $5 is spent, run returns 402 while the free discovery calls keep working. Prices range $0.001–$3.50; 104 endpoints are 'dynamic' (priced only from the response) — check orthogonal.details first when you need the cost up front." }, { "name": "orthogonal.balance", - "summary": "Orthogonal account credit balance (string '$X.XX'). FREE, read-only. Note: your per-user $5 Pilot budget is metered separately by the broker and is surfaced in the X-Pilot-Credits-Remaining header on every call — that header, not this account balance, is your personal remaining budget." - }, - { - "name": "orthogonal.check", - "summary": "Preflight the shared account: does it hold ≥ amountCents credits? (amountCents is in Orthogonal credit units = dollars × 100000, so $5 = 500000). FREE." - }, - { - "name": "orthogonal.transactions", - "summary": "Account transaction ledger — purchases, API charges, bonuses, refunds — with amounts, direction and timestamps, paginated. FREE, read-only. Amounts are in the credits unit (dollars × 100000)." - }, - { - "name": "orthogonal.usage", - "summary": "Per-call spend history plus totalSpent over a window (dollar strings), for the shared account. FREE, read-only." + "summary": "YOUR remaining per-user budget on this app — returned by the broker from its own ledger as '$X.XX' plus credits_remaining (micro-USD; $5 = 5000000) and credits_seed. FREE, read-only, no upstream call. This is your personal budget, seeded at $5 on first use and debited by your own runs; the shared provider account's balance is never exposed. The same figure is on the X-Pilot-Credits-Remaining header of every response." }, { "name": "orthogonal.help", @@ -87,16 +75,16 @@ ], "changelog": [ { - "version": "0.1.0", + "version": "0.1.1", "notes": [ - "Released v0.1.0" + "Released v0.1.1" ] } ], "links": [ { "label": "Source", - "url": "https://github.com/pilot-protocol/app-template" + "url": "https://github.com/pilot-protocol/app-template/tree/main/submissions/io.pilot.orthogonal" }, { "label": "Website", diff --git a/catalogue/apps/io.pilot.smolmachines/metadata.json b/catalogue/apps/io.pilot.smolmachines/metadata.json deleted file mode 100644 index 522394e1..00000000 --- a/catalogue/apps/io.pilot.smolmachines/metadata.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "schema_version": 1, - "id": "io.pilot.smolmachines", - "display_name": "Smol Machines", - "tagline": "Fast, hardware-isolated microVMs on demand", - "description_md": "Smol Machines — the app-store front door for the smolmachines VM engine. It lets an agent spin up fast, hardware-isolated Linux microVMs on demand (sub-second boot, real hypervisor isolation — not shared-kernel containers), then run workloads in a disposable sandbox. Free to use. Portable .smolmachine artifacts run identically on macOS and Linux, locally or in the cloud.\n\nUse it to:\n- Run untrusted or AI-generated code safely, with networking off by default\n- Give an agent a real Linux shell — a stateful, isolated execution backend\n- Automate headless browsers (GPU-accelerated) for scraping, screenshots, and web tasks\n- Run GPU/compute jobs via Vulkan with container-like speed\n- Spin up disposable dev sandboxes — a clean VM per task, torn down after\n- Keep persistent dev VMs — installed packages survive restarts\n- Run CI-style jobs — build, test, lint in clean environments\n- Fan out parallel ephemeral workers thanks to sub-second boot\n- Analyze malware / suspicious files in a throwaway environment\n- Build once, run anywhere — same artifact local, cloud, or self-hosted\n\nDiscover the live method surface at runtime with smolmachines.help, which lists each method's parameters and latency class.", - "vendor": { - "name": "smol machines", - "url": "https://smolmachines.com", - "publisher_pubkey": "ed25519:3QJm6H6OdjtfrF+Es1lrRjfFmdtq2tGvVSWxia63vcI=" - }, - "homepage": "https://smolmachines.com", - "source_url": "https://github.com/smol-machines/smolvm", - "license": "Apache-2.0", - "categories": [ - "dev", - "virtualization", - "security" - ], - "keywords": [ - "microvm", - "sandbox", - "vm", - "isolation", - "gpu", - "ci" - ], - "size": { - "bundle_bytes": 5346146, - "installed_bytes": 9601119 - }, - "compat": { - "min_pilot_version": "1.0.0", - "runtimes": [ - "go" - ] - }, - "methods": [ - { - "name": "smolmachines.exec", - "summary": "Run any smolvm subcommand in a fast, hardware-isolated Linux microVM. Payload is {\"args\":[...]} — the verbatim smolvm argv. Command surface: `machine run` (ephemeral VM, one-off command), `machine create|start|exec|stop|delete|shell|status|ls|cp|update|monitor|prune` (persistent VMs; `exec` persists filesystem changes), `pack create|run` (portable .smolmachine artifacts), `serve` (HTTP API), `config`. Key flags: `--net` (networking is OFF by default), `--image `, `-v HOST:GUEST`, `-p HOST:GUEST`, `--gpu`, `--ssh-agent`, `--secret-env GUEST=HOST`. Example args: [\"machine\",\"run\",\"--net\",\"--image\",\"alpine\",\"--\",\"sh\",\"-c\",\"echo hi\"]. Not supported over IPC: interactive sessions (-it / `machine shell`) and long-running `serve`." - }, - { - "name": "smolmachines.help", - "summary": "Discovery: every method with params, kind, and latency class." - } - ], - "changelog": [ - { - "version": "1.2.0", - "notes": [ - "Released v1.2.0" - ] - } - ], - "links": [ - { - "label": "Source", - "url": "https://github.com/smol-machines/smolvm" - }, - { - "label": "Website", - "url": "https://smolmachines.com" - } - ] -} diff --git a/catalogue/catalogue.json b/catalogue/catalogue.json index af06bedb..aa113ee5 100644 --- a/catalogue/catalogue.json +++ b/catalogue/catalogue.json @@ -1,6 +1,6 @@ { "version": 2, - "updated_at": "2026-07-03T00:00:00Z", + "updated_at": "2026-07-08T00:44:44Z", "apps": [ { "id": "io.pilot.wallet", @@ -82,34 +82,10 @@ }, { "id": "io.pilot.smolmachines", - "version": "1.2.0", - "description": "Smol Machines — spin up fast, hardware-isolated Linux microVMs on demand (sub-second boot, real hypervisor isolation) to safely run untrusted code, GPU tasks, or headless browser automation in a disposable sandbox.", - "display_name": "Smol Machines", - "vendor": "smol machines", - "license": "Apache-2.0", - "source_url": "https://github.com/smol-machines/smolvm", - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.smolmachines/1.2.0/io.pilot.smolmachines-1.2.0-linux-amd64.tar.gz", - "bundle_sha256": "44141c68524081f4ef6b439f4bb18e949b91900db1d93bf88c6178aaaac917e6", - "bundles": { - "darwin/arm64": { - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.smolmachines/1.2.0/io.pilot.smolmachines-1.2.0-darwin-arm64.tar.gz", - "bundle_sha256": "b6e6b1604b96939966e4fbe9daaa7f1a994e073e168a1be69c6be55498d3d62c" - }, - "darwin/amd64": { - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.smolmachines/1.2.0/io.pilot.smolmachines-1.2.0-darwin-amd64.tar.gz", - "bundle_sha256": "06835bbdfabe684bc302c75dbafcc27e820c462bde0bedfc7b78d6e2593443e8" - }, - "linux/arm64": { - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.smolmachines/1.2.0/io.pilot.smolmachines-1.2.0-linux-arm64.tar.gz", - "bundle_sha256": "1df2c5d0ff9b5ad1db4773278da4f52e60cd77fb9169955c5e81f316da610d3c" - }, - "linux/amd64": { - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.smolmachines/1.2.0/io.pilot.smolmachines-1.2.0-linux-amd64.tar.gz", - "bundle_sha256": "44141c68524081f4ef6b439f4bb18e949b91900db1d93bf88c6178aaaac917e6" - } - }, - "metadata_url": "https://raw.githubusercontent.com/pilot-protocol/pilotprotocol/main/catalogue/apps/io.pilot.smolmachines/metadata.json", - "metadata_sha256": "f43493f690786b8adbe1ac1072bbec0b0d04e05d9a247e7b14e5d36c4e397a3b", + "renamed_to": "io.pilot.smol", + "hidden": true, + "display_name": "Smol Machines (renamed → io.pilot.smol)", + "description": "Renamed to io.pilot.smol. Install io.pilot.smol — it is the same app plus a cloud plane. This tombstone entry is retained only to keep already-installed copies running and to redirect the old id; it is not listed and is not installable.", "publisher": "ed25519:3QJm6H6OdjtfrF+Es1lrRjfFmdtq2tGvVSWxia63vcI=" }, { @@ -541,31 +517,31 @@ }, { "id": "io.pilot.orthogonal", - "version": "0.1.0", + "version": "0.1.1", "description": "Connect your agent to Orthogonal — one key fronting 58 paid APIs / 851 endpoints (lead & contact enrichment, email & phone finding, web & social scraping, AI search, company/people/jobs data, weather, voice). Describe a task in plain English and Orthogonal's router returns the exact APIs to call; execute any of them through a single run, billed at the real per-call price and metered against a per-user $5 budget. Discovery, pricing and balance calls are free.", "display_name": "Orthogonal", "vendor": "Orthogonal", "license": "MIT", "source_url": "https://github.com/pilot-protocol/app-template/tree/main/submissions/io.pilot.orthogonal", - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.0/io.pilot.orthogonal-0.1.0-linux-amd64.tar.gz", - "bundle_sha256": "a773c017d0f9b4498db2123473541dece29b8367a34387d52ca5abc7c2932c7e", - "bundle_size": 5076601, + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.1/io.pilot.orthogonal-0.1.1-linux-amd64.tar.gz", + "bundle_sha256": "3f2c9bfc009e41898b36ff2e88a313c0c3bc8bb39500f9673572a1d40b84fac7", + "bundle_size": 5073695, "bundles": { "darwin/amd64": { - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.0/io.pilot.orthogonal-0.1.0-darwin-amd64.tar.gz", - "bundle_sha256": "55a0645bbf8506040d88231e392bc43c517385f64875e1718090b403bf05809b" + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.1/io.pilot.orthogonal-0.1.1-darwin-amd64.tar.gz", + "bundle_sha256": "dbfdd6624a04a9fa6cdec86c9b5cfad1cdb3fda455bd58eeae6e03d59b4e9305" }, "darwin/arm64": { - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.0/io.pilot.orthogonal-0.1.0-darwin-arm64.tar.gz", - "bundle_sha256": "404cc4588b38e5eb2341cad160b07d39a34f6ca7dd7f4cdaafc56e03dd54bfcd" + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.1/io.pilot.orthogonal-0.1.1-darwin-arm64.tar.gz", + "bundle_sha256": "25bb06edfca365b201fd137c46cb06a340c8df98ec43181d57f7214c555a55e9" }, "linux/amd64": { - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.0/io.pilot.orthogonal-0.1.0-linux-amd64.tar.gz", - "bundle_sha256": "a773c017d0f9b4498db2123473541dece29b8367a34387d52ca5abc7c2932c7e" + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.1/io.pilot.orthogonal-0.1.1-linux-amd64.tar.gz", + "bundle_sha256": "3f2c9bfc009e41898b36ff2e88a313c0c3bc8bb39500f9673572a1d40b84fac7" }, "linux/arm64": { - "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.0/io.pilot.orthogonal-0.1.0-linux-arm64.tar.gz", - "bundle_sha256": "2882a4db3250bb0e3e63ee1f3d7746eadad823b9e1c6bebf8aa96b8c1f18e9a9" + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.orthogonal/0.1.1/io.pilot.orthogonal-0.1.1-linux-arm64.tar.gz", + "bundle_sha256": "e1d70fa3f9b49f3d2d92717b63db4ac84b525d24bd7c102ce64de62beb57a6f6" } }, "categories": [ @@ -576,7 +552,7 @@ "ai" ], "metadata_url": "https://raw.githubusercontent.com/pilot-protocol/pilotprotocol/main/catalogue/apps/io.pilot.orthogonal/metadata.json", - "metadata_sha256": "e3094041d1bc38841c7fe82ce7596b409dbed25f8a81722a5456898b7360d554", + "metadata_sha256": "edd74b570be207cc27405a5cf924e38e777ee5c9b091f4d805b3981f629e4486", "publisher": "ed25519:8zcpGtp5sUw3sjLptDR9W/btumEaOv3DO3tTWN/+PCk=" }, { @@ -610,6 +586,84 @@ "metadata_url": "https://raw.githubusercontent.com/pilot-protocol/pilotprotocol/main/catalogue/apps/io.pilot.agentphone/metadata.json", "metadata_sha256": "9099d15e6008aaf0506c4dc91607644b201a866f78f5db2343f85413de7a6ee1", "publisher": "ed25519:mvVzYABubZwOTzWWQA/TDbRLYkKzmD/x6k/w0nz+zHc=" + }, + { + "id": "io.pilot.mysql", + "version": "9.7.1", + "description": "MySQL 9.7.1 server + client as a native CLI for agents: a full, real client/server SQL database that runs entirely locally with no cloud account and no provisioning. Initialize a data directory, start a server on 127.0.0.1, create databases, and run SQL — results as an aligned table or TSV — with the actual InnoDB engine (transactions, foreign keys, triggers, JSON, window functions). Lifecycle (initialize/start/stop/ping), createdb, query/query_tsv, databases/tables listing, mysqldump, and the full tool surface via a verbatim-argv passthrough. The transactional server-grade sibling of io.pilot.sqlite and io.pilot.postgres.", + "display_name": "MySQL", + "vendor": "Pilot Protocol", + "license": "GPL-2.0", + "source_url": "https://github.com/mysql/mysql-server", + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.mysql/9.7.1/io.pilot.mysql-9.7.1-linux-amd64.tar.gz", + "bundle_sha256": "74e57bee2b99cbef1cc10a1120894c6872027766509f0d8d4b252b93d1d0a9e1", + "bundle_size": 5359647, + "bundles": { + "darwin/arm64": { + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.mysql/9.7.1/io.pilot.mysql-9.7.1-darwin-arm64.tar.gz", + "bundle_sha256": "2d6007afea3c6e59aea914c551631c39624ded6cf988dd2252d0dab092e88c21" + }, + "darwin/amd64": { + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.mysql/9.7.1/io.pilot.mysql-9.7.1-darwin-amd64.tar.gz", + "bundle_sha256": "1cdf637b8dae34b57503ee2dfbd8a6e24eb587c90233edd5718976e9db60120e" + }, + "linux/amd64": { + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.mysql/9.7.1/io.pilot.mysql-9.7.1-linux-amd64.tar.gz", + "bundle_sha256": "74e57bee2b99cbef1cc10a1120894c6872027766509f0d8d4b252b93d1d0a9e1" + }, + "linux/arm64": { + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.mysql/9.7.1/io.pilot.mysql-9.7.1-linux-arm64.tar.gz", + "bundle_sha256": "b37777304ef1976c783e2ad1f667cd8b0188c5a39d5bfe45880ec2baca007fa3" + } + }, + "categories": [ + "database", + "data", + "sql" + ], + "metadata_url": "https://raw.githubusercontent.com/pilot-protocol/pilotprotocol/main/catalogue/apps/io.pilot.mysql/metadata.json", + "metadata_sha256": "9d921a50dc7f7231b55176b2655bd1b0402cc017bea4d605c4391434ccbb79d5", + "publisher": "ed25519:cTZr4unmAHK/r8fS+TVQ4QiCIORimYe+y+bIdiUUfAY=" + }, + { + "id": "io.pilot.didit", + "version": "1.0.0", + "description": "Full Didit identity-verification platform over one HTTPS app — KYC/ID, liveness, face match, AML, proof-of-address, database validation, email/phone OTP, plus hosted sessions, workflows, users, billing, blocklists, questionnaires and webhooks. didit.signup mints your own Didit API key in ONE call with no email and no code (Pilot's broker handles the whole signup), caches it locally, and then every method authenticates as you and bills to your own Didit balance (500 free full-KYC checks/month).", + "display_name": "Didit", + "vendor": "Didit", + "license": "Proprietary", + "source_url": "https://github.com/didit-protocol/skills", + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.didit/1.0.0/io.pilot.didit-1.0.0-linux-amd64.tar.gz", + "bundle_sha256": "57c63cf5c51c10871273126b408dae17e2c4cffc5330342d96c648ca95aa29d5", + "bundle_size": 5130067, + "bundles": { + "darwin/arm64": { + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.didit/1.0.0/io.pilot.didit-1.0.0-darwin-arm64.tar.gz", + "bundle_sha256": "5910ff8f2af94f8545d49fa6c0a179c75c4334bd54fb37a3feb7ee197ad551c5" + }, + "darwin/amd64": { + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.didit/1.0.0/io.pilot.didit-1.0.0-darwin-amd64.tar.gz", + "bundle_sha256": "03275275e7838ca9b8be39d7355462271a8d23ad32a60ea4cb87b1f7b6cbf5a0" + }, + "linux/amd64": { + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.didit/1.0.0/io.pilot.didit-1.0.0-linux-amd64.tar.gz", + "bundle_sha256": "57c63cf5c51c10871273126b408dae17e2c4cffc5330342d96c648ca95aa29d5" + }, + "linux/arm64": { + "bundle_url": "https://pub-f09f9a4ea848491198d48e329ba030e3.r2.dev/bundles/io.pilot.didit/1.0.0/io.pilot.didit-1.0.0-linux-arm64.tar.gz", + "bundle_sha256": "090b73c669f4df41e3994441edd261bfd4ec511c806899a28571d8d99a675c6c" + } + }, + "categories": [ + "identity", + "verification", + "compliance", + "security", + "kyc" + ], + "metadata_url": "https://raw.githubusercontent.com/pilot-protocol/pilotprotocol/main/catalogue/apps/io.pilot.didit/metadata.json", + "metadata_sha256": "2f511777b817867da6f2a99ae08e731bff64024ce3848ea0a49732882936a65d", + "publisher": "ed25519:ii6QAc8qZB4NvbOXqnLfnv+OhPqepuK0BISAv9jM5x0=" } ] } diff --git a/catalogue/catalogue.json.sig b/catalogue/catalogue.json.sig index be456b58..71543d20 100644 --- a/catalogue/catalogue.json.sig +++ b/catalogue/catalogue.json.sig @@ -1 +1 @@ -zjuh59YUSuwetWmEVs3K/0A3v8gsQCRN2NjDW98UoSU8UCj2AGHPWOZg30ncUayLLDove9N7URsQmMqPSF3BDg== +WecP3toRUBJ1pkCzJHp2SlHZE92fnXkonVMb7FjJBTEdACTXpK3roS+LHmf5HFrH+OJqVS5q5ut5h28w1bTqBQ== diff --git a/cmd/pilotctl/appstore.go b/cmd/pilotctl/appstore.go index 296d4edc..b467fc37 100644 --- a/cmd/pilotctl/appstore.go +++ b/cmd/pilotctl/appstore.go @@ -2152,6 +2152,20 @@ func cmdAppStoreCall(args []string) { sockPath := filepath.Join(appStoreRoot(), appID, "app.sock") if _, err := os.Stat(sockPath); err != nil { + // No local socket for this id. It may be a rename tombstone — an id that + // was renamed and never installed here. Consult the catalogue (best-effort, + // offline-tolerant) and, if so, warn with a switch hint before failing. + // We do NOT silently retarget to the new app's socket: the method + // namespace changes with the rename (e.g. smolmachines.* → smol.*), so a + // verbatim old method would only 404 there. An existing old-id install + // keeps its own socket at this path, so this branch never fires for it. + if c, lerr := loadCatalogue(); lerr == nil { + if canonicalID, _, renamed := resolveRenamed(c, appID); renamed { + fatalHint("invalid_argument", + fmt.Sprintf("install %q (`pilotctl appstore install %s`) and call its methods — see `pilotctl appstore view %s`", canonicalID, canonicalID, canonicalID), + "app %q was renamed to %q", appID, canonicalID) + } + } fatalHint("io_error", "is the daemon running and has it supervised this app yet?", "socket %s not present: %v", sockPath, err) diff --git a/cmd/pilotctl/appstore_catalogue.go b/cmd/pilotctl/appstore_catalogue.go index 174a5f87..c612f32a 100644 --- a/cmd/pilotctl/appstore_catalogue.go +++ b/cmd/pilotctl/appstore_catalogue.go @@ -118,6 +118,22 @@ type catalogueEntry struct { // manifest. MetadataURL string `json:"metadata_url,omitempty"` MetadataSHA string `json:"metadata_sha256,omitempty"` + + // --- rename / deprecation redirect --- + // RenamedTo, when non-empty, marks this entry as a tombstone: the app was + // renamed to the given catalogue id. The entry is kept (not deleted) only to + // preserve the publisher pin for already-installed copies — the daemon + // supervisor fail-closes an installed app whose id has no catalogue pin — and + // to redirect id resolution. `install`/`view`/`call` of this id warn and route + // to RenamedTo; it is omitted from the `catalogue` listing (see Hidden). + // Older clients that don't know this field simply ignore it (omitempty keeps + // v1/v2 wire-compat). At most one hop is followed — a chain is a catalogue bug. + RenamedTo string `json:"renamed_to,omitempty"` + + // Hidden, when true, omits this entry from the `catalogue` listing. It is + // still resolvable by id (for redirect and back-compat). A RenamedTo tombstone + // is treated as hidden regardless of this flag. + Hidden bool `json:"hidden,omitempty"` } // bundleVariant is one platform's downloadable tarball + its pinned sha256. @@ -307,15 +323,25 @@ func cmdAppStoreCatalogue(_ []string) { "check $PILOT_APPSTORE_CATALOG_URL (currently: "+catalogueURL()+")", "%v", err) } + // Tombstones (renamed_to) and explicitly hidden entries are not offered in + // the listing — they exist only to preserve publisher pins and to redirect + // old ids. They remain resolvable by `view`/`install`/`call`. + visible := make([]catalogueEntry, 0, len(c.Apps)) + for _, e := range c.Apps { + if e.Hidden || e.RenamedTo != "" { + continue + } + visible = append(visible, e) + } if jsonOutput { - _ = json.NewEncoder(os.Stdout).Encode(c.Apps) + _ = json.NewEncoder(os.Stdout).Encode(visible) return } - if len(c.Apps) == 0 { + if len(visible) == 0 { fmt.Println("catalogue is empty") return } - for _, e := range c.Apps { + for _, e := range visible { fmt.Printf("%-40s %s\n", e.ID, e.Description) } fmt.Println("\nRun 'pilotctl appstore view ' for full details.") @@ -338,12 +364,43 @@ const ( installSourceLocal ) +// findEntry returns a pointer to the catalogue entry with the given id, or nil. +// The pointer aliases c.Apps' backing array, so it stays valid for reads as long +// as c does. +func (c *catalogue) findEntry(id string) *catalogueEntry { + for i := range c.Apps { + if c.Apps[i].ID == id { + return &c.Apps[i] + } + } + return nil +} + +// resolveRenamed follows a rename tombstone. Given a requested id, it returns the +// canonical id and its entry (or nil if the canonical id is absent), plus whether +// a rename was followed. When the id is not a tombstone it returns (id, its own +// entry-or-nil, false). On a followed rename it prints a one-time deprecation +// warning to stderr. Only one hop is followed — a tombstone that points at another +// tombstone is a catalogue bug and is not chased, avoiding any loop. +func resolveRenamed(c *catalogue, id string) (canonicalID string, entry *catalogueEntry, renamed bool) { + e := c.findEntry(id) + if e == nil || e.RenamedTo == "" { + return id, e, false + } + canonical := e.RenamedTo + fmt.Fprintf(os.Stderr, + "warn: app %q has been renamed to %q — use %q instead. The old id is deprecated: it no longer receives updates and will be removed.\n", + id, canonical, canonical) + return canonical, c.findEntry(canonical), true +} + // resolveInstallTarget turns the user's `target` arg into a local // bundle directory the existing install code can consume, plus a // source tag indicating which trust regime the bundle came from. If // `target` matches a catalogue ID, the catalogue entry is fetched, -// verified, and unpacked. Otherwise `target` is treated as a local -// path and the caller is expected to apply sideload policy. +// verified, and unpacked. A `target` that matches a rename tombstone +// warns and installs the canonical app instead. Otherwise `target` is +// treated as a local path and the caller applies sideload policy. func resolveInstallTarget(target string) (string, installSource, error) { c, err := loadCatalogue() if err != nil { @@ -353,11 +410,15 @@ func resolveInstallTarget(target string) (string, installSource, error) { // the user knows their URL or env override might be the issue. fmt.Fprintf(os.Stderr, "warn: catalogue lookup failed (%v); proceeding with local-path interpretation\n", err) } else { - for _, e := range c.Apps { - if target == e.ID { - dir, err := fetchAndUnpackBundle(e) - return dir, installSourceCatalogue, err - } + // resolveRenamed also handles the plain (non-renamed) match: it + // returns the exact entry when `target` isn't a tombstone. + canonicalID, entry, renamed := resolveRenamed(c, target) + if renamed && entry == nil { + return "", installSourceLocal, fmt.Errorf("app %q was renamed to %q, but %q is missing from the catalogue", target, canonicalID, canonicalID) + } + if entry != nil { + dir, err := fetchAndUnpackBundle(*entry) + return dir, installSourceCatalogue, err } } info, err := os.Stat(target) diff --git a/cmd/pilotctl/appstore_rename_test.go b/cmd/pilotctl/appstore_rename_test.go new file mode 100644 index 00000000..db83ffa8 --- /dev/null +++ b/cmd/pilotctl/appstore_rename_test.go @@ -0,0 +1,68 @@ +package main + +import "testing" + +// A rename tombstone (renamed_to set) resolves to the canonical entry, and a +// plain id resolves to itself. This is the shared resolver behind the +// install/view/call redirect. +func TestResolveRenamed(t *testing.T) { + c := &catalogue{Apps: []catalogueEntry{ + {ID: "io.pilot.smolmachines", RenamedTo: "io.pilot.smol", Hidden: true, Publisher: "ed25519:old"}, + {ID: "io.pilot.smol", Version: "1.2.0", Publisher: "ed25519:new"}, + {ID: "io.pilot.other", Version: "1.0.0"}, + }} + + // Tombstone → canonical entry. + canonical, entry, renamed := resolveRenamed(c, "io.pilot.smolmachines") + if !renamed { + t.Fatalf("expected renamed=true for the tombstone id") + } + if canonical != "io.pilot.smol" { + t.Fatalf("canonical = %q, want io.pilot.smol", canonical) + } + if entry == nil || entry.ID != "io.pilot.smol" { + t.Fatalf("entry = %+v, want the io.pilot.smol entry", entry) + } + + // Plain id → itself, not flagged renamed. + id, e2, r2 := resolveRenamed(c, "io.pilot.smol") + if r2 || id != "io.pilot.smol" || e2 == nil || e2.ID != "io.pilot.smol" { + t.Fatalf("plain resolve got (%q, %+v, %v), want (io.pilot.smol, io.pilot.smol-entry, false)", id, e2, r2) + } + + // Unknown id → itself, nil entry, not renamed. + id3, e3, r3 := resolveRenamed(c, "io.pilot.missing") + if r3 || e3 != nil || id3 != "io.pilot.missing" { + t.Fatalf("unknown resolve got (%q, %+v, %v), want (io.pilot.missing, nil, false)", id3, e3, r3) + } +} + +// A tombstone that points at a missing canonical id still reports the rename, +// with a nil entry — callers surface a clear error rather than chasing it. +func TestResolveRenamed_CanonicalMissing(t *testing.T) { + c := &catalogue{Apps: []catalogueEntry{ + {ID: "io.pilot.old", RenamedTo: "io.pilot.gone"}, + }} + canonical, entry, renamed := resolveRenamed(c, "io.pilot.old") + if !renamed || canonical != "io.pilot.gone" || entry != nil { + t.Fatalf("got (%q, %+v, %v), want (io.pilot.gone, nil, true)", canonical, entry, renamed) + } +} + +// Only one hop is followed: a tombstone pointing at another tombstone is a +// catalogue bug and must not loop. resolveRenamed returns the (still-tombstone) +// second entry without recursing. +func TestResolveRenamed_OneHopOnly(t *testing.T) { + c := &catalogue{Apps: []catalogueEntry{ + {ID: "a", RenamedTo: "b"}, + {ID: "b", RenamedTo: "c"}, + {ID: "c", Version: "1.0.0"}, + }} + canonical, entry, renamed := resolveRenamed(c, "a") + if !renamed || canonical != "b" { + t.Fatalf("canonical = %q (renamed=%v), want b/true — one hop only", canonical, renamed) + } + if entry == nil || entry.ID != "b" { + t.Fatalf("entry = %+v, want the (tombstone) b entry, not a chased c", entry) + } +} diff --git a/cmd/pilotctl/appstore_view.go b/cmd/pilotctl/appstore_view.go index afd96b74..c8ba3430 100644 --- a/cmd/pilotctl/appstore_view.go +++ b/cmd/pilotctl/appstore_view.go @@ -157,12 +157,10 @@ func cmdAppStoreView(args []string) { var entry *catalogueEntry var meta *appMetadata if c, err := loadCatalogue(); err == nil { - for i := range c.Apps { - if c.Apps[i].ID == appID { - entry = &c.Apps[i] - break - } - } + // Follow a rename tombstone so `view ` resolves to (and displays) + // the renamed app, with a deprecation warning. Local install facts above + // stay keyed to the requested id so an existing old-id install still shows. + _, entry, _ = resolveRenamed(c, appID) if entry != nil { if m, err := loadAppMetadata(*entry); err != nil { fmt.Fprintf(os.Stderr, "warn: could not load detail metadata: %v\n", err)