Skip to content

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.

manifest.json
{
"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"]
}
FieldTypeRequiredNotes
manifest_versionintegeryes1. A future version bumps this; the runtime refuses unknown values.
idstringyesReverse-DNS, immutable for the life of the app. Also derived from the signing key — see Package format.
namestringyes1–48 characters.
short_namestringno≤ 12 characters, for the home grid. Falls back to name.
versionstringyesSemantic version. Must increase monotonically; the store rejects a re-used or lowered version.
descriptionstringyes1–300 characters. Shown in the store and in the install prompt.
iconsarrayyesAt least 192x192 and 512x512 PNG, packaged, not remote.
localesarraynoBCP 47 tags. First entry is the default.
FieldRequiredNotes
idyesStable vendor identifier. Two mini apps sharing a vendor.id may share a vendor_id identity — with explicit user consent only.
nameyesLegal or trading name, shown on the install prompt.
contactyesA monitored address. Guideline 4.7.1 requires timely response to reports; this is where they go.
privacy_policyconditionalRequired if any permission is declared.

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" }
channelRequiresSollar review
tenant-privatetenants — a non-empty list of tenant slugsNone
storeVerified entity on the developer accountRequired

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.

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.

ScopeGrants
identity.basicDisplay name and avatar of the current user
identity.emailThe user’s email address within the tenant
room.contextWhich room the mini app was opened from
room.sendSending a message as the user, after per-message confirmation
files.readOpening a file the user picks from Sollar storage
files.writeWriting a file into Sollar storage, at the user’s direction
notificationsPosting a notification through the host
cameraWeb getUserMedia video — the engine still prompts
microphoneWeb getUserMedia audio — the engine still prompts
geolocationWeb Geolocation — the engine still prompts
clipboard.writeWriting to the clipboard
contacts.directorySearching the tenant’s user directory (not the device address book)

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"] }
modeForAdditional fields
sollar-exchangeBackends that trust Sollar as the identity provideraudience (required), scopes
external-oauthSystems with their own IdPissuer, client_id, scopes
noneMini apps with no backend identity

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.

FieldRequiredNotes
nameyessnake_case. Exposed to agents as <mini_app_id>__<name>.
titleyesHuman-readable, shown in the confirmation prompt.
descriptionyesWritten for the agent. See Agent actions for what makes one work.
input_schemayesJSON Schema. Validated before your handler runs.
output_schemanoJSON Schema. Validated before the result reaches the agent.
annotationsyesAll four booleans, explicitly.
confirmyesnever, if_destructive or always.

All four are required. Omitting one is a validation error — the point is a deliberate answer, not a default.

AnnotationQuestion it answers
read_onlyDoes this change any state?
destructiveIs the change hard or impossible to undo?
idempotentDoes calling it twice with the same input differ from calling it once?
open_worldDoes 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.

ValueBehaviour
neverRuns without asking. Only acceptable with read_only: true.
if_destructiveResolves to always when annotations.destructive is true.
alwaysAlways asks a human first.

The manifest sets the floor. A tenant policy or a user preference can tighten it; nothing can loosen it.

FieldDefaultNotes
offlinefalseThe app declares it functions without network. Enables offline launch.
background_refreshfalsePlanned. Reserved; currently rejected if true.
Terminal window
sollar validate

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