# Manifest

> manifest.json field by field — identity, pages, permissions, network allowlist, auth and the actions an AI agent can call.

Source: https://miniapp.sollar.com/reference/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

```json title="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"]
}
```

## 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](/reference/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`

| 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`

Exactly one channel. Declaring both is a validation error, not a warning — channels with different
review rules cannot share an artifact.

```json
"distribution": { "channel": "tenant-private", "tenants": ["acme-corp", "acme-eu"] }
```

```json
"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 |

## `pages`

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.

```json
{ "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`

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.

```json
{ "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) |

> **NOTE**
Permission granted to the Sollar app does **not** flow to a mini app. Apple's Guideline 4.7.3
requires this, and Sollar enforces it whether or not the app is on iOS. Each mini app asks for its
own, and the user can revoke it at any time.

## `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.

```json
"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.

## `auth`

One of three modes.

```json
"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 | — |

> **DANGER**
`external-oauth` opens `ASWebAuthenticationSession` on iOS or Custom Tabs on Android — never the
runtime's WebView. RFC 8252 §8.12 forbids embedded user-agents in native-app OAuth flows, and the
reason is direct: inside a WebView the user cannot verify the URL or the certificate, and the host
is technically able to read the password. There is no configuration that changes this.

## `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](/guides/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`

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`

| 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`

| 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

```sh
sollar validate
```

Runs the same schema the submission gate runs. The canonical schema is published at
[`manifest.schema.json`](/schemas/manifest.schema.json) (JSON Schema 2020-12) — point your editor at
it and get completion and inline errors while you type.
