Manifest
manifest.json sits at the root of the package and is covered by the signature. It is the only
place a mini app can declare what it is allowed to do — there is no runtime path that widens it.
The manifest is a dual artifact. The same file that tells the runtime which permissions to prompt for is what an AI agent reads as a tool registry. One declaration, two readers.
A complete example
Section titled “A complete example”{ "manifest_version": 1, "id": "com.acme.erp", "name": "Acme ERP", "short_name": "Acme", "version": "2.4.1", "description": "Purchase orders, approvals and expense reports for Acme staff.", "icons": [ { "src": "assets/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "assets/icon-512.png", "sizes": "512x512", "type": "image/png" } ], "vendor": { "id": "acme-corp", "name": "Acme Corporation", "contact": "miniapps@acme.example", "privacy_policy": "https://acme.example/privacy" }, "distribution": { "channel": "tenant-private", "tenants": ["acme-corp"] }, "entry": "index.html", "pages": [ { "path": "/", "title": "Approvals" }, { "path": "/orders/:order_id", "title": "Purchase order" }, { "path": "/expenses", "title": "Expenses" } ], "permissions": [ { "scope": "identity.basic", "purpose": "Shows your name on the approval you are about to sign." }, { "scope": "camera", "purpose": "Photographs a paper receipt so you do not have to type it in." } ], "network": { "connect": ["erp.acme.example", "*.cdn.acme.example"] }, "auth": { "mode": "sollar-exchange", "audience": "acme-erp-backend", "scopes": ["erp.read", "erp.approve"] }, "actions": [ { "name": "list_pending_approvals", "title": "List pending approvals", "description": "Returns the purchase orders awaiting approval by the current user. Never returns another user's queue.", "input_schema": { "type": "object", "properties": { "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } } }, "output_schema": { "type": "object", "properties": { "orders": { "type": "array", "items": { "$ref": "#/$defs/order" } } } }, "annotations": { "read_only": true, "destructive": false, "idempotent": true, "open_world": false }, "confirm": "never" }, { "name": "approve_purchase_order", "title": "Approve a purchase order", "description": "Approves one purchase order on behalf of the current user. This is final: an approved order is released to the vendor and cannot be revoked from within this mini app.", "input_schema": { "type": "object", "properties": { "purchase_order_id": { "type": "string" }, "note": { "type": "string", "maxLength": 500 } }, "required": ["purchase_order_id"] }, "annotations": { "read_only": false, "destructive": true, "idempotent": true, "open_world": false }, "confirm": "always" } ], "capabilities": { "offline": true, "background_refresh": false }, "content_rating": { "system": "sollar", "value": "general" }, "locales": ["en", "pt-BR", "zh-Hans"]}Identity
Section titled “Identity”| Field | Type | Required | Notes |
|---|---|---|---|
manifest_version | integer | yes | 1. A future version bumps this; the runtime refuses unknown values. |
id | string | yes | Reverse-DNS, immutable for the life of the app. Also derived from the signing key — see Package format. |
name | string | yes | 1–48 characters. |
short_name | string | no | ≤ 12 characters, for the home grid. Falls back to name. |
version | string | yes | Semantic version. Must increase monotonically; the store rejects a re-used or lowered version. |
description | string | yes | 1–300 characters. Shown in the store and in the install prompt. |
icons | array | yes | At least 192x192 and 512x512 PNG, packaged, not remote. |
locales | array | no | BCP 47 tags. First entry is the default. |
vendor
Section titled “vendor”| Field | Required | Notes |
|---|---|---|
id | yes | Stable vendor identifier. Two mini apps sharing a vendor.id may share a vendor_id identity — with explicit user consent only. |
name | yes | Legal or trading name, shown on the install prompt. |
contact | yes | A monitored address. Guideline 4.7.1 requires timely response to reports; this is where they go. |
privacy_policy | conditional | Required if any permission is declared. |
distribution
Section titled “distribution”Exactly one channel. Declaring both is a validation error, not a warning — channels with different review rules cannot share an artifact.
"distribution": { "channel": "tenant-private", "tenants": ["acme-corp", "acme-eu"] }"distribution": { "channel": "store" }channel | Requires | Sollar review |
|---|---|---|
tenant-private | tenants — a non-empty list of tenant slugs | None |
store | Verified entity on the developer account | Required |
Declares the routes the mini app answers. Used to render deep links, to populate the store listing, and to let the host restore a user to where they were.
{ "path": "/orders/:order_id", "title": "Purchase order" }Path parameters use :name. A route not declared here still renders — routing is yours — but it
cannot be deep-linked to and does not appear in the index required by Apple’s Guideline 4.7.4.
permissions
Section titled “permissions”Every entry needs a purpose string. This is not decoration: it is what the user reads in the
prompt, and it is what a reviewer holds you to.
{ "scope": "camera", "purpose": "Photographs a paper receipt so you do not have to type it in." }A purpose that does not plausibly match the app’s declared function is a rejection reason. “For
app functionality” is not a purpose.
| Scope | Grants |
|---|---|
identity.basic | Display name and avatar of the current user |
identity.email | The user’s email address within the tenant |
room.context | Which room the mini app was opened from |
room.send | Sending a message as the user, after per-message confirmation |
files.read | Opening a file the user picks from Sollar storage |
files.write | Writing a file into Sollar storage, at the user’s direction |
notifications | Posting a notification through the host |
camera | Web getUserMedia video — the engine still prompts |
microphone | Web getUserMedia audio — the engine still prompts |
geolocation | Web Geolocation — the engine still prompts |
clipboard.write | Writing to the clipboard |
contacts.directory | Searching the tenant’s user directory (not the device address book) |
network
Section titled “network”An allowlist of hosts the mini app may reach. Enforced natively, in the request interceptor — not by Content Security Policy, which a page can rewrite at runtime.
"network": { "connect": ["erp.acme.example", "*.cdn.acme.example"] }- Hostnames only. Scheme is always
https. Bare IP addresses are rejected. - One leading wildcard label is allowed (
*.cdn.acme.example);*alone is not. - Changing this list requires a new version and a new gate. That is the point.
One of three modes.
"auth": { "mode": "sollar-exchange", "audience": "acme-erp-backend", "scopes": ["erp.read"] }mode | For | Additional fields |
|---|---|---|
sollar-exchange | Backends that trust Sollar as the identity provider | audience (required), scopes |
external-oauth | Systems with their own IdP | issuer, client_id, scopes |
none | Mini apps with no backend identity | — |
actions
Section titled “actions”The agent tool registry. Each entry is simultaneously a tool an AI agent may call and an affordance the host can surface to a human.
| Field | Required | Notes |
|---|---|---|
name | yes | snake_case. Exposed to agents as <mini_app_id>__<name>. |
title | yes | Human-readable, shown in the confirmation prompt. |
description | yes | Written for the agent. See Agent actions for what makes one work. |
input_schema | yes | JSON Schema. Validated before your handler runs. |
output_schema | no | JSON Schema. Validated before the result reaches the agent. |
annotations | yes | All four booleans, explicitly. |
confirm | yes | never, if_destructive or always. |
annotations
Section titled “annotations”All four are required. Omitting one is a validation error — the point is a deliberate answer, not a default.
| Annotation | Question it answers |
|---|---|
read_only | Does this change any state? |
destructive | Is the change hard or impossible to undo? |
idempotent | Does calling it twice with the same input differ from calling it once? |
open_world | Does it reach systems outside this mini app’s control? |
These are declarations, not guarantees. Sollar cannot verify that an action marked read_only
really is. What it can do — and MCP cannot — is make the declaration part of a signed, reviewed
artifact that is attributable to a key and cannot be silently changed after a user has trusted it.
confirm
Section titled “confirm”| Value | Behaviour |
|---|---|
never | Runs without asking. Only acceptable with read_only: true. |
if_destructive | Resolves to always when annotations.destructive is true. |
always | Always asks a human first. |
The manifest sets the floor. A tenant policy or a user preference can tighten it; nothing can loosen it.
capabilities
Section titled “capabilities”| Field | Default | Notes |
|---|---|---|
offline | false | The app declares it functions without network. Enables offline launch. |
background_refresh | false | Planned. Reserved; currently rejected if true. |
Validation
Section titled “Validation”sollar validateRuns the same schema the submission gate runs. The canonical schema is published at
manifest.schema.json (JSON Schema 2020-12) — point your editor at
it and get completion and inline errors while you type.