# SheevChat Action Library Manifest Wiki

This page documents the current SheevChat action library manifest format. It is written for humans and for AI agents that are asked to create importable `.sheevactions` or `.sheevactionszip` libraries from an application's API documentation.

The goal of an action library is to describe an external app's actions in JSON so SheevChat can expose them inside Custom Commands, Automations, Timers, and SheevPad without custom code for every app.

## Quick Rules For AI Agents

When creating a manifest, output only valid JSON. Do not output Markdown, comments, trailing commas, or invented fields.

Use only SheevChat's schema, not a generic action schema. If an API feature cannot fit the supported schema, explain what runtime support would be needed instead of inventing new manifest keys.

The most common AI mistakes are:

- Making `sources` an array. It must be an object keyed by source id.
- Placing `rows`, `value`, `label`, or `extra` directly on a source. They must be inside `source.map`.
- Omitting `execute.type`.
- Using nested placeholders like `{scene.id}`. SheevChat placeholders are flat, like `{scene_id}`.
- Forgetting that WebSocket channel responses are wrapped as `{ "channel": "...", "payload": ... }`.
- Putting instance discovery settings beside `discovery` instead of inside it.
- Inventing field types. The only accepted field types are `text`, `textarea`, `number`, `select`, `dynamic_select`, `checkbox`, `color`, `file`, `hotkey`, `hidden`, and `info`. Use `checkbox`, not `toggle`, for a boolean input.
- Writing `%USERPROFILE%`, `$HOME`, `~`, an absolute path, or a drive letter in `discovery.directory`. Use a home-relative path such as `.veadotube/instances`.
- Reusing a source containing multiple resource types without filtering it for the action. Use `map.where`, or separate typed sources.
- Assuming an API id is globally unique. If ids can overlap across types, use a collision-safe option value such as `{type}:{id}`.
- Creating write-only packs when the API also supports useful reads. Add query actions with typed sequence outputs when later actions can reuse the returned state.

## File Type

Action libraries can be imported in either of two formats.

### Plain Manifest

A plain manifest is a single JSON file. The recommended extension is:

```text
.sheevactions
```

Internally, SheevChat installs the imported manifest as:

```text
action-libraries/installed/<library-id>/manifest.json
```

### Packaged Library

A packaged library is a zip file with the recommended extension:

```text
.sheevactionszip
```

The package must contain a manifest at the root of the zip. Use either `manifest.json` or one root-level `.sheevactions` file. If both exist, `manifest.json` takes priority.

```text
my-library.sheevactionszip
  manifest.json
  brand-icon.png
```

This is also valid:

```text
my-library.sheevactionszip
  my-library.sheevactions
  brand-icon.png
```

Packages may include a brand icon. SheevChat selects the first compatible image file found in the zip package and stores that relative path in the installed manifest's `icon` field. The file does not need to be named `icon`.

Compatible icon extensions:

```text
.png
.jpg
.jpeg
.webp
.svg
.gif
```

Safe package rules:

- `manifest.json` or one `.sheevactions` file must be at the package root.
- If no `manifest.json` exists, do not include multiple root `.sheevactions` files.
- Paths must stay inside the package. Do not use absolute paths, drive letters, or `../`.
- Keep packages small. Icons should be optimized for UI display.
- Existing `.sheevactions` plain JSON imports continue to work.

## Current Schema Version

```json
"schemaVersion": 1
```

Only schema version `1` is accepted.

## Top-Level Manifest Shape

```json
{
  "schemaVersion": 1,
  "id": "example-app",
  "name": "Example App",
  "version": "1.0.0",
  "publisher": "Example Publisher",
  "category": "Example Controls",
  "description": "Adds Example App actions to SheevChat.",
  "homepage": "https://example.com/docs",
  "icon": "",
  "trust": "third-party",
  "permissions": ["local_websocket"],
  "compatibility": {},
  "connections": [],
  "sources": {},
  "actions": []
}
```

## Top-Level Static Fields

| Field | Required | Type | Accepted / Notes |
|---|---:|---|---|
| `schemaVersion` | yes | number | Must be `1`. |
| `id` | yes | string | Lowercase letters, numbers, hyphens, or underscores. 3-64 chars. Must start and end with a letter or number. |
| `name` | yes | string | User-facing library name. Max 120 chars. |
| `version` | yes | string | Semantic-ish version such as `1.0.0`, `1.2`, or `1.0.0-beta.1`. |
| `publisher` | no | string | Displayed in action category as `Category by Publisher`. Max 120 chars. |
| `category` | no | string | User-facing action category. Defaults to `Community`. Max 80 chars. |
| `description` | no | string | Library summary. Max 700 chars. |
| `homepage` | no | string | Documentation/homepage URL. Max 500 chars. |
| `icon` | no | string | Relative packaged icon path. Max 500 chars. For `.sheevactionszip`, SheevChat can auto-fill this with the first compatible image file found in the package. |
| `permissions` | no | array | See permissions below. |
| `compatibility` | no | object | Freeform metadata. Currently stored, not enforced. |
| `trust` | no | string | Freeform label such as `local`, `third-party`, `community`, or `example-pack`. |
| `connections` | no | array | Connection definitions. |
| `sources` | no | object | Dynamic option sources keyed by source id. Must be an object, not an array. |
| `actions` | yes | array | Must contain at least one action. |

## ID Rules

Manifest ids, connection ids, source ids, action ids, field keys, and extra keys are normalized with the same id rules:

```text
^[a-z0-9][a-z0-9_-]{1,62}[a-z0-9]$
```

Good:

```text
vtube-studio
trigger_hotkey
state_node_id
local
```

Bad:

```text
VTubeStudio
trigger.hotkey
state node
_hidden
id!
```

## Action Category Display

SheevChat displays imported action categories as:

```text
<category> by <publisher>
```

Example:

```json
{
  "category": "Avatar Controls",
  "publisher": "ExampleApp"
}
```

Displays as:

```text
Avatar Controls by ExampleApp
```

Use exactly one top-level `category`. Do not create per-action categories.

## Permissions

Accepted permission values:

| Permission | Meaning |
|---|---|
| `local_http` | Manifest may call local HTTP APIs. |
| `local_websocket` | Manifest may call local WebSocket APIs. |
| `local_ipc` | Manifest may call approved local IPC/RPC transports such as hardened named-pipe protocol presets. |
| `local_udp` | Manifest may send bounded UDP request/response packets to localhost or private LAN targets. |
| `local_tcp` | Manifest may send bounded TCP request/response messages to localhost or private LAN targets. |
| `local_mqtt` | Manifest may publish bounded MQTT/MQTTS messages to declared topics on localhost/private LAN brokers and manually subscribe to declared topics for diagnostics. |
| `local_discovery` | Manifest may perform approved LAN/local service discovery such as mDNS/DNS-SD. |
| `oauth` | Manifest may declare a safe OAuth 2.0 authorization-code-with-PKCE profile. Loopback PKCE login and token storage are supported for public clients. |
| `hotkey` | Manifest may send bounded Windows keystroke sequences through SheevChat's macro sender. |
| `launch_application` | Manifest may launch a user-selected local app/file through SheevChat's bounded launcher. |
| `read_file` | Manifest may read small UTF-8/JSON files from its own SheevChat-managed library storage folder. |
| `write_file` | Manifest may write small UTF-8/JSON files inside its own SheevChat-managed library storage folder. |
| `sheevchat_command` | Manifest may trigger a SheevChat command. |
| `remote_network` | Allows non-local HTTP/WebSocket hosts. Without this, remote URLs are rejected. |

For current third-party app packs, prefer:

```json
"permissions": ["local_websocket"]
```

or:

```json
"permissions": ["local_http"]
```

Only use `remote_network` when the manifest intentionally calls an internet host.

Remote/cloud HTTP packs should also declare a connection-level `remote.allowedHosts` list so SheevChat can pin requests to the expected API host. See [Remote HTTP Policy](#remote-http-policy).

## Connections

Connections describe how SheevChat talks to the target app.

```json
"connections": [
  {
    "id": "local",
    "label": "Example App",
    "type": "local_websocket",
    "defaultUrl": "ws://127.0.0.1:12345",
    "test": {
      "url": "ws://127.0.0.1:12345"
    }
  }
]
```

### Accepted Connection Types

| Type | Current behavior |
|---|---|
| `none` | No app connection required. Status defaults to connected. |
| `local_http` | Uses `fetch` against a local HTTP API. |
| `local_websocket` | Opens a WebSocket, sends a message, optionally waits for response. |
| `local_ipc` | Opens a validated local IPC endpoint and uses a hardened protocol preset. |
| `local_udp` | Sends bounded UDP packets to local/LAN targets. |
| `local_tcp` | Opens a short-lived TCP socket to a local/LAN target, sends a bounded message, optionally waits for a bounded response. |
| `local_mqtt` | Connects to a local/LAN MQTT broker for bounded publish actions and manual subscriptions. |
| `oauth` | OAuth profile connection. Status defaults to needs login until the user signs in through the loopback PKCE flow. |
| `hotkey` | No app connection required. Status defaults to connected. Used with `execute.type: "hotkey"`. |

### Connection Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `id` | yes | string | Stable connection id. Usually `local`. |
| `label` | no | string | User-facing connection label. Defaults to id. |
| `type` | no | string | Defaults to `none`. |
| `defaultUrl` | no | string | Base URL such as `http://127.0.0.1:3000` or `ws://127.0.0.1:8001`. |
| `fallbackUrls` / `urls` | no | array | Ordered fallback base URLs. SheevChat tries `defaultUrl` first, then each fallback. Useful for apps that expose one of several documented local ports. |
| `provider` | no | string | Metadata for OAuth/provider naming. |
| `scopes` | no | array | Metadata for OAuth scopes. |
| `discovery` | no | object | Supports generic instance file discovery. |
| `auth` | no | object | Describes local app authentication for APIs that require a token or session handshake. |
| `remote` | no | object | Remote/cloud HTTP safety policy. Use with `remote_network` for internet APIs. |
| `signing` | no | object | Declarative request signing preset for HTTP APIs that require HMAC signatures. Secrets must come from saved credentials. |
| `preflight` | no | object | First-message or setup request sent before source dropdowns/actions. Use this for app hello/register messages that do not return a reusable token. |
| `protocol` | no | object | Required for `local_ipc`. Selects a hardened IPC protocol preset such as `discord_rpc`. |
| `tcp` | no | object | Optional `local_tcp` defaults such as framing, response framing, timeout, and payload/response byte caps. |
| `test` | no | object | Connection test settings. |

## OAuth 2.0 Profiles

Use `connection.type: "oauth"` when an imported cloud pack needs a user authorization flow instead of a pasted API key. The manifest must declare the `oauth` permission and include a `connection.oauth` profile. SheevChat supports desktop authorization-code-with-PKCE login through a loopback callback, exchanges the returned code for tokens, and stores tokens locally in the same user-config auth state used by other imported-library credentials.

OAuth profiles are for desktop authorization-code-with-PKCE flows. Public clients use `tokenAuthMethod: "none"`. Confidential clients use `client_secret_post` or `client_secret_basic` with a user-entered local credential. Do not place client secrets, access tokens, refresh tokens, authorization headers, or pre-issued bearer tokens in the manifest.

```json
{
  "permissions": ["oauth"],
  "connections": [
    {
      "id": "cloud",
      "label": "Example Cloud",
      "type": "oauth",
      "provider": "example",
      "oauth": {
        "flow": "authorization_code_pkce",
        "authorizationUrl": "https://auth.example.com/oauth/authorize",
        "tokenUrl": "https://auth.example.com/oauth/token",
        "revokeUrl": "https://auth.example.com/oauth/revoke",
        "clientId": "public-desktop-client-id",
        "scopes": ["devices.read", "devices.write"],
        "redirectMode": "loopback",
        "pkce": true,
        "tokenAuthMethod": "none"
      }
    }
  ]
}
```

### OAuth Object Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `flow` / `type` | no | string | Only `authorization_code_pkce` is accepted. Hyphen form is normalized. |
| `authorizationUrl` / `authorizeUrl` / `authUrl` | yes | string | HTTPS authorize URL. Embedded credentials are rejected. |
| `tokenUrl` | yes | string | HTTPS token URL. |
| `revokeUrl` / `revocationUrl` | no | string | HTTPS revocation URL. |
| `clientId` / `client_id` | yes | string | Public client id for desktop/PKCE use. |
| `scopes` / `scope` | no | array or string | Arrays are preferred. Space-separated strings are split into scopes. |
| `redirectMode` / `redirect` | no | string | `loopback` or `relay`. Defaults to `loopback`. |
| `pkce` | no | boolean | Defaults to `true`. |
| `tokenAuthMethod` / `authMethod` | no | string | `none`, `client_secret_post`, or `client_secret_basic`. Secret methods require a matching locally saved credential. |
| `clientSecretKey` / `clientSecretCredential` / `client_secret_key` | no | string | Credential key to use for `client_secret_post` or `client_secret_basic`. Defaults to `client_secret`. The credential value must be entered locally by the user, never embedded in the manifest. |
| `extraAuthorizeParams` / `authorizeParams` | no | object | Scalar extra authorize parameters such as `audience` or `prompt`. Secrets/tokens are rejected. |
| `extraTokenParams` / `tokenParams` | no | object | Scalar extra token parameters. Secrets/tokens are rejected. |
| `extraRevokeParams` / `revokeParams` / `revocationParams` | no | object | Scalar extra revoke parameters. Secrets/tokens are rejected. |

When the user clicks Authenticate for an OAuth connection, SheevChat opens the provider authorize URL with:

- `response_type=code`
- `client_id`
- `redirect_uri=http://localhost:{current-port}/api/action-libraries/oauth/callback`
- `state`
- `scope`
- `code_challenge`
- `code_challenge_method=S256`
- optional `audience`, `resource`, `prompt`, and `extraAuthorizeParams`

After callback, SheevChat exchanges the code at `tokenUrl` with `grant_type=authorization_code`, `client_id`, `redirect_uri`, `code_verifier`, and any `extraTokenParams`. Stored values are exposed to later runtime templates as `{auth.access_token}`, `{auth.refresh_token}`, `{auth.token_type}`, `{auth.scope}`, and `{auth.expires_at}`.

For providers that require a confidential OAuth client, declare a local credential and choose the provider's token auth method:

```json
{
  "permissions": ["oauth", "remote_network"],
  "connections": [
    {
      "id": "cloud",
      "type": "oauth",
      "remote": { "allowedHosts": ["api.example.com"] },
      "credentials": [
        { "key": "client_secret", "label": "Client Secret", "type": "password" }
      ],
      "oauth": {
        "authorizationUrl": "https://auth.example.com/authorize",
        "tokenUrl": "https://auth.example.com/token",
        "clientId": "desktop-client-id",
        "tokenAuthMethod": "client_secret_basic"
      }
    }
  ]
}
```

`client_secret_post` sends `client_id` and `client_secret` in the form body. `client_secret_basic` sends them in an HTTP Basic Authorization header. The same method is used for authorization-code exchange, refresh, and revoke calls.

`oauth_api` actions reference an OAuth connection and run HTTP requests with the stored access token injected as an `Authorization` header. They use the same request shape as `local_http` actions: `method`, `path`/`url`, `headers`, `query`, `body`, `json`, `bodyEncoding`, `responseMap`, `responseNamespace`, and `responseTemplate`.

```json
{
  "id": "set_state",
  "label": "Set State",
  "connectionId": "cloud",
  "execute": {
    "type": "oauth_api",
    "connectionId": "cloud",
    "method": "POST",
    "path": "/v1/state",
    "body": { "enabled": true }
  }
}
```

OAuth request rules:

- The user must authenticate the OAuth connection first.
- Runtime token exchange supports public PKCE clients with `tokenAuthMethod: "none"` plus confidential-client `client_secret_post` and `client_secret_basic` methods when the secret is saved as a local credential.
- `oauth_api` requests automatically add `Authorization: Bearer {auth.access_token}` unless the manifest explicitly provides an `Authorization` header.
- Remote/cloud API hosts still use the normal remote HTTP safety policy. Include `remote_network` and `connection.remote.allowedHosts` when calling internet APIs.
- If the stored access token is expired and `{auth.refresh_token}` exists, SheevChat refreshes the token before running OAuth sources/actions. If refresh fails or no refresh token exists, the connection returns to `needs login`.
- If the provider returns no replacement refresh token during refresh, SheevChat preserves the existing stored refresh token.
- Clearing OAuth auth deletes SheevChat's local tokens. If `revokeUrl` is present, SheevChat first makes a best-effort revocation request with `token`, `token_type_hint`, `client_id`, and any `extraRevokeParams`; local cleanup still completes if the provider revoke call fails.

OAuth dynamic dropdown sources are supported with `source.type: "connection_request"` against an OAuth connection. This lets authenticated packs populate cloud-side lists before running an action.

Do not fake OAuth by asking users to paste access tokens into normal fields unless the provider officially documents API-key-style personal tokens.

## Remote HTTP Policy

Use `connection.remote` for cloud APIs and internet-hosted webhooks. This makes remote action packs first-class and reviewable instead of relying on arbitrary full URLs inside individual actions.

Remote HTTP rules:

- The top-level manifest must include `remote_network`.
- Remote HTTP URLs must use HTTPS by default.
- Remote URLs cannot contain embedded credentials such as `https://user:pass@example.com`.
- Remote hosts must match the connection's `remote.allowedHosts` or the remote host in `defaultUrl` / `fallbackUrls`.
- Redirects are followed manually and capped. Cross-host redirects are rejected so credential-bearing headers are not forwarded to another host.
- Responses are size-limited before parsing.
- HTTP `429` responses are surfaced as rate-limit feedback, including `Retry-After` when present.

```json
{
  "permissions": ["local_http", "remote_network"],
  "connections": [
    {
      "id": "govee",
      "label": "Govee Cloud",
      "type": "local_http",
      "defaultUrl": "https://openapi.api.govee.com",
      "remote": {
        "allowedHosts": ["openapi.api.govee.com"],
        "maxResponseBytes": 1048576,
        "maxRedirects": 2
      },
      "credentials": [
        {
          "key": "govee_api_key",
          "label": "Govee API Key",
          "type": "password",
          "required": true
        }
      ],
      "test": {
        "method": "GET",
        "path": "/router/api/v1/user/devices",
        "headers": {
          "Govee-API-Key": "{auth.govee_api_key}"
        }
      }
    }
  ]
}
```

### Remote Object Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `allowedHosts` | recommended | array | Exact remote hostnames, such as `api.example.com`. Do not include protocol, paths, query strings, ports, or credentials. Alias: `hosts`. |
| `allowInsecureHttp` | no | boolean | Defaults to `false`. Only use `true` for a documented local/private API that cannot use TLS. Marketplace cloud packs should not use it. |
| `maxResponseBytes` | no | number | Maximum response body size. Defaults to 1 MB and is capped at 5 MB. |
| `maxRedirects` | no | number | Redirect limit. Defaults to 3 and is capped at 5. |

For local APIs, do not add `remote_network` or `connection.remote`.

## Local HTTP Connections

Use `local_http` for REST-like APIs.

Imported action libraries intentionally support only interactive HTTP methods:

```text
GET
POST
PUT
```

`PATCH`, `DELETE`, `HEAD`, and other methods are rejected. SheevChat action libraries should query or control apps, not remove records or perform arbitrary destructive mutation.

```json
{
  "id": "local",
  "label": "Example REST API",
  "type": "local_http",
  "defaultUrl": "http://127.0.0.1:4567",
  "test": {
    "url": "/health",
    "method": "GET"
  }
}
```

Relative URLs are resolved against `defaultUrl`.

### HTTP Body Encoding

HTTP requests default to JSON bodies. For token endpoints, webhook-style APIs, and desktop apps that expect non-JSON payloads, add `bodyEncoding` to a non-GET request.

Supported values:

| Value | Body behavior | Default content type |
|---|---|---|
| `json` | Object bodies are sent as JSON. String bodies pass through as-is. | `application/json` |
| `form` | Object bodies are sent as URL-encoded form data. Array values repeat the same key. | `application/x-www-form-urlencoded` |
| `form_urlencoded` | Alias for `form`. | `application/x-www-form-urlencoded` |
| `urlencoded` | Alias for `form`. | `application/x-www-form-urlencoded` |
| `text` | Body is sent as plain text. | `text/plain; charset=utf-8` |
| `raw` | Alias for `text`. | `text/plain; charset=utf-8` |

If a request already declares a `Content-Type` header, SheevChat preserves it. `bodyEncoding` is ignored for `GET` requests and local import diagnostics will warn about that mismatch.

```json
{
  "method": "POST",
  "url": "/oauth/token",
  "bodyEncoding": "form",
  "body": {
    "grant_type": "refresh_token",
    "refresh_token": "{auth.refresh_token}"
  }
}
```

## Local UDP Connections

Use `local_udp` for local or LAN device APIs that communicate through UDP request/response packets. This runtime is intentionally bounded for safety:

- The manifest must include `local_udp`.
- Connection URLs must use `udp://host:port`.
- Targets must be localhost, RFC1918 private LAN addresses, or `.local` hostnames.
- Broadcast and multicast targets require explicit opt-in with `udp.allowBroadcast` or `udp.allowMulticast`.
- Packets and responses are size-limited.
- Retries are capped.
- Sources/actions may parse UDP responses into text, JSON, hex, or base64.

```json
{
  "permissions": ["local_udp"],
  "connections": [
    {
      "id": "local",
      "label": "LAN Device",
      "type": "local_udp",
      "defaultUrl": "udp://192.168.1.50:4001",
      "udp": {
        "retries": 1,
        "timeoutMs": 2500,
        "maxPacketBytes": 8192,
        "maxResponseBytes": 8192
      },
      "test": {
        "payload": { "event": "ping" },
        "encoding": "json",
        "responseEncoding": "json"
      }
    }
  ]
}
```

### UDP Connection Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `defaultUrl` | usually | string | `udp://host:port`. The host must be local/LAN. |
| `udp.retries` | no | number | Integer from 0 to 5. Defaults to 0. |
| `udp.timeoutMs` | no | number | Timeout from 250ms to 10000ms. Defaults to 2500ms. |
| `udp.maxPacketBytes` | no | number | Outgoing packet cap from 64 to 65507 bytes. Defaults to 8192. |
| `udp.maxResponseBytes` | no | number | Incoming response cap from 64 to 65507 bytes. Defaults to 65507. |
| `udp.allowBroadcast` | no | boolean | Required for broadcast targets such as `udp://255.255.255.255:4001` or subnet broadcasts ending in `.255`. Defaults to `false`. |
| `udp.allowMulticast` | no | boolean | Required for multicast targets in `224.0.0.0/4`. Defaults to `false`. |
| `udp.multicastTtl` | no | number | Multicast TTL from 1 to 8. Defaults to 1. Keep this low for LAN discovery. |

### UDP Request Fields

UDP requests can appear in `connection.test`, `sources.<id>.request`, or `actions[].execute`.

| Field | Required | Type | Notes |
|---|---:|---|---|
| `url` / `path` | no | string | Optional `udp://host:port` override. Most packs use the connection `defaultUrl`. |
| `packet` / `message` / `payload` / `body` | yes | any | Packet contents. Objects are normally encoded as JSON. |
| `encoding` / `payloadEncoding` | no | string | `json`, `text`, `hex`, or `base64`. Defaults to `json` for objects and `text` for strings. |
| `responseEncoding` / `parse` | no | string | `json`, `text`, `hex`, or `base64`. Defaults to `json`. |
| `awaitResponse` | no | boolean | Defaults to `true`. Set `false` for fire-and-forget controls. Query actions must wait for a response. |
| `retries` | no | number | Overrides connection retry count for this request. 0-5. |
| `timeoutMs` | no | number | Overrides request timeout. 250-10000ms. |
| `maxPacketBytes` | no | number | Overrides outgoing packet cap. |
| `maxResponseBytes` | no | number | Overrides incoming response cap. |
| `responseFromAny` | no | boolean | Defaults to `false`. Leave off unless the protocol documents replies from a different address/port. |

### UDP Broadcast And Multicast Discovery

Use broadcast or multicast only for documented LAN discovery protocols. The connection must still use `local_udp`, and the target must be a UDP URL with an explicit port.

```json
{
  "permissions": ["local_udp"],
  "connections": [
    {
      "id": "discovery",
      "type": "local_udp",
      "defaultUrl": "udp://255.255.255.255:4001",
      "udp": {
        "allowBroadcast": true,
        "timeoutMs": 1500,
        "retries": 1,
        "maxPacketBytes": 512,
        "maxResponseBytes": 8192
      },
      "test": {
        "message": "discover",
        "encoding": "text",
        "responseEncoding": "json",
        "responseFromAny": true
      }
    }
  ]
}
```

For multicast:

```json
{
  "defaultUrl": "udp://239.255.0.1:4001",
  "udp": {
    "allowMulticast": true,
    "multicastTtl": 1
  }
}
```

Discovery responses often come from a device-specific IP/port rather than the discovery destination. In those cases, set `responseFromAny: true` on the specific test/source/action request that expects discovery replies. Do not set it on ordinary device-control actions unless the protocol requires it.

Example action:

```json
{
  "id": "toggle_power",
  "label": "Toggle Power",
  "connectionId": "local",
  "fields": [],
  "execute": {
    "type": "local_udp",
    "connectionId": "local",
    "payload": { "cmd": "toggle" },
    "encoding": "json",
    "awaitResponse": false
  }
}
```

## Local MQTT Connections

Use `local_mqtt` for local or LAN MQTT brokers, such as smart-home bridges and IoT tools that expose a documented MQTT control API. This runtime is intentionally bounded for safety:

- The manifest must include `local_mqtt`.
- Broker URLs must use `mqtt://` or `mqtts://`.
- Broker hosts must be localhost, RFC1918 private LAN addresses, or `.local` hostnames.
- Broker URLs cannot contain embedded credentials.
- Credentials must be declared with `connection.credentials` and referenced through `{auth.<key>}` placeholders.
- Publish and subscribe topics must be allowlisted in `connection.mqtt.topics`.
- Publish actions are fire-and-forget. MQTT query/request-response actions are not supported yet.
- Payloads are capped and can be encoded as JSON, text, hex, or base64.
- Retained publishes are disabled unless the connection explicitly sets `mqtt.allowRetain: true`.

```json
{
  "permissions": ["local_mqtt"],
  "connections": [
    {
      "id": "mqtt",
      "label": "Local MQTT Broker",
      "type": "local_mqtt",
      "defaultUrl": "mqtt://127.0.0.1:1883",
      "credentials": [
        {
          "key": "username",
          "label": "Username",
          "type": "text",
          "required": false
        },
        {
          "key": "password",
          "label": "Password",
          "type": "password",
          "required": false
        }
      ],
      "mqtt": {
        "clientId": "sheevchat-{library.id}",
        "username": "{auth.username}",
        "password": "{auth.password}",
        "topics": {
          "publish": ["lights/+/set"],
          "subscribe": ["lights/+/state"]
        },
        "qos": 0,
        "timeoutMs": 3000,
        "maxPayloadBytes": 65536
      }
    }
  ]
}
```

### MQTT Connection Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `defaultUrl` | yes | string | `mqtt://host:port` or `mqtts://host:port`. The host must be local/LAN. |
| `credentials` | no | array | Declare username/password/API-key fields here. Never embed credential values in `defaultUrl`. |
| `mqtt.clientId` | no | string | Client id sent to the broker. Supports placeholders such as `{library.id}`. |
| `mqtt.username` | no | string | Broker username. Usually a `{auth.username}` placeholder. |
| `mqtt.password` | no | string | Broker password/API key. Usually a `{auth.password}` placeholder. |
| `mqtt.clean` | no | boolean | Defaults to `true`. |
| `mqtt.keepalive` | no | number | Keepalive seconds from 0 to 120. |
| `mqtt.timeoutMs` | no | number | Connection timeout from 500ms to 30000ms. Defaults to 3000ms. |
| `mqtt.protocolVersion` | no | number | `4` or `5`. Defaults to the MQTT library default. |
| `mqtt.rejectUnauthorized` | no | boolean | Defaults to `true`. Set `false` only for documented local self-signed brokers. |
| `mqtt.qos` | no | number | Default QoS from 0 to 2. |
| `mqtt.allowRetain` | no | boolean | Required before any action can publish retained messages. Defaults to `false`. |
| `mqtt.maxPayloadBytes` | no | number | Payload cap from 64 bytes to 1 MB. Defaults to 64 KB. |
| `mqtt.topics.publish` | yes for publish actions | array | Topic filters that publish actions may target. MQTT wildcards are allowed here as allowlist filters. |
| `mqtt.topics.subscribe` | yes for subscriptions | array | Topic filters that manual event subscriptions may listen to. |

### MQTT Publish Action

```json
{
  "id": "set_light",
  "label": "Set Light",
  "connectionId": "mqtt",
  "fields": [
    { "key": "light", "label": "Light", "type": "text" },
    { "key": "enabled", "label": "On", "type": "checkbox" }
  ],
  "execute": {
    "type": "local_mqtt",
    "connectionId": "mqtt",
    "topic": "lights/{light}/set",
    "payload": {
      "on": "{enabled}"
    },
    "encoding": "json",
    "qos": 0
  }
}
```

### MQTT Request Fields

MQTT publish requests can appear in `actions[].execute`. `connection.test` may connect to the broker, but it does not publish unless used by a future explicit test payload.

| Field | Required | Type | Notes |
|---|---:|---|---|
| `topic` / `publishTopic` | yes | string | Concrete publish topic. It cannot contain MQTT wildcards after placeholders resolve. |
| `payload` / `message` / `body` / `json` | no | any | Payload contents. Objects are normally encoded as JSON. |
| `encoding` / `payloadEncoding` | no | string | `json`, `text`, `hex`, or `base64`. Defaults to `json` for objects and `text` for strings. |
| `qos` | no | number | 0, 1, or 2. Defaults to `connection.mqtt.qos` or 0. |
| `retain` | no | boolean | Only honored when `connection.mqtt.allowRetain` is `true`. |
| `maxPayloadBytes` | no | number | Optional lower payload cap for this request. |

Do not set `query: true` or `awaitResponse: true` on MQTT publish actions. MQTT request/response patterns need a future bounded correlation design before they can be safely used as query outputs. For the same reason, `local_mqtt` does not support `sources.<id>.type: "connection_request"` dropdown sources yet.

## Local TCP Connections

Use `local_tcp` for local or LAN desktop/device APIs that communicate through plain TCP sockets. This is useful for simple line-delimited command protocols, local control bridges, and LAN tools that do not expose HTTP, WebSocket, UDP, MQTT, or a supported IPC preset.

The runtime is intentionally bounded:

- The manifest must include `local_tcp`.
- Connection URLs must use `tcp://host:port`.
- Targets must be localhost, RFC1918 private LAN addresses, or `.local` hostnames.
- TCP URLs cannot contain embedded credentials.
- Payloads and responses are size-limited.
- Requests can be raw bytes or line-framed messages.
- Payloads and responses can be encoded as JSON, text, hex, or base64.
- Query actions and dynamic dropdown sources are supported only when `awaitResponse` is true.

```json
{
  "permissions": ["local_tcp"],
  "connections": [
    {
      "id": "local",
      "label": "Local TCP App",
      "type": "local_tcp",
      "defaultUrl": "tcp://127.0.0.1:9000",
      "tcp": {
        "framing": "line",
        "responseFraming": "line",
        "timeoutMs": 3000,
        "maxPayloadBytes": 65536,
        "maxResponseBytes": 65536
      },
      "test": {
        "payload": { "event": "ping" },
        "encoding": "json",
        "responseEncoding": "json"
      }
    }
  ]
}
```

### TCP Connection Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `defaultUrl` | usually | string | `tcp://host:port`. The host must be local/LAN. |
| `fallbackUrls` / `urls` | no | array | Ordered `tcp://host:port` fallbacks. Useful for apps that bind one of several documented ports. |
| `tcp.timeoutMs` | no | number | Timeout from 250ms to 30000ms. Defaults to 3000ms. |
| `tcp.framing` | no | string | `raw` or `line`. Defaults to `raw`. `line` appends `\n` when the payload does not already end in a newline. |
| `tcp.responseFraming` | no | string | `raw` or `line`. Defaults to the request framing. `line` resolves after the first newline-delimited response frame. |
| `tcp.maxPayloadBytes` | no | number | Outgoing payload cap from 64 bytes to 1 MB. Defaults to 64 KB. |
| `tcp.maxResponseBytes` | no | number | Incoming response cap from 64 bytes to 1 MB. Defaults to 64 KB. |

### TCP Request Fields

TCP requests can appear in `connection.test`, `sources.<id>.request`, `eventSubscriptions[].subscribe`, or `actions[].execute`.

| Field | Required | Type | Notes |
|---|---:|---|---|
| `url` / `path` | no | string | Optional `tcp://host:port` override. Most packs use the connection `defaultUrl`. |
| `packet` / `message` / `payload` / `body` | yes | any | Message contents. Objects are normally encoded as JSON. |
| `encoding` / `payloadEncoding` | no | string | `json`, `text`, `hex`, or `base64`. Defaults to `json` for objects and `text` for strings. |
| `framing` | no | string | `raw` or `line`. Overrides `connection.tcp.framing`. |
| `responseEncoding` / `parse` | no | string | `json`, `text`, `hex`, or `base64`. Defaults to `json`. |
| `responseFraming` | no | string | `raw` or `line`. Overrides `connection.tcp.responseFraming`. |
| `awaitResponse` | no | boolean | Defaults to `true`. Set `false` for fire-and-forget controls. Query actions and dropdown sources must wait for a response. |
| `timeoutMs` | no | number | Overrides request timeout. 250-30000ms. |
| `maxPayloadBytes` | no | number | Overrides outgoing payload cap. |
| `maxResponseBytes` | no | number | Overrides incoming response cap. |

Example action:

```json
{
  "id": "set_mode",
  "label": "Set Mode",
  "connectionId": "local",
  "fields": [
    { "key": "mode", "label": "Mode", "type": "text" }
  ],
  "execute": {
    "type": "local_tcp",
    "connectionId": "local",
    "payload": { "event": "set_mode", "mode": "{mode}" },
    "encoding": "json",
    "framing": "line",
    "awaitResponse": false
  }
}
```

Example dynamic source:

```json
"sources": {
  "modes": {
    "type": "connection_request",
    "connectionId": "local",
    "request": {
      "payload": { "event": "list_modes" },
      "encoding": "json",
      "framing": "line",
      "responseEncoding": "json",
      "responseFraming": "line"
    },
    "map": {
      "root": "$.modes",
      "value": "$.id",
      "label": "$.name"
    }
  }
}
```

## Connection Preflight / Hello Messages

Use `connection.preflight` when a local HTTP or WebSocket API requires a setup message before normal requests, but does not have a token/session authentication flow. This is common for desktop app control APIs that require a client registration, hello message, or app key as the first WebSocket message.

Preflight is different from `connection.auth`:

- `auth` expects SheevChat to prove authentication succeeded through `statusMap`, `tokenMap`, or `sessionMap`.
- `preflight` sends setup messages before sources/actions and does not require an authenticated response.

### When To Use Preflight, Auth, Or Both

| API behavior | Use | Why |
|---|---|---|
| The app requires a hello/register/client-key message as the first WebSocket message, then normal commands work. | `connection.preflight` | There is no token to store and no authenticated boolean to prove. |
| The app returns a reusable token that must be saved locally. | `connection.auth.tokenRequest` + `tokenMap` | SheevChat maps and stores returned values as `{auth.token}` or `{auth.key}`. |
| The app returns a token and each new WebSocket session must authenticate with that token. | `connection.auth.tokenRequest` + `tokenMap` + `sessionRequest` + `sessionMap` | SheevChat stores the token, then sends session auth before sources/actions. |
| The app can report whether the current session is already authenticated. | `connection.auth.statusRequest` + `statusMap` | SheevChat can show authenticated without requesting a new token. |
| The app requires both a first-message hello and later token/session auth. | `connection.preflight` + `connection.auth` | Preflight runs first, auth/session requests run after it. |

Do not force a hello/register API into `connection.auth` unless the API actually returns an auth result that can be mapped to `authenticated`, `connected`, `ok`, or `success`.

### Fallback URLs / Multiple Local Ports

Use `fallbackUrls` when a desktop app may bind to one of several local ports. SheevChat tries the connection in this order:

1. `defaultUrl`
2. each URL in `fallbackUrls`

Fallback URLs are sufficient when the app uses a small documented port range. They do not replace endpoint-file discovery when the active port can be arbitrary. Some apps also require WebSocket handshake headers such as `Origin`; a manifest cannot emulate those headers unless the core connection schema and runtime explicitly support them.

The first URL that opens and completes the request is used for that operation. This is generic and not tied to any specific app.

```json
"connections": [
  {
    "id": "local",
    "label": "Local App",
    "type": "local_websocket",
    "defaultUrl": "ws://127.0.0.1:59129/v1/",
    "fallbackUrls": [
      "ws://127.0.0.1:20000/v1/",
      "ws://127.0.0.1:39273/v1/"
    ],
    "preflight": {
      "beforeSources": true,
      "beforeActions": true,
      "awaitResponse": false,
      "request": {
        "id": "register-sheevchat",
        "action": "registerClient",
        "payload": {
          "clientKey": "public-developer-client-key"
        }
      }
    }
  }
]
```

| Field | Required | Type | Meaning |
|---|---:|---|---|
| `request` | yes, unless `requests` is used | object | Single preflight request. |
| `requests` | yes, unless `request` is used | array | Multiple preflight requests in order. |
| `beforeSources` | no | boolean | Defaults to `true`. Sends preflight before dynamic dropdown source requests. |
| `beforeActions` | no | boolean | Defaults to `true`. Sends preflight before action execution. |
| `awaitResponse` | no | boolean | Defaults to `false`. Set `true` only when the app always replies and the reply should be received before the next request. |

For WebSockets, preflight requests are sent on the same socket immediately before the source/action request, so APIs that require “first message must be registration” work correctly.

### Match the Intended WebSocket Response

A WebSocket connection can receive more than the direct reply to the request SheevChat just sent. Apps may emit an initial server greeting, registration acknowledgement, status event, subscription update, or another unsolicited message on the same socket. A valid `map.rows` or `responseMap` path will still fail if SheevChat applies it to one of those unrelated frames.

When the API can emit multiple message shapes, add `responseMatch` to every dynamic source request and awaited query action. Match a stable response discriminator documented by the API, such as an action type, event name, request id, or channel. SheevChat ignores non-matching frames and waits for the intended response before applying `map.rows` or `responseMap`.

Dynamic source example:

```json
"request": {
  "id": "sheevchat-voicemod",
  "action": "getVoices",
  "payload": {},
  "responseMatch": {
    "$.actionType": "getVoices"
  }
}
```

Awaited query action example:

```json
"execute": {
  "type": "local_websocket",
  "connectionId": "local",
  "action": "getCurrentVoice",
  "payload": {},
  "awaitResponse": true,
  "responseMatch": {
    "$.actionType": "getCurrentVoice"
  },
  "responseNamespace": "voicemod",
  "responseMap": {
    "voice_id": {
      "path": "$.actionObject.voiceID",
      "default": ""
    }
  }
}
```

Choose the narrowest stable discriminator available. Do not match fields whose values change between calls. If the protocol echoes a unique request id, prefer that id; otherwise use a documented message type such as `$.actionType`. A preflight with `awaitResponse: false` makes response matching especially important because its acknowledgement or the app's greeting may arrive before the requested data.

When an `empty_rows_path` warning reports top-level keys that do not belong to the expected response, first confirm that the wrong WebSocket frame was selected. Do not change a documented rows path merely to fit a greeting or status frame.

### Voicemod-Style Example

Voicemod's Control API requires a `registerClient` message first, but that is not token/session auth. Model it as preflight:

```json
{
  "id": "local",
  "label": "Voicemod",
  "type": "local_websocket",
  "defaultUrl": "ws://127.0.0.1:59129/v1/",
  "fallbackUrls": [
    "ws://127.0.0.1:20000/v1/",
    "ws://127.0.0.1:39273/v1/",
    "ws://127.0.0.1:42152/v1/",
    "ws://127.0.0.1:43782/v1/",
    "ws://127.0.0.1:46667/v1/",
    "ws://127.0.0.1:35679/v1/",
    "ws://127.0.0.1:37170/v1/",
    "ws://127.0.0.1:38501/v1/",
    "ws://127.0.0.1:33952/v1/",
    "ws://127.0.0.1:30546/v1/"
  ],
  "test": {},
  "preflight": {
    "beforeSources": true,
    "beforeActions": true,
    "awaitResponse": false,
    "request": {
      "id": "register-sheevchat",
      "action": "registerClient",
      "payload": {
        "clientKey": "controlapi-example"
      }
    }
  }
}
```

Then a normal action can send only the actual app command:

```json
"execute": {
  "type": "local_websocket",
  "connectionId": "local",
  "id": "load-voice",
  "action": "loadVoice",
  "payload": {
    "voiceID": "{voice_id}"
  }
}
```

## Local WebSocket Connections

Use `local_websocket` for local WebSocket APIs.

```json
{
  "id": "local",
  "label": "Example WebSocket API",
  "type": "local_websocket",
  "defaultUrl": "ws://127.0.0.1:8001",
  "websocket": {
    "origin": "streamdeck://"
  },
  "test": {
    "message": { "id": 100, "jsonrpc": "2.0", "method": "getApplicationInfo", "params": null },
    "awaitResponse": true,
    "responseMatch": { "$.id": 100 }
  }
}
```

If `test.message` is omitted, Local WebSocket tests only check whether the socket opens. That does not prove protocol compatibility. When the app has a cheap info/ping request, include it in `test.message` with `awaitResponse` and `responseMatch`.

Use `connection.websocket.origin` when an app requires a WebSocket Origin during the opening handshake. Use `connection.websocket.headers` only for safe custom headers. Do not place `Origin`, `Host`, `Connection`, `Upgrade`, `Sec-WebSocket-*`, `Content-Length`, or `Transfer-Encoding` inside `headers`; those are controlled by the WebSocket transport. Header values are not logged.

The same WebSocket handshake settings apply to:

- connection tests
- dynamic source requests
- preflight sequences
- local auth/session requests
- query actions
- non-query actions

If the target app requires authentication, define `auth` so SheevChat can authenticate before resolving dynamic dropdowns or running actions.

## Local IPC Connections

Use `local_ipc` when an app exposes a local IPC/RPC endpoint instead of HTTP or WebSocket. The first supported transport is Windows named pipes through hardened protocol presets. Do not use `local_ipc` for arbitrary files, device paths, remote pipe hosts, or custom native code.

Local IPC requires:

```json
"permissions": ["local_ipc"]
```

Discord desktop RPC example:

```json
{
  "id": "discord",
  "type": "local_ipc",
  "discovery": {
    "type": "named_pipe_range",
    "template": "discord-ipc-{index}",
    "start": 0,
    "end": 9
  },
  "protocol": {
    "type": "discord_rpc",
    "clientId": "{auth.client_id}"
  }
}
```

### Named-Pipe Discovery

`named_pipe_range` expands a local pipe-name template such as `discord-ipc-{index}`. The template must resolve to simple local names only. SheevChat rejects slashes, backslashes, drive letters, traversal, remote pipe hosts, null bytes, and unrestricted device paths.

| Field | Required | Description |
|---|---:|---|
| `type` | yes | Must be `named_pipe_range`. |
| `template` | yes | Local pipe-name template containing `{index}`. |
| `start` | no | First integer index. Defaults to `0`. |
| `end` | no | Last integer index. Defaults to `start`. |

### Protocol Presets

`local_ipc` separates transport from protocol. The manifest can select a supported protocol preset, but it cannot define arbitrary binary parsing logic.

Currently supported:

| Protocol | Purpose |
|---|---|
| `discord_rpc` | Discord desktop RPC opcode/header framing, handshake, nonce correlation, and dispatch-event handling. |

Discord RPC uses bounded binary frames containing JSON payloads. SheevChat correlates responses by `nonce` and ignores unrelated dispatch events while a request is pending.

Discord's supported desktop RPC transport is IPC. Discord's localhost WebSocket RPC is deprecated and unavailable to new integrations. If a manifest points Discord controls at localhost WebSocket ports, SheevChat reports a warning and recommends `local_ipc` with `protocol.type = "discord_rpc"`.

Discord RPC requires a Discord application client ID and OAuth authorization before commands can run. Do not place secrets or user access tokens directly in the manifest. Use SheevChat connection storage for credentials/tokens. Runtime errors should tell the user whether Discord is closed, no matching pipe was found, the handshake failed, authorization is required, the command is unsupported, or the request timed out.

When IPC requests fail, SheevChat shows a sanitized user-safe message and includes a short developer-facing `errorCode` in the connection status. The code is useful for debugging imported packs, while tokens, authorization headers, secrets, and raw sensitive payload values remain redacted.

Security restrictions:

- Only approved protocol presets are accepted.
- Pipe targets must be validated local names.
- Arbitrary filesystem paths, traversal, remote hosts, environment expansion, and unrestricted device paths are rejected.
- Frames and responses are size bounded.
- Connection and request timeouts are enforced.
- Tokens, secrets, raw Authorization headers, and sensitive IPC payloads are not logged.
- Complex binary protocols should be implemented as hardened presets, not untrusted manifest-defined parsers.

## Manual Connection Credentials

Use `connection.credentials` when a library needs a user-entered API key, bearer token, webhook key, client identifier, or similar connection-level value. Credentials are stored locally by SheevChat and are not embedded in the manifest, exported with the pack, or returned to the renderer after saving.

Manual credentials are best for services such as Govee Cloud API keys, Lumia local API tokens, IFTTT webhook keys, and other APIs where the user obtains a value outside SheevChat and pastes it once.

```json
{
  "id": "cloud",
  "label": "Govee Cloud",
  "type": "local_http",
  "defaultUrl": "https://openapi.api.govee.com",
  "credentials": [
    {
      "key": "govee_api_key",
      "label": "Govee API Key",
      "type": "password",
      "required": true,
      "placeholder": "Paste API key",
      "help": "Create this in the Govee Developer portal."
    }
  ],
  "test": {
    "method": "GET",
    "path": "/router/api/v1/user/devices",
    "headers": {
      "Govee-API-Key": "{auth.govee_api_key}"
    }
  }
}
```

### Credential Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `key` | yes | string | Placeholder key. Must be unique within the connection. Use lowercase snake case, such as `govee_api_key`. |
| `label` | no | string | User-facing field label. Defaults to the key. |
| `type` | no | string | `password` or `text`. Defaults to `password`. Use `password` for API keys, bearer tokens, webhook secrets, and session-like values. |
| `required` | no | boolean | Defaults to `true`. Required credentials must be saved before tests, dropdowns, or actions can run. |
| `placeholder` | no | string | Placeholder shown before a value is saved. |
| `help` | no | string | Short user-facing helper text. |

Credential definitions must never include a value. SheevChat rejects manifest fields such as `value`, `default`, `defaultValue`, `secret`, `token`, `apiKey`, and `api_key` inside each credential object. Marketplace packs should only declare what the user needs to enter.

### Credential Placeholders

Saved credential values are available through the same auth placeholder namespace used by local app authentication:

| Placeholder | Meaning |
|---|---|
| `{auth.govee_api_key}` | Saved value for the `govee_api_key` credential. |
| `{govee_api_key}` | Convenience alias for the same saved value. Prefer the `{auth.*}` form. |

Credentials can be used in approved connection URL, query, header, WebSocket message, and body positions. This includes `connection.defaultUrl`, `connection.fallbackUrls`, `connection.test.url`, and request/action URLs.

```json
{
  "method": "POST",
  "url": "https://maker.ifttt.com/trigger/{event_name}/with/key/{auth.ifttt_key}",
  "json": {
    "value1": "{message}"
  }
}
```

For remote/cloud hosts, the manifest must also request `remote_network`, and the URL must pass the current runtime's remote-host validation.

Editable local ports should use a non-required text credential with a normal fallback URL. SheevChat tries the interpolated URL first when the user has saved a value, then falls back to the documented default:

```json
{
  "id": "local",
  "label": "Local WebSocket App",
  "type": "local_websocket",
  "defaultUrl": "ws://127.0.0.1:{auth.websocket_port}/",
  "fallbackUrls": ["ws://127.0.0.1:8081/"],
  "credentials": [
    {
      "key": "websocket_port",
      "label": "WebSocket Port",
      "type": "text",
      "required": false,
      "placeholder": "8081",
      "help": "Use this only if the app's WebSocket server is configured to a different port."
    }
  ]
}
```

## Declarative HTTP Request Signing

Use `connection.signing` when an HTTP API requires a predictable signature on every request. This is for approved signing presets only. It does not run manifest code, and it does not allow secrets to be embedded in the pack.

Current supported preset:

| Field | Required | Type | Accepted / Notes |
|---|---:|---|---|
| `type` | no | string | Currently `hmac_sha256`. Defaults to `hmac_sha256`. |
| `secret` / `secretTemplate` | yes | string | Must reference a locally saved credential such as `{auth.client_secret}`. Do not put literal secrets in manifests. |
| `payload` / `template` | usually | string | String to sign after placeholder substitution. Required unless `canonical` is configured. |
| `canonical` | no | string/object | Approved canonical request preset. Currently supports `tuya_v2`. |
| `target` | no | string | `header`, `query`, or `body`. Defaults to `header`. |
| `name` | yes | string | Header name, query parameter name, or object body field that receives the signature. Aliases: `header`, `query`, `field`. |
| `encoding` | no | string | `hex`, `base64`, or `base64url`. Defaults to `hex`. |
| `case` / `outputCase` | no | string | `preserve`, `upper`, or `lower`. Defaults to `preserve`. |
| `prefix` | no | string | Optional text prepended to the signature value, such as `sha256=`. |
| `bodyHashEncoding` | no | string | Encoding for `{signing.bodySha256}` / `{body.sha256}`. Defaults to `hex`. |
| `timestamp` | no | boolean/object | `true` enables `{signing.timestamp}` using `unix_ms`. Object accepts `{ "format": "unix_ms" }`, `{ "format": "unix" }`, or `{ "format": "iso" }`. |
| `nonce` | no | boolean/object | `true` enables `{signing.nonce}` using 16 random bytes as hex. Object accepts `bytes` from 8-64 and `encoding` of `hex`, `base64`, or `base64url`. |

Signing placeholders available inside `payload`:

| Placeholder | Meaning |
|---|---|
| `{request.method}` | Final HTTP method, uppercased. |
| `{request.url}` | Final target URL before signature injection. |
| `{request.path}` | URL path plus query string. |
| `{request.pathname}` | URL path only. |
| `{request.query}` | Query string without leading `?`. |
| `{request.body}` | Exact string body SheevChat will send. Empty for GET/HEAD or no body. This respects `bodyEncoding`, so form bodies are signed as URL-encoded text instead of JSON. |
| `{body.sha256}` / `{signing.bodySha256}` | SHA-256 hash of `{request.body}`. |
| `{signing.timestamp}` | Generated timestamp when `timestamp` is enabled. |
| `{signing.nonce}` | Generated nonce when `nonce` is enabled. |
| `{auth.<key>}` | Saved credential or local auth value. Use this for signing secrets and related client ids/tokens. |

### Canonical Request Presets

Use a canonical preset when an API documents a fixed request-signing recipe that should not be hand-assembled in every pack. Presets still use the same credential, timestamp, nonce, and target rules as normal signing.

#### `tuya_v2`

The `tuya_v2` preset builds the Tuya-style HMAC payload:

```text
client_id + access_token + timestamp + nonce + stringToSign
```

Where `stringToSign` is:

```text
METHOD
bodySha256
signedHeaderBlock
canonicalPathAndSortedQuery
```

Use `includeAccessToken: false` for token acquisition endpoints that do not have an access token yet. Use `signedHeaders` only when the target API explicitly requires extra headers in the canonical string.

```json
{
  "signing": {
    "type": "hmac_sha256",
    "secret": "{auth.client_secret}",
    "canonical": {
      "preset": "tuya_v2",
      "clientId": "{auth.client_id}",
      "accessToken": "{auth.access_token}",
      "timestamp": "{signing.timestamp}",
      "nonce": "{signing.nonce}",
      "includeAccessToken": true
    },
    "target": "header",
    "name": "sign",
    "encoding": "hex",
    "case": "upper",
    "timestamp": { "format": "unix_ms" },
    "nonce": { "bytes": 16, "encoding": "hex" }
  }
}
```

Then declare the visible API headers in each request:

```json
{
  "method": "POST",
  "path": "/v1.0/devices/{device_id}/commands",
  "headers": {
    "client_id": "{auth.client_id}",
    "access_token": "{auth.access_token}",
    "t": "{signing.timestamp}",
    "nonce": "{signing.nonce}"
  },
  "body": {
    "commands": [
      { "code": "switch_led", "value": true }
    ]
  }
}
```

For token requests:

```json
{
  "canonical": {
    "preset": "tuya_v2",
    "clientId": "{auth.client_id}",
    "timestamp": "{signing.timestamp}",
    "nonce": "{signing.nonce}",
    "includeAccessToken": false
  }
}
```

If the API requires timestamp, nonce, client id, or token values in headers/query/body, add those placeholders to the request itself:

```json
{
  "permissions": ["local_http", "remote_network"],
  "connections": [
    {
      "id": "cloud",
      "label": "Signed Cloud API",
      "type": "local_http",
      "defaultUrl": "https://api.example.com",
      "remote": { "allowedHosts": ["api.example.com"] },
      "credentials": [
        { "key": "client_id", "label": "Client ID", "type": "text" },
        { "key": "client_secret", "label": "Client Secret", "type": "password" }
      ],
      "signing": {
        "type": "hmac_sha256",
        "secret": "{auth.client_secret}",
        "payload": "{request.method}\n{request.path}\n{body.sha256}\n{signing.timestamp}",
        "target": "header",
        "name": "X-Signature",
        "encoding": "hex",
        "case": "upper",
        "timestamp": { "format": "unix_ms" }
      }
    }
  ],
  "actions": [
    {
      "id": "set_power",
      "label": "Set Power",
      "connectionId": "cloud",
      "fields": [],
      "execute": {
        "type": "local_http",
        "connectionId": "cloud",
        "method": "POST",
        "path": "/v1/device/power",
        "headers": {
          "X-Client-ID": "{auth.client_id}",
          "X-Timestamp": "{signing.timestamp}"
        },
        "body": { "on": true }
      }
    }
  ]
}
```

Important limits:

- This section only signs HTTP requests. For refreshable access tokens, use `connection.auth.refreshRequest` / `refreshMap` in the token lifecycle section below.
- Body-target signing can only inject into object bodies. Use header or query signing for text bodies.
- Marketplace review should reject packs with literal signing secrets, even if local import diagnostics already catch this for developers.

### Clock Skew Feedback

Some signed APIs reject otherwise-correct requests when the user's PC clock is several minutes away from the API server clock. Pack authors do not need to declare a special field for this. When a signed HTTP request receives `401` or `403`, SheevChat compares the response's standard HTTP `Date` header to the local clock. If the clocks differ by more than about five minutes, the runtime adds a user/developer-facing hint that the PC clock may need to be corrected.

## Runtime Secret Redaction

When a connection uses `connection.credentials`, `connection.auth`, or `{auth.<key>}` placeholders, SheevChat treats those runtime values as secrets. If a request fails after interpolation, SheevChat redacts saved auth values and their URL-encoded forms before storing connection status details or returning action-library API errors to the UI.

Pack authors should still avoid putting secret-bearing values in friendly response templates, labels, descriptions, static options, or output variables. Redaction is a runtime safety net for failures and diagnostics, not a reason to expose credentials through normal manifest metadata.

For best diagnostics, APIs should return a normal `Date` response header on authentication/signature failures.

## Local App Authentication

Use `connection.auth` when a local app requires a token, session authentication, or pairing approval before source dropdowns or actions can work. This is for local HTTP/WebSocket/IPC/UDP app APIs, not platform OAuth. The manifest describes the handshake; SheevChat stores returned local tokens in the user's local config, keyed by library and connection.

Typical flow:

1. User expands Action Libraries details.
2. User clicks **Test** to confirm the app is reachable.
3. User clicks **Authenticate**.
4. SheevChat sends the token request, maps the returned token, stores it locally, then sends the session request.
5. Future dropdowns and actions automatically run the session request first when required.

For device-code or approval-code flows, `connection.auth.pairing` can start pairing, show the user code/verification URL, poll in the background, then store the returned token as normal `{auth.*}` values.

### Auth Object Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `type` | no | string | Metadata. Use `local_session` unless a more specific label helps the library author. |
| `label` | no | string | User-facing auth label shown in details. |
| `buttonLabel` | no | string | Button text. Defaults to `Authenticate`. |
| `instructions` | no | string | Short user-facing note, such as "Approve the prompt in the target app." |
| `tokenKey` | no | string | Stored token field name. Defaults to `token`. |
| `persist` | no | boolean | Defaults to `true`. Set `false` for one-time session data that should not be saved. |
| `statusRequest` | no | object | Optional request to check whether the app/session is already authenticated. |
| `statusMap` | no | object | Maps response values from `statusRequest`, commonly `{ "authenticated": "$.data.authenticated" }`. |
| `tokenRequest` | no | object | Request used to obtain a token. |
| `tokenMap` | no | object | Maps response values from `tokenRequest`, commonly `{ "token": "$.data.authenticationToken" }`. |
| `tokenPath` | no | string | Shortcut for `tokenMap.token`. |
| `pairing` | no | object | Device-code/local pairing flow with `startRequest`, `startMap`, `pollRequest`, `pollMap`, `intervalMs`, and `timeoutMs`. |
| `refreshRequest` | no | object | Request used to refresh an expiring token before sources/actions run. Uses saved `{auth.*}` values. |
| `refreshMap` | no | object | Maps refreshed response values, commonly `{ "access_token": "$.access_token", "expires_in": "$.expires_in" }`. Falls back to `tokenMap` if omitted. |
| `refreshTokenKey` | no | string | Stored refresh token key. Defaults conceptually to `refresh_token` for generated manifests. |
| `expiresInKey` | no | string | Response/storage key containing seconds until expiry. Defaults to `expires_in`. |
| `expiresAtKey` | no | string | Stored absolute expiry key. Defaults to `expires_at`. |
| `refreshSkewSeconds` | no | number | Refresh this many seconds before expiry. Defaults to 60. Max 3600. |
| `sessionRequest` | no | object | Request used to authenticate the current session/socket with the saved token. |
| `sessionMap` | no | object | Maps response values from `sessionRequest`, commonly `{ "authenticated": "$.data.authenticated" }`. |
| `authenticatedPath` | no | string | Shortcut for `sessionMap.authenticated`. |
| `inject` | no | object | Controls automatic session preflight before sources/actions. Defaults to both enabled. |

At least one of `statusRequest`, `tokenRequest`, `sessionRequest`, or `pairing` must be present.

### Auth Response Truthiness

For `statusMap.authenticated` or `sessionMap.authenticated`, SheevChat treats these values as authenticated:

```text
true, 1, "true", "1", "yes", "authenticated", "connected", "ok", "success"
```

Anything else is treated as not authenticated.

### Pairing Auth Flow

Use `connection.auth.pairing` for local or LAN apps that require a user to approve SheevChat with a code, browser URL, device URL, or in-app prompt before returning a reusable token. The pairing flow is intentionally bounded and uses the same local HTTP/WebSocket/IPC/UDP request runtime as other action-library auth.

Runtime behavior:

1. User clicks **Authenticate**.
2. SheevChat sends `pairing.startRequest`.
3. SheevChat maps the response with `pairing.startMap`.
4. If a user code or verification URL is mapped, SheevChat shows it in the connection status/toast.
5. SheevChat polls `pairing.pollRequest` in the background until `pairing.pollMap` returns the configured token key.
6. The mapped token values are stored locally and become `{auth.token}` / `{auth.<key>}` for future sources/actions.

Pairing object fields:

| Field | Required | Type | Notes |
|---|---:|---|---|
| `startRequest` | yes | object | Request that begins pairing. |
| `startMap` | no | object | Maps code/display fields from the start response. Common keys: `device_code`, `user_code`, `verification_uri`, `verification_url`, `expires_in`, `interval`. |
| `pollRequest` | yes | object | Request repeated until a token is returned. Can use `{pairing.device_code}`, `{pairing.user_code}`, or any key mapped by `startMap`. |
| `pollMap` | no | object | Maps token values from the poll response. Should include the auth `tokenKey`, or `token`. |
| `intervalMs` | no | number | Poll interval from 1000 to 30000 ms. Defaults to 5000. |
| `timeoutMs` | no | number | Pairing timeout from 10000 to 300000 ms. Defaults to 120000. |

Example:

```json
{
  "id": "local",
  "label": "Example Pairing App",
  "type": "local_http",
  "defaultUrl": "http://127.0.0.1:4000",
  "auth": {
    "type": "local_pairing",
    "buttonLabel": "Pair",
    "tokenKey": "access_token",
    "pairing": {
      "startRequest": {
        "method": "POST",
        "path": "/pair/start"
      },
      "startMap": {
        "device_code": "$.device_code",
        "user_code": "$.user_code",
        "verification_uri": "$.verification_uri",
        "expires_in": "$.expires_in"
      },
      "pollRequest": {
        "method": "POST",
        "path": "/pair/poll",
        "body": {
          "device_code": "{pairing.device_code}"
        }
      },
      "pollMap": {
        "access_token": "$.access_token",
        "refresh_token": "$.refresh_token",
        "expires_in": "$.expires_in"
      },
      "intervalMs": 5000,
      "timeoutMs": 120000
    }
  }
}
```

Do not use pairing for OAuth authorization-code flows. Use `connection.type: "oauth"` and `connection.oauth` for those.

### Token Expiry And Refresh

For cloud APIs or local APIs with short-lived access tokens, map both the token and expiry values. SheevChat stores the mapped values locally, computes `expires_at` from `expires_in` when needed, and refreshes just before a source dropdown or imported action runs.

Use snake_case keys in generated manifests:

```json
{
  "auth": {
    "type": "token_lifecycle",
    "tokenKey": "access_token",
    "refreshTokenKey": "refresh_token",
    "expiresInKey": "expires_in",
    "expiresAtKey": "expires_at",
    "refreshSkewSeconds": 120,
    "tokenRequest": {
      "method": "POST",
      "path": "/v1/token",
      "body": {
        "client_id": "{auth.client_id}",
        "client_secret": "{auth.client_secret}"
      }
    },
    "tokenMap": {
      "access_token": "$.access_token",
      "refresh_token": "$.refresh_token",
      "expires_in": "$.expires_in"
    },
    "refreshRequest": {
      "method": "POST",
      "path": "/v1/token/refresh",
      "body": {
        "refresh_token": "{auth.refresh_token}"
      }
    },
    "refreshMap": {
      "access_token": "$.access_token",
      "expires_in": "$.expires_in"
    }
  }
}
```

Runtime behavior:

- If no expiry is stored, SheevChat does not refresh automatically.
- If an expiry is stored and the token is within the refresh skew window, SheevChat sends `refreshRequest`, maps the response, stores the new values, then continues with the original source/action.
- If refresh fails, SheevChat marks the connection as needing authentication and stops the original request.
- If the provider returns no new refresh token, the old stored `{auth.refresh_token}` remains available because refreshed values merge into the existing local auth store.
- This is still not a full generic OAuth 2.0 authorization-code runtime. It is token lifecycle support for APIs where the manifest can express token/refresh HTTP requests safely.

### WebSocket Token + Session Example

Use this shape for apps that first return a reusable token, then require the token to authenticate each WebSocket session:

```json
{
  "id": "local",
  "label": "Example Local App",
  "type": "local_websocket",
  "defaultUrl": "ws://127.0.0.1:8001",
  "auth": {
    "type": "local_session",
    "label": "Local app authentication",
    "buttonLabel": "Authenticate",
    "instructions": "Approve the prompt in the target app if one appears.",
    "tokenKey": "token",
    "statusRequest": {
      "messageType": "APIStateRequest"
    },
    "statusMap": {
      "authenticated": "$.data.currentSessionAuthenticated"
    },
    "tokenRequest": {
      "messageType": "AuthenticationTokenRequest",
      "data": {
        "pluginName": "SheevChat",
        "pluginDeveloper": "SheevChat"
      }
    },
    "tokenMap": {
      "token": "$.data.authenticationToken"
    },
    "sessionRequest": {
      "messageType": "AuthenticationRequest",
      "data": {
        "pluginName": "SheevChat",
        "pluginDeveloper": "SheevChat",
        "authenticationToken": "{auth.token}"
      }
    },
    "sessionMap": {
      "authenticated": "$.data.authenticated"
    },
    "inject": {
      "beforeSources": true,
      "beforeActions": true
    }
  }
}
```

### Auth Placeholders

Mapped auth values are available as placeholders:

| Placeholder | Meaning |
|---|---|
| `{auth.token}` | Saved token when `tokenKey` is `token`. |
| `{token}` | Convenience alias for the same saved token. |
| `{auth.some_key}` | Saved auth value from `tokenMap.some_key`. |
| `{some_key}` | Convenience alias for the same saved value. |

Prefer `{auth.token}` in generated manifests because it makes the source of the value obvious.

### Auth Preflight

When `inject.beforeSources` is true, SheevChat sends `sessionRequest` before a `connection_request` source request. This lets authenticated dropdowns populate correctly.

When `inject.beforeActions` is true, SheevChat sends `sessionRequest` before an imported action runs.

For WebSocket APIs, the preflight and the real request are sent on the same short-lived WebSocket connection. For HTTP APIs, SheevChat sends the preflight request first, then sends the real request.

## Generic Instance File Discovery

Use instance file discovery when an app writes local JSON files that tell clients which instance/port is active.

Example:

```json
{
  "id": "local",
  "label": "Example App",
  "type": "local_websocket",
  "discovery": {
    "type": "instance_file",
    "directory": ".example-app/instances",
    "map": {
      "time": "$.time",
      "id": "$.id",
      "server": "$.server",
      "name": "$.name",
      "version": "$.version",
      "language": "$.language"
    },
    "preferredType": "main",
    "selectionMode": "exact_or_newest",
    "clientName": "SheevChat",
    "staleMs": 10000
  }
}
```

### Instance Discovery Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `type` | yes | string | Must be `instance_file`. |
| `provider` | no | string | Metadata only. Defaults to `generic`. |
| `directory` | no | string | Relative paths resolve under the user's home directory. |
| `map` | no | object | Maps instance JSON fields to SheevChat fields. |
| `preferredType` | no | string | Chooses a matching instance type when multiple are active. |
| `selectionMode` | no | string | How SheevChat picks an instance. Defaults to `exact_or_newest`. |
| `instanceSlot` | no | number | Default 1-based ordinal slot used when `selectionMode` is `ordinal`. |
| `clientName` | no | string | Used in generated WebSocket query string. Defaults to `SheevChat`. |
| `staleMs` | no | number | Active window for instance files. Clamped 1000-60000 ms. Defaults to 10000. |

Relative directory example:

```json
"directory": ".veadotube/instances"
```

The directory value is already resolved under the user's home directory. Do not put environment variables, home aliases, absolute paths, or drive letters in the manifest.

Do not use:

```text
%USERPROFILE%\.veadotube\instances
$HOME/.veadotube/instances
~/.veadotube/instances
C:\Users\name\.veadotube\instances
```

On Windows, this means:

```text
%USERPROFILE%\.veadotube\instances
```

### Instance Map

Default instance map:

```json
{
  "time": "$.time",
  "id": "$.id",
  "server": "$.server",
  "name": "$.name",
  "version": "$.version",
  "language": "$.language"
}
```

Required mapped values are:

- `time`
- `id`
- `server`

The `server` value can be:

```text
127.0.0.1:12345
ws://127.0.0.1:12345
wss://127.0.0.1:12345
```

SheevChat converts bare `host:port` values to `ws://host:port` for WebSocket instance connections.

### Instance Selection Modes

Use `selectionMode` when an app can run more than one instance at the same time.

| Mode | Meaning |
|---|---|
| `exact_or_newest` | Backward-compatible default. Use the selected `instance_id`/`instance_server` if it is active; otherwise fall back to the newest matching instance. |
| `exact` | Require the selected `instance_id`/`instance_server`. If that instance is not active, the action fails instead of switching targets. |
| `newest` | Pick the newest active instance, filtered by `preferredType` when possible. |
| `oldest` | Pick the oldest active instance, filtered by `preferredType` when possible. |
| `ordinal` | Pick a 1-based slot from active instances ordered oldest-to-newest. Slot `5` only works when the previous four matching instances are also open. |

Action fields may override the connection default by sending hidden values named:

```text
instance_selection_mode
instance_slot
instance_id
instance_server
preferredType
```

For explicit user-picked instance dropdowns, use an `instance_file` source. The dropdown option stores `instance_selection_mode: "exact"` so the selected instance stays pinned while it is active.

## Generic Endpoint File Discovery

Use endpoint-file discovery when an app writes one stable JSON file containing its current port or WebSocket URL. This is different from directory-scanned instance discovery:

- `instance_file` scans a directory of active instance files and requires mapped `time`, `id`, and `server`.
- `endpoint_file` reads one specific JSON file and can generate a stable endpoint from a mapped `port`.

Endpoint files are useful for apps that randomize their local port each launch.

Example:

```json
{
  "id": "local",
  "label": "Example App",
  "type": "local_websocket",
  "discovery": {
    "type": "endpoint_file",
    "root": "localAppData",
    "file": "Packages/Example.App/LocalState/ws-info.json",
    "map": {
      "port": "$.port"
    },
    "urlTemplate": "ws://127.0.0.1:{port}"
  }
}
```

### Endpoint Discovery Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `type` | yes | string | Must be `endpoint_file`. |
| `root` | no | string | Safe root. Use `home` or `localAppData`. Defaults to `home`. |
| `file` | yes | string | Relative JSON file path under the safe root. |
| `map` | no | object | Maps `port`, optional `host`, `protocol`, `path`, `url`, `id`, `name`, `version`, and `language`. |
| `urlTemplate` | no | string | WebSocket URL template such as `ws://127.0.0.1:{port}`. |
| `host` | no | string | Default host when no `url` is mapped. Defaults to `127.0.0.1`. |
| `protocol` | no | string | `ws` or `wss`. Defaults to `ws`. |
| `pathSuffix` / `endpointPath` | no | string | Optional path appended when SheevChat builds a URL without `urlTemplate`. |
| `selectionMode` | no | string | Usually `newest`; present for shared selection behavior. |

The `file` value must be relative to its safe root. Do not use `%LOCALAPPDATA%`, `%USERPROFILE%`, `$HOME`, `~`, drive letters, absolute paths, or `..`.

For Windows Store apps, prefer:

```json
{
  "root": "localAppData",
  "file": "Packages/Publisher.App/LocalState/ws-info.json"
}
```

SheevChat resolves that under the current user's `%LOCALAPPDATA%` without exposing arbitrary absolute file access.

### URL Templates

`urlTemplate` may use mapped/default placeholders:

```text
{port}
{host}
{protocol}
{path}
{url}
{id}
{name}
{version}
{language}
```

Every placeholder must be known from the endpoint map or built-in defaults. The resolved URL must be `ws://` or `wss://` and must include a valid port from 1 to 65535.

## mDNS / DNS-SD Service Discovery

Use `mdns_service` when the target app or device advertises itself on the local network with Bonjour/mDNS/DNS-SD. This is common for LAN devices and desktop companion apps that do not have a fixed port, but do publish a service type.

mDNS discovery is intentionally narrow:

- The manifest must include `local_discovery`.
- The service type must be explicit, such as `_elg._tcp.local`.
- SheevChat only accepts discovered endpoints that resolve to loopback, RFC1918 LAN addresses, or `.local` names.
- The manifest chooses the protocol and optional path suffix used to build the endpoint URL.
- Discovery results are cached briefly so repeated dropdown loads and actions do not constantly probe the LAN.

```json
{
  "permissions": ["local_http", "local_discovery"],
  "connections": [
    {
      "id": "local",
      "label": "Elgato Light",
      "type": "local_http",
      "discovery": {
        "type": "mdns_service",
        "provider": "elgato",
        "serviceType": "_elg._tcp.local",
        "protocol": "http",
        "path": "/elgato/lights",
        "selectionMode": "newest",
        "timeoutMs": 1200,
        "cacheMs": 10000
      }
    }
  ]
}
```

### mDNS Discovery Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `type` | yes | string | Must be `mdns_service`. |
| `serviceType` / `service` | yes | string | DNS-SD service type. Must look like `_service._tcp.local` or `_service._udp.local`. |
| `provider` | no | string | Metadata label for the app/device family. |
| `protocol` | no | string | `http`, `https`, `ws`, or `wss`. Defaults to `http`. |
| `path` / `pathSuffix` | no | string | Optional endpoint path appended to the discovered host and port. |
| `selectionMode` / `mode` | no | string | `newest`, `oldest`, `ordinal`, `exact`, or `exact_or_newest`. Defaults to `newest`. |
| `instanceSlot` / `slot` | no | number | Used with `selectionMode: "ordinal"` to select the Nth discovered service after stable ordering. |
| `timeoutMs` | no | number | Discovery wait time. Clamped from 250ms to 5000ms. Defaults to 1200ms. |
| `cacheMs` | no | number | Runtime connection cache time. Clamped from 0ms to 60000ms. Defaults to 10000ms. |

### mDNS Dynamic Instance Dropdowns

If a source uses `type: "instance_file"` against a connection whose discovery type is `mdns_service`, SheevChat returns discovered services as selectable options. The option `extra` values include the same common instance fields used by file-based discovery:

```text
instance_id
instance_name
instance_server
instance_type
instance_version
instance_index
instance_ordinal
instance_selection_mode
```

Use this when a user needs to select one specific LAN device from several discovered devices.

```json
"sources": {
  "lights": {
    "type": "instance_file",
    "connectionId": "local"
  }
}
```

The selected option can then pass `instance_id`, `instance_server`, or ordinal values into actions that should target a specific discovered service.

## Response-Based Pipeline Discovery

Use pipeline discovery when an app writes a local bootstrap file, but the file does not contain the final service URL directly. A pipeline can read mapped values from a safe local JSON file, call a loopback HTTP/HTTPS discovery endpoint, map the response, and return the final app service URL.

This is useful for apps with a local broker that reports sub-application endpoints after every restart.

Example:

```json
{
  "id": "sonar",
  "label": "SteelSeries GG Sonar Local API",
  "type": "local_http",
  "discovery": {
    "type": "pipeline",
    "cacheMs": 3000,
    "steps": [
      {
        "type": "json_file",
        "base": "programData",
        "file": "SteelSeries/GG/coreProps.json",
        "map": {
          "gg_address": "$.ggEncryptedAddress"
        }
      },
      {
        "type": "http_request",
        "method": "GET",
        "urlTemplate": "https://{gg_address}/subApps",
        "allowSelfSignedLoopback": true,
        "map": {
          "service_url": "$.subApps.sonar.metadata.webServerAddress",
          "sonar_enabled": "$.subApps.sonar.isEnabled",
          "sonar_running": "$.subApps.sonar.isRunning",
          "sonar_ready": "$.subApps.sonar.isReady"
        },
        "readiness": [
          { "key": "sonar_enabled", "equals": true, "message": "Sonar is installed but disabled." },
          { "key": "sonar_running", "equals": true, "message": "Sonar is not running." },
          { "key": "sonar_ready", "equals": true, "message": "Sonar is not ready yet." }
        ]
      }
    ],
    "result": "{service_url}"
  },
  "test": {
    "method": "GET",
    "path": "/Mode"
  }
}
```

### Pipeline Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `type` | yes | string | Must be `pipeline`. |
| `cacheMs` | no | number | Short-lived resolved endpoint cache. Defaults to 3000 ms. |
| `steps` | yes | array | Ordered `json_file` and `http_request` steps. |
| `result` | yes | string | Template for the final service URL, such as `{service_url}`. |

### Pipeline Step Types

`json_file` reads one safe local JSON file and maps values for later steps.

| Field | Required | Type | Notes |
|---|---:|---|---|
| `type` | yes | string | Must be `json_file`. |
| `base` / `root` | no | string | Safe root. Use `home`, `localAppData`, or `programData`. |
| `file` | yes | string | Relative JSON file path under the safe root. |
| `map` | yes | object | Maps variable names to JSON paths. |

`http_request` calls a loopback discovery endpoint and maps the response.

| Field | Required | Type | Notes |
|---|---:|---|---|
| `type` | yes | string | Must be `http_request`. |
| `method` | no | string | `GET`, `POST`, or `PUT`. Defaults to `GET`. |
| `urlTemplate` | yes | string | `http://` or `https://` loopback URL template. |
| `allowSelfSignedLoopback` | no | boolean | Allows a self-signed certificate only for this loopback HTTPS discovery request. |
| `map` | no | object | Maps response values for later steps or `result`. |
| `readiness` | no | array | Optional checks that must pass before accepting the endpoint. |

### Pipeline Safety Rules

- File paths must stay under their selected safe root.
- Do not use absolute paths, drive letters, UNC paths, environment variables, home aliases, or `..`.
- Discovery request URLs must resolve to loopback hosts only: `127.0.0.1`, `localhost`, or `::1`.
- Discovery requests may use `http://` or `https://`; final app service URLs may use `http://`, `https://`, `ws://`, or `wss://`.
- URLs with embedded credentials are rejected.
- Redirects must stay on loopback.
- `allowSelfSignedLoopback` never disables TLS checks globally and does not apply to normal action/source requests.

### Readiness Checks

Use `readiness` when a discovery response can identify installed-but-disabled, stopped, or not-yet-ready states.

```json
{
  "readiness": [
    { "key": "app_enabled", "equals": true, "message": "The app is installed but disabled." },
    { "key": "app_running", "equals": true, "message": "The app is not running." }
  ]
}
```

Each `key` must be mapped earlier in that step's `map`. If `equals` is omitted, SheevChat treats the mapped value as a truthy/falsy readiness value.

## Sources

Sources power dynamic dropdowns. `sources` must be an object keyed by source id.

Correct:

```json
"sources": {
  "items": {
    "type": "static_options",
    "options": []
  }
}
```

Incorrect:

```json
"sources": [
  {
    "id": "items",
    "type": "static_options"
  }
]
```

### Accepted Source Types

| Type | Purpose |
|---|---|
| `static_options` | Fixed dropdown options defined in the manifest. |
| `instance_file` | Dropdown of currently discovered app instances. |
| `connection_request` | Calls the app/API and maps the response into dropdown options. |

### Source Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `type` | no | string | Defaults to `connection_request`. |
| `connectionId` | depends | string | Required for `instance_file` and `connection_request`. |
| `dependsOn` | no | array | Source-level dependencies. Used with cascading fields. |
| `request` | no | object | HTTP/WebSocket request definition for `connection_request`. |
| `map` | no | object | Maps API response into dropdown options. |
| `options` | no | array | Fixed options for `static_options`. |
| `cacheMs` | no | number | Cache duration for source results. `0` disables cache. |

## Static Options Sources

Use `static_options` when all dropdown choices are known in advance.

```json
"sources": {
  "boolean_state": {
    "type": "static_options",
    "options": [
      { "value": "on", "label": "On" },
      { "value": "off", "label": "Off" },
      { "value": "toggle", "label": "Toggle" }
    ]
  }
}
```

Static options may include `extra` values:

```json
{
  "value": "scene_a",
  "label": "Scene A",
  "extra": {
    "scene_id": "abc123"
  }
}
```

Static `value`, `label`, and `extra` strings can use placeholders from existing values.

## Instance File Sources

Use this with a connection that has `discovery.type = instance_file`.

```json
"sources": {
  "instances": {
    "type": "instance_file",
    "connectionId": "local",
    "cacheMs": 1000
  }
}
```

SheevChat returns options with these hidden extras:

| Extra key | Meaning |
|---|---|
| `instance_id` | Discovered instance id. |
| `instance_name` | Discovered instance name. |
| `instance_server` | Discovered host/port or URL. |
| `instance_type` | Inferred instance type. |
| `instance_version` | Discovered app version. |
| `instance_index` | Index in the discovered instance list. |
| `instance_ordinal` | 1-based slot when instances are ordered oldest-to-newest. |
| `instance_selection_mode` | Set to `exact` for selected instance options. |

For multi-instance apps, include an Instance field first in every action:

```json
{
  "key": "instance",
  "label": "Instance",
  "type": "dynamic_select",
  "source": "instances",
  "labelKey": "instance_name"
}
```

## Connection Request Sources

Use `connection_request` to ask the target app/API for dropdown options.

```json
"sources": {
  "hotkeys": {
    "type": "connection_request",
    "connectionId": "local",
    "cacheMs": 5000,
    "request": {
      "messageType": "HotkeysRequest"
    },
    "map": {
      "value": "$.data.hotkeys[].id",
      "label": "$.data.hotkeys[].name"
    }
  }
}
```

For HTTP connections, `request` can contain:

```json
{
  "url": "/api/items",
  "method": "GET",
  "headers": {
    "Accept": "application/json"
  }
}
```

For non-GET HTTP requests (`POST` and `PUT`), `body` is supported:

```json
{
  "url": "/api/items/search",
  "method": "POST",
  "headers": {
    "Content-Type": "application/json"
  },
  "body": {
    "query": "{search}"
  }
}
```

Use `bodyEncoding` when the API expects form data or plain text instead of JSON:

```json
{
  "url": "/oauth/token",
  "method": "POST",
  "bodyEncoding": "form_urlencoded",
  "body": {
    "grant_type": "client_credentials",
    "client_id": "{auth.client_id}",
    "client_secret": "{auth.client_secret}"
  }
}
```

Do not use `PATCH` or `DELETE` in imported action libraries. They are intentionally unsupported.

For WebSocket connections, `request` can be a plain JSON message:

```json
{
  "messageType": "ListHotkeysRequest",
  "data": {
    "modelID": "{model_id}"
  }
}
```

Or a channel-style message:

```json
{
  "channel": "nodes",
  "payload": {
    "event": "list"
  }
}
```

## WebSocket Channel Messages

Some apps use messages shaped like:

```text
nodes: {"event":"list"}
```

Represent that in the manifest as:

```json
"request": {
  "channel": "nodes",
  "payload": {
    "event": "list"
  }
}
```

SheevChat sends:

```text
nodes: {"event":"list"}
```

When the app replies:

```text
nodes: {"event":"list","entries":[{"id":"mini","name":"avatar state"}]}
```

SheevChat parses it as:

```json
{
  "channel": "nodes",
  "payload": {
    "event": "list",
    "entries": [
      {
        "id": "mini",
        "name": "avatar state"
      }
    ]
  }
}
```

Therefore the map path must include `$.payload`:

```json
"map": {
  "rows": "$.payload.entries[]",
  "value": "{id}",
  "label": "{name}"
}
```

For nested payload responses:

```text
nodes: {"event":"payload","payload":{"states":[...]}}
```

Use:

```json
"rows": "$.payload.payload.states[]"
```

## Source Mapping

The `map` object converts an API response into dropdown options.

### Simple Path Mapping

Use this when values and labels are parallel arrays or array paths.

```json
"map": {
  "value": "$.data.items[].id",
  "label": "$.data.items[].name"
}
```

`value` and `label` also support fallback path arrays and full extraction objects:

```json
"map": {
  "value": {
    "paths": ["$.data.items[].id", "$.items[].id", "$.results[].value"]
  },
  "label": {
    "paths": ["$.data.items[].name", "$.items[].name", "$.results[].label"]
  }
}
```

The full extraction object accepts the same fields as `execute.responseMap`: `path`, `paths`, `default`, `transform`, and `separator`.

### Row Mapping

Use row mapping for most APIs.

```json
"map": {
  "rows": "$.payload.entries[]",
  "value": "{id}",
  "label": "{name}"
}
```

Each row becomes a temporary object. `{id}` and `{name}` are read from that row.

### Parent/Child Row Flattening

Use `childRows` when an API returns parent resources that contain nested child arrays, but the user should see one friendly dropdown. Each child becomes an option while the parent row stays available as `$.parent`.

Example response:

```json
{
  "result": {
    "inputDevices": [
      {
        "id": "device-id",
        "name": "Wave:3",
        "inputs": [
          { "id": "input-id", "name": "Wave:3" }
        ]
      }
    ]
  }
}
```

Mapping:

```json
"map": {
  "rows": "$.result.inputDevices[]",
  "childRows": "$.inputs[]",
  "value": "{parent_id}:{id}",
  "label": "{parent_name} - {name}",
  "extra": {
    "device_id": "$.parent.id",
    "input_id": "$.id",
    "device_name": "$.parent.name",
    "input_name": "$.name"
  }
}
```

For templates, SheevChat provides flat parent aliases for scalar parent properties, such as `{parent_id}` and `{parent_name}`. JSON paths in `extra` can use `$.parent.id` for parent fields and `$.id` for child fields.

### Filtering Rows

Use `where` to filter rows.

```json
"map": {
  "rows": "$.payload.entries[]",
  "where": {
    "type": "stateEvents"
  },
  "value": "{type}:{id}",
  "label": "{name}"
}
```

`where` is a flat object of row property/path keys to scalar exact-match values. Supported comparison values are strings, numbers, booleans, and `null`.

Correct:

```json
"where": {
  "type": "boolean",
  "enabled": true
}
```

Do not use extraction or operator objects inside `where`:

```json
"where": {
  "path": "$.type",
  "equals": "boolean"
}
```

That shape is not a filter expression. It compares row properties named `path` and `equals`; it does not mean “read this path and compare it.” Use the row property or JSON-style path as the key instead:

```json
"where": {
  "$.type": "boolean"
}
```

If one response contains heterogeneous resource types, do not reuse the unfiltered source for type-specific actions. A boolean action should only offer boolean nodes, a number action should only offer number nodes, and so on. Create separate filtered sources when that produces a safer builder experience.

Option values must also be unique within a source. When the API may reuse the same id for different types, prefer a composite value such as `{type}:{id}` while keeping the raw id and type in `map.extra` for the request payload.

### Source Dependencies And Cache Correctness

List every field that can change a source request or its result in `source.dependsOn`. Dependencies are not only builder ordering metadata; they also participate in source refresh and cache identity.

For a state list selected by both instance and node:

```json
"dependsOn": ["instance", "state_node"]
```

Do not omit `instance` merely because selecting an instance usually refreshes the node dropdown. Two instances may expose the same node option value, such as `stateEvents:mini`. If the cache only depends on `state_node`, changing instances can reuse state options from the previous instance.

### Hidden Extra Values

Use `extra` to store stable ids or related values needed by later fields/actions.

```json
"map": {
  "rows": "$.payload.entries[]",
  "value": "{type}:{id}",
  "label": "{name}",
  "extra": {
    "state_node_type": "$.type",
    "state_node_id": "$.id",
    "state_node_name": "$.name"
  }
}
```

`extra` values can also use fallback paths or full extraction objects:

```json
"extra": {
  "stable_id": ["$.id", "$.uuid", "$.name"],
  "enabled": { "path": "$.enabled", "default": false, "transform": "boolean" }
}
```

If an action later needs the selected node id, use:

```json
"id": "{state_node_id}"
```

Do not use:

```json
"id": "{state_node.state_node_id}"
```

SheevChat placeholders are flat.

## Supported Map Paths

Map paths are simple JSON paths.

Supported examples:

```text
$
$.data
$.data.items
$.data.items[]
$.data.items[].id
$.payload.entries[]
$.payload.payload.states[]
$.items[0].id
```

Unsupported examples:

```text
$..id
$.items[?(@.type=="scene")]
$.items[*].id
```

Use `rows` plus `where` instead of JSONPath filters.

## Fields

Fields define the form users fill out when configuring an action.

```json
"fields": [
  {
    "key": "message",
    "label": "Message",
    "type": "text",
    "default": "Hello!"
  }
]
```

### Accepted Field Types

| Type | Purpose |
|---|---|
| `text` | Single-line text input. |
| `textarea` | Multi-line text input. |
| `number` | Numeric input. |
| `select` | Fixed dropdown defined directly on the field. |
| `dynamic_select` | Dropdown populated by a source. |
| `checkbox` | Boolean field. |
| `color` | Color picker field. |
| `file` | File path field. |
| `hotkey` | Hotkey capture field. |
| `hidden` | Hidden value field. |
| `info` | Informational field. |

### Field Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `key` | yes | string | Flat placeholder key. |
| `label` | yes | string | User-facing label. |
| `type` | no | string | Defaults to `text`. |
| `placeholder` | no | string | Placeholder text. |
| `default` | no | any | Default value. |
| `options` | no | array | Required for useful `select` fields. |
| `source` | no | string | Source id for `dynamic_select`. |
| `dependsOn` | no | array | Field keys this field depends on. |
| `labelKey` | no | string | Extra key that stores the selected option label. |
| `min` | no | number | Numeric minimum. |
| `max` | no | number | Numeric maximum. |
| `hint` | no | string | Help text. |

## Select Fields

Use `select` when choices are fixed and small.

```json
{
  "key": "operation",
  "label": "Operation",
  "type": "select",
  "default": "set",
  "options": [
    { "value": "set", "label": "Set" },
    { "value": "toggle", "label": "Toggle" }
  ]
}
```

For fixed dropdowns that must send a non-string JSON value, use `valueJson` instead of `value`.
This is useful for APIs that require real booleans or numbers in an exact placeholder.

```json
{
  "key": "muted",
  "label": "Mute State",
  "type": "select",
  "default": "toggle",
  "options": [
    { "value": "toggle", "label": "Toggle" },
    { "valueJson": true, "label": "Muted" },
    { "valueJson": false, "label": "Unmuted" }
  ]
}
```

When an action uses an exact placeholder, SheevChat preserves the selected JSON type:

```json
{
  "isMuted": "{muted}"
}
```

If the user selects `Muted`, the outgoing payload contains `"isMuted": true`, not `"isMuted": "true"`.
Use this pattern for toggle-state controls where the app accepts a named toggle value plus explicit set values.

### Remembered Toggle-State Selects

SheevChat gives the `Toggle` option special runtime behavior when one `select` field contains all three of these values:

- string `"toggle"`
- JSON boolean `true`
- JSON boolean `false`

This pattern is intended for mute, visibility, enabled, and similar state controls where users expect one button to flip state.

Runtime behavior:

- If the user selects `Toggle` and SheevChat has never seen that exact action target before, SheevChat sends `true` first.
- After that first toggle, SheevChat remembers the target state and alternates between `false` and `true`.
- If the user runs the same action with explicit `Muted`/`Unmuted`, `Visible`/`Hidden`, or another true/false pair, SheevChat updates the remembered state to match.
- State memory is per action target. Different selected channels, sources, devices, or other field values keep separate toggle memory.
- State memory is runtime/session memory, not a guaranteed read from the external app. If the external app changes outside SheevChat, use an explicit true/false action once to resync.

Use a stable target field such as `channel_id`, `source_id`, `device_id`, or `input_id` so SheevChat can keep independent toggle memory for each target.

## Dynamic Select Fields

Use `dynamic_select` when choices come from the app/API.

```json
{
  "key": "hotkey_id",
  "label": "Hotkey",
  "type": "dynamic_select",
  "source": "hotkeys",
  "labelKey": "hotkey_name"
}
```

`source` must match a key in top-level `sources`.

`labelKey` should match an `extra` key returned by that source when you want to retain the selected label.

## Cascading Dropdowns

Cascading dropdowns let one selection populate another.

Example:

1. Select model.
2. Fetch hotkeys for selected model.
3. Select hotkey.

```json
"sources": {
  "models": {
    "type": "connection_request",
    "connectionId": "local",
    "request": {
      "messageType": "CurrentModelRequest"
    },
    "map": {
      "value": "$.data.modelID",
      "label": "$.data.modelName"
    }
  },
  "hotkeys": {
    "type": "connection_request",
    "connectionId": "local",
    "dependsOn": ["model_id"],
    "request": {
      "messageType": "HotkeysInCurrentModelRequest",
      "data": {
        "modelID": "{model_id}"
      }
    },
    "map": {
      "value": "$.data.availableHotkeys[].hotkeyID",
      "label": "$.data.availableHotkeys[].name"
    }
  }
}
```

Action fields:

```json
"fields": [
  {
    "key": "model_id",
    "label": "Model",
    "type": "dynamic_select",
    "source": "models",
    "labelKey": "model_name"
  },
  {
    "key": "hotkey_id",
    "label": "Hotkey",
    "type": "dynamic_select",
    "source": "hotkeys",
    "dependsOn": ["model_id"],
    "labelKey": "hotkey_name"
  }
]
```

The child field's `dependsOn` must reference another field key in the same action.

## Actions

Actions are what users add to commands, automations, timers, or SheevPad buttons.

```json
{
  "id": "trigger_hotkey",
  "label": "Trigger Hotkey",
  "description": "Runs one hotkey.",
  "connectionId": "local",
  "contexts": ["timer", "automation", "command", "pad"],
  "fields": [],
  "execute": {
    "type": "local_websocket",
    "connectionId": "local",
    "message": {}
  }
}
```

### Action Fields

| Field | Required | Type | Notes |
|---|---:|---|---|
| `id` | yes | string | Stable action id. Runtime type becomes `<library-id>.<action-id>`. |
| `label` | yes | string | User-facing action name. |
| `description` | no | string | User-facing action description. |
| `contexts` | no | array | Defaults to all contexts. |
| `connectionId` | no | string | Connection used by the action. |
| `fields` | no | array | Config fields. |
| `execute` | yes | object | Executor definition. Must include `type`. |

## Contexts

Accepted action contexts:

| Context | Meaning |
|---|---|
| `timer` | Available to timers. |
| `automation` | Available to automations. |
| `command` | Available to custom commands. |
| `pad` | Available to SheevPad. |

Default:

```json
["timer", "automation", "command", "pad"]
```

## Event Subscriptions

Some apps expose pushed events, notification streams, or subscription APIs in addition to request/response actions. SheevChat recognizes top-level `eventSubscriptions` metadata so pack authors can document those future integration points without inventing fake actions.

Current runtime boundary:

- `local_websocket` subscriptions can be started and stopped explicitly through SheevChat's Action Libraries subscription API. SheevChat owns the socket, sends the declared subscribe message, receives JSON/channel messages, applies `eventMatch`, maps values with `eventMap`, and reports last-event diagnostics.
- `local_http_poll` subscriptions can be started and stopped explicitly through the same API. SheevChat owns the timer, sends the declared HTTP request on a capped interval, applies `eventMatch`, maps values with `eventMap`, and reports last-event diagnostics.
- `local_udp` subscriptions can be started and stopped explicitly through the same API. SheevChat owns a bounded local UDP listener, receives JSON/text/channel packets, applies `eventMatch`, maps values with `eventMap`, and reports last-event diagnostics.
- `local_tcp` subscriptions can be started and stopped explicitly through the same API. SheevChat owns the TCP socket, sends the declared subscribe message if present, receives bounded raw or line-framed JSON/text/hex/base64 messages, applies `eventMatch`, maps values with `eventMap`, and reports last-event diagnostics.
- `local_ipc` subscriptions can be started and stopped explicitly through the same API for approved IPC protocol presets. The first supported event runner is Discord RPC dispatch events over safe named-pipe discovery.
- `local_mqtt` subscriptions can be started and stopped explicitly through the same API. SheevChat owns the broker connection, subscribes only to declared allowlisted topics, receives bounded JSON/text/hex/base64 payloads, applies `eventMatch`, maps values with `eventMap`, and reports last-event diagnostics.
- Local event subscriptions start manually by default. Add `autoStart: true` when SheevChat should start the subscription after the library loads.
- Persistent socket-style subscriptions (`local_websocket`, `local_tcp`, `local_ipc`, `local_mqtt`) can opt into capped reconnect attempts with `reconnect: true`.
- Local event subscriptions can emit SheevChat Automations through the explicit `Imported app event` trigger. Automation matching uses the installed library id, event stream id, and mapped `eventMap` values. SheevChat applies a small per-stream internal throttle before routing these events to automations so rapid duplicate payloads cannot run unbounded action chains.
- Installed Apps can locally override stream enablement, auto-start, reconnect, and Automation routing without editing the manifest. These user settings are stored in SheevChat config and take precedence over pack defaults. Disabling a stream stops it immediately; disabling Automation routing keeps diagnostics running while preventing `Imported app event` automations from firing.
- `oauth_webhook` subscription declarations remain metadata-only until a server/relay-safe runner exists.

Accepted top-level shape:

```json
"eventSubscriptions": [
  {
    "id": "hotkey_event",
    "label": "Hotkey Event",
    "type": "local_websocket",
    "connectionId": "local",
    "autoStart": true,
    "reconnect": true,
    "reconnectDelayMs": 5000,
    "maxReconnectAttempts": 5,
    "subscribe": {
      "messageType": "SubscribeToHotkeyEvents"
    },
    "eventMatch": {
      "$.messageType": "HotkeyTriggeredEvent"
    },
    "eventMap": {
      "hotkey_id": "$.data.hotkeyID",
      "hotkey_name": "$.data.name"
    },
    "eventNamespace": "app_events",
    "eventTypes": {
      "hotkey_id": "string",
      "hotkey_name": "string"
    }
  }
]
```

Accepted `eventSubscriptions[].type` values:

| Type | Meaning |
|---|---|
| `local_websocket` | App can push events over a local WebSocket. Manual start/stop runner is available. |
| `local_http_poll` | App has pollable event/state endpoints. Manual start/stop runner is available. |
| `local_udp` | App can emit UDP messages to a local UDP listener. Manual start/stop runner is available. |
| `local_tcp` | App can push events over a local TCP socket. Manual start/stop runner is available. |
| `local_ipc` | App can emit events over an approved IPC protocol. Manual start/stop runner is available for supported presets. |
| `local_mqtt` | App can publish events to an MQTT/MQTTS broker topic. Manual start/stop runner is available. |
| `oauth_webhook` | Cloud API has webhook/subscription concepts. Metadata-only today. |

Fields:

| Field | Required | Notes |
|---|---:|---|
| `id` | yes | Stable subscription id. |
| `label` | no | Human-readable name. |
| `description` | no | Short explanation for future details views. |
| `type` | no | Defaults to `local_websocket`. |
| `connectionId` | yes for executable local subscriptions | `local_websocket` requires a `local_websocket` connection. `local_http_poll` requires a `local_http` connection. `local_udp` requires a `local_udp` connection. `local_tcp` requires a `local_tcp` connection. `local_ipc` requires a `local_ipc` connection using a supported protocol preset. `local_mqtt` requires a `local_mqtt` connection. |
| `autoStart` | no | Defaults to `false`. When `true`, SheevChat starts the subscription shortly after the library loads, reloads, imports, or installs. Startup still respects `enabled: false` and connection/auth failures. |
| `reconnect` / `autoReconnect` | no | Defaults to `false`. When `true`, persistent socket-style subscriptions reconnect after unexpected close until `maxReconnectAttempts` is reached. This is ignored for HTTP polling and UDP listeners. |
| `reconnectDelayMs` | no | Base reconnect delay in milliseconds, clamped from 1000 to 60000. Defaults to 5000. Each retry uses a capped linear backoff. |
| `maxReconnectAttempts` | no | Integer from 0 to 20. Defaults to 5. `0` means no retry attempts. |
| `subscribe` | no | For `local_websocket` and `local_tcp`, request object sent when the runner starts. Uses the same message shape as that transport's actions. For `local_http_poll`, accepted as a legacy alias for the poll request. |
| `request` | yes for `local_http_poll` unless `poll`/`subscribe` is used | HTTP request object sent on each poll. Uses the same shape as local HTTP actions, including `method`, `path`/`url`, `headers`, `body`, and `bodyEncoding`. |
| `poll` | no | Alias for `request` in `local_http_poll` subscriptions. |
| `listen` | yes for `local_udp` unless the connection `defaultUrl` supplies a port | UDP listener settings. `host` may be `localhost`, `127.0.0.1`, or `0.0.0.0`; `port` must be 1-65535. `responseEncoding`/`parse` can be `json`, `text`, `hex`, or `base64`. |
| `framing` / `responseFraming` | no for `local_tcp` | `raw` or `line`. Defaults to `connection.tcp.responseFraming`, then `connection.tcp.framing`, then `line` for subscriptions. |
| `topic` / `subscribeTopic` | yes for `local_mqtt` | MQTT topic filter to subscribe to. Must be allowed by `connection.mqtt.topics.subscribe`. |
| `responseEncoding` / `parse` | no for `local_mqtt` | `json`, `text`, `hex`, or `base64`. Defaults to `json`, falling back to text if JSON parsing fails. |
| `intervalMs` | no | Poll interval for `local_http_poll`. SheevChat clamps it between 1000ms and 60000ms. Defaults to 5000ms. |
| `unsubscribe` | no | Best-effort request object sent before WebSocket or UDP runners stop. Not used by HTTP polling. |
| `eventMatch` | no | Exact-match object keyed by JSON path. Non-matching frames are ignored. |
| `eventMap` | no | Same extraction-map shape as `responseMap`, used to document likely event variables. |
| `eventNamespace` | no | Stable namespace for future event variables. |
| `eventTypes` | no | Optional type labels for mapped event values. |
| `eventTemplate` | no | Optional compact text summary for diagnostics. Supports mapped event values. |

Use this section when an API documentation page clearly has a subscription/event endpoint. Do not replace normal query actions with event subscriptions. If the current pack needs values inside a command/action sequence now, create explicit query actions with `awaitResponse`, `query`, `responseMap`, and `responseNamespace`.

Manual runtime endpoints for development:

```text
GET  /api/action-libraries/subscriptions
POST /api/action-libraries/{libraryId}/subscriptions/{subscriptionId}/start
POST /api/action-libraries/{libraryId}/subscriptions/{subscriptionId}/stop
POST /api/action-libraries/{libraryId}/subscriptions/{subscriptionId}/test-event
POST /api/action-libraries/{libraryId}/subscriptions/{subscriptionId}/settings
```

`GET /api/action-libraries/subscriptions` returns developer diagnostics for every installed event stream:

| Response field | Notes |
|---|---|
| `libraryId` / `libraryName` | Installed action library that owns the stream. |
| `id` / `label` / `description` | Subscription identity from the manifest. |
| `type` | Runtime transport, such as `local_websocket`, `local_tcp`, `local_ipc`, or `local_mqtt`. |
| `connectionId` | Manifest connection used by the stream. |
| `enabled` / `runnable` / `status` | Manifest availability. `runnable: false` means the stream is metadata-only or failed validation. |
| `autoStart` | Whether SheevChat should attempt to start this stream after library load/reload/install. |
| `reconnect`, `reconnectDelayMs`, `maxReconnectAttempts` | Manifest reconnect policy after normalization and clamping. |
| `outputs` | Variables produced by `eventMap` / `eventTypes`, useful for future trigger and response wiring. |
| `runner.status` | Current runtime state: `not running`, `connecting`, `connected`, `listening`, `subscribed`, `reconnecting`, or `error`. |
| `runner.detail` / `runner.error` / `runner.errorCode` | Sanitized runtime diagnostics. These are intended for developers and advanced troubleshooting. |
| `runner.eventCount` | Count of matching events observed by this runner. |
| `runner.lastEventAt` | ISO timestamp for the most recent matched event. |
| `runner.lastEventValues` | Last mapped values produced by `eventMap`. |
| `runner.lastEventText` | Last rendered `eventTemplate`, if declared. |
| `runner.lastAutomationEventAt` | ISO timestamp for the last imported app event routed into the Automation engine. |
| `runner.lastAutomationSkippedAt` / `runner.lastAutomationSkippedReason` | Guard diagnostics when a mapped event was intentionally not routed, such as rapid duplicate throttling. |
| `runner.startedAt` | ISO timestamp for the active or most recent runner session. |
| `runner.reconnectAttempts` / `runner.nextReconnectAt` | Reconnect progress when `reconnect: true` is active. |

`POST /test-event` is a developer-only local test helper for Installed Apps. Send a JSON body with mapped sample values:

```json
{
  "values": {
    "scene_name": "Starting Soon",
    "source_visible": true
  }
}
```

SheevChat wraps those values in a synthetic local payload, applies the subscription `eventMatch` where possible, maps through `eventMap`, stores the result in diagnostics, and routes one `Imported app event` Automation trigger. It does not call the third-party application and it does not allow action packs to run code.

`POST /settings` stores local user overrides for a runnable event stream:

```json
{
  "enabled": true,
  "autoStart": false,
  "reconnect": true,
  "routeToAutomations": true
}
```

All fields are optional booleans, but at least one field must be supplied. Use this endpoint from SheevChat UI only; action packs cannot call it directly.

Installed Apps shows the same event-stream diagnostics in the Details panel, with Start/Stop controls, Send Sample, and local toggles for runnable local streams. Automations shows the `Imported app event` trigger when creating or editing an automation. Leave library/event-stream filters blank only when you intentionally want a broad catch-all; most packs should target one installed library and one event stream.

The Automation editor also shows an imported-app helper panel for this trigger. When no stream is selected, it summarizes available installed event streams. When a stream is selected, it shows the stream status, connection type, mapped `eventMap` outputs, suggested mapped-value filter keys, last runtime diagnostics, and the most recent mapped sample if one exists. This is intended to make local pack development easier without giving manifests arbitrary code execution.

Planned future work for production subscriptions still needs richer enablement controls and additional loop-guard UI, but action packs still cannot execute code. They only declare connection, parsing, and mapped event metadata; SheevChat owns the runtime and action execution.

Example `local_http_poll` subscription:

```json
"eventSubscriptions": [
  {
    "id": "status_poll",
    "label": "Status Poll",
    "type": "local_http_poll",
    "connectionId": "local",
    "intervalMs": 5000,
    "request": {
      "method": "GET",
      "path": "/api/status"
    },
    "eventMatch": {
      "$.event": "status"
    },
    "eventMap": {
      "state": "$.state",
      "count": "$.count"
    },
    "eventTemplate": "{state}:{count}"
  }
]
```

Example `local_udp` subscription:

```json
"eventSubscriptions": [
  {
    "id": "status_events",
    "label": "Status Events",
    "type": "local_udp",
    "connectionId": "local",
    "listen": {
      "host": "127.0.0.1",
      "port": 45555,
      "responseEncoding": "json"
    },
    "eventMatch": {
      "$.event": "status"
    },
    "eventMap": {
      "state": "$.state",
      "count": "$.count"
    },
    "eventTemplate": "{state}:{count}"
  }
]
```

UDP subscriptions are inbound local listeners, not arbitrary internet sockets. For safety, the listener host is restricted to localhost-style addresses. Use a normal `local_udp` action or source for outbound UDP request/response behavior.

Example `local_tcp` subscription:

```json
"eventSubscriptions": [
  {
    "id": "status_events",
    "label": "Status Events",
    "type": "local_tcp",
    "connectionId": "local",
    "subscribe": {
      "payload": { "event": "subscribe_status" },
      "encoding": "json",
      "framing": "line"
    },
    "responseEncoding": "json",
    "responseFraming": "line",
    "eventMatch": {
      "$.event": "status"
    },
    "eventMap": {
      "state": "$.state",
      "count": "$.count"
    },
    "eventTemplate": "{state}:{count}"
  }
]
```

TCP subscriptions are outbound local/LAN socket connections owned by SheevChat. They are best for apps that push newline-delimited status frames after a subscribe/hello message. TCP subscriptions support `autoStart`, capped `reconnect`, diagnostics, and `Imported app event` Automation routing when declared in the manifest.

Example `local_ipc` subscription:

```json
"eventSubscriptions": [
  {
    "id": "voice_events",
    "label": "Voice Events",
    "type": "local_ipc",
    "connectionId": "discord",
    "subscribe": {
      "cmd": "SUBSCRIBE"
    },
    "eventMatch": {
      "$.evt": "VOICE_STATE"
    },
    "eventMap": {
      "user": "$.data.user",
      "mute": "$.data.mute"
    },
    "eventTemplate": "{user}:{mute}"
  }
]
```

IPC subscriptions only run through approved protocol presets. Today that means Discord RPC dispatch events over safe named-pipe discovery. Manifests cannot define arbitrary IPC binary parsers.

Example `local_mqtt` subscription:

```json
"eventSubscriptions": [
  {
    "id": "light_events",
    "label": "Light Events",
    "type": "local_mqtt",
    "connectionId": "mqtt",
    "topic": "lights/+/state",
    "responseEncoding": "json",
    "eventMatch": {
      "$.event": "state"
    },
    "eventMap": {
      "light": "$.light",
      "enabled": "$.on"
    },
    "eventTemplate": "{light}:{enabled}"
  }
]
```

MQTT subscriptions are broker connections, not internet relays. The broker must still pass the local/LAN URL policy, and the subscription topic must be declared in `connection.mqtt.topics.subscribe`. MQTT subscriptions support `autoStart`, capped `reconnect`, diagnostics, and `Imported app event` Automation routing when declared in the manifest.

## Execute Types

Accepted executor type values:

| Type | Current behavior |
|---|---|
| `local_http` | Sends an HTTP request. |
| `local_websocket` | Sends a WebSocket message. |
| `local_ipc` | Sends a request through an approved local IPC protocol preset. |
| `local_udp` | Sends a bounded UDP packet. |
| `local_tcp` | Sends a bounded TCP message. |
| `local_mqtt` | Publishes a bounded MQTT message. |
| `sheevchat_command` | Runs an existing SheevChat command. |
| `oauth_api` | Sends an OAuth-authenticated HTTP request using the stored bearer token from an OAuth connection. |
| `hotkey` | Sends a bounded Windows keystroke sequence through SheevChat's macro sender. |
| `launch_application` | Launches a local app/file through SheevChat's bounded launcher. |
| `read_file` | Reads a small file from the library's scoped SheevChat storage folder. |
| `write_file` | Writes a small file inside the library's scoped SheevChat storage folder. |

For importable third-party app packs today, use:

```json
"type": "local_websocket"
```

or:

```json
"type": "local_http"
```

Use `local_ipc` only when the connection also uses `type: "local_ipc"` and the manifest declares the `local_ipc` permission.

or:

```json
"type": "sheevchat_command"
```

or, for Windows macro-style packs:

```json
"type": "hotkey"
```

or, for launching a user-selected local app/file:

```json
"type": "launch_application"
```

or, for writing small scoped state/output files:

```json
"type": "write_file"
```

or, for reading small scoped state/output files:

```json
"type": "read_file"
```

Every `execute` object must include `type`.

### Hotkey Execute Runtime

Use `execute.type: "hotkey"` when a pack needs to send a simple keyboard macro rather than call an app API. The manifest must declare the `hotkey` permission. A hotkey action does not need a connection; use no connection or a `hotkey`/`none` connection for status display only.

The action must provide `execute.sequence` or `execute.keys`. The sequence is interpolated with action fields and variables, capped to a short bounded string, and sent through the same Windows SendKeys runtime used by SheevChat's built-in keystroke action. `{WAIT 250}` or `{DELAY 250}` lines pause in milliseconds. Keystroke actions are Windows-only.

```json
{
  "permissions": ["hotkey"],
  "connections": [],
  "actions": [
    {
      "id": "send_macro",
      "label": "Send Macro",
      "fields": [
        {
          "key": "macro",
          "label": "Macro",
          "type": "hotkey",
          "required": true
        }
      ],
      "execute": {
        "type": "hotkey",
        "sequence": "{macro}"
      }
    }
  ]
}
```

Do not use hotkey packs for app APIs that expose HTTP, WebSocket, IPC, or UDP. Prefer the real API runtime whenever possible because hotkeys are focus-sensitive and can affect whichever window currently has focus.

### Launch Application Execute Runtime

Use `execute.type: "launch_application"` when a pack needs to open a local app, helper, or file that the user has selected on the SheevChat PC. The manifest must declare the `launch_application` permission. A launch action does not need a connection.

Launch actions use SheevChat's existing app launcher:

- `execute.path` is required and may use placeholders from fields or variables.
- `execute.args` is optional. Strings are split like a normal command line; arrays are passed as argument lists.
- `execute.workingDir` is optional.
- SheevChat starts the target with `shell: false`.
- On Windows, if the target is an `.exe` and the exact executable path is already running, SheevChat skips launching a duplicate instance.

```json
{
  "permissions": ["launch_application"],
  "connections": [],
  "actions": [
    {
      "id": "launch_tool",
      "label": "Launch Tool",
      "fields": [
        {
          "key": "tool_path",
          "label": "Application",
          "type": "file",
          "required": true
        }
      ],
      "execute": {
        "type": "launch_application",
        "path": "{tool_path}",
        "args": "--profile \"{profile_name}\"",
        "workingDir": "{working_dir}"
      }
    }
  ]
}
```

Do not use launch actions as a replacement for HTTP, WebSocket, IPC, UDP, or OAuth APIs. Prefer the real API runtime whenever possible because launch actions only start an app; they do not provide app state, dropdown discovery, or structured responses.

### Write File Execute Runtime

Use `execute.type: "write_file"` when a pack needs to save a small state file, debug output, generated payload, or handoff file for its own library. The manifest must declare the `write_file` permission. A write action does not need a connection.

Write actions are intentionally scoped:

- `execute.path` is required.
- The path must be relative.
- Absolute paths, drive letters, parent traversal, and null bytes are rejected.
- Files are written under SheevChat's managed per-library data folder, not arbitrary PC folders.
- Content is UTF-8 and capped to 256 KB at runtime.
- Use `execute.content` or `execute.text` for plain text.
- Use `execute.json` for JSON output.
- `execute.append: true` appends instead of replacing.
- `execute.newline: true` adds a trailing newline when missing.

```json
{
  "permissions": ["write_file"],
  "connections": [],
  "actions": [
    {
      "id": "save_state",
      "label": "Save State",
      "execute": {
        "type": "write_file",
        "path": "state/{user}.json",
        "json": {
          "user": "{user}",
          "value": "{value}"
        }
      }
    }
  ]
}
```

Do not use `write_file` to edit application config files, system files, OBS files, or user documents. If a target app requires file-based integration outside SheevChat's library storage, that should become a reviewed/hardened runtime or a dedicated first-party integration instead of an arbitrary manifest path.

### Read File Execute Runtime

Use `execute.type: "read_file"` when a pack needs to load a small state file, handoff file, debug output, or cached query result from its own library storage. The manifest must declare the `read_file` permission. A read action does not need a connection.

Read actions are intentionally scoped:

- `execute.path` is required.
- The path must be relative.
- Absolute paths, drive letters, parent traversal, and null bytes are rejected.
- Files are read only from SheevChat's managed per-library data folder, not arbitrary PC folders.
- Content is read as UTF-8 and capped to 256 KB by default and at maximum.
- `execute.parse` may be `text` or `json`; default is `text`.
- Text reads return `text`, `content`, `relativePath`, and `bytes`.
- JSON reads return the parsed object; non-object JSON returns `{ "value": parsed }`.
- Use `query: true`, `awaitResponse: true`, `responseMap`, `responseNamespace`, and output metadata when later actions should consume loaded values.

```json
{
  "permissions": ["read_file"],
  "connections": [],
  "actions": [
    {
      "id": "load_state",
      "label": "Load State",
      "execute": {
        "type": "read_file",
        "path": "state/{user}.json",
        "parse": "json",
        "query": true,
        "awaitResponse": true,
        "responseMap": {
          "savedValue": "$.value"
        },
        "responseNamespace": "state",
        "outputLabels": {
          "savedValue": "Saved value"
        },
        "outputTypes": {
          "savedValue": "string"
        }
      }
    }
  ]
}
```

Use paired `write_file` and `read_file` actions only for pack-owned state. Do not use `read_file` to inspect application config files, secrets, OBS files, or user documents.

Incorrect:

```json
"execute": {
  "channel": "nodes",
  "payload": {}
}
```

Correct:

```json
"execute": {
  "type": "local_websocket",
  "connectionId": "local",
  "channel": "nodes",
  "payload": {}
}
```

## Local WebSocket Execute

### JSON-RPC Must Use `message`

For JSON-RPC over WebSocket, put the complete protocol envelope inside `message`:

```json
"execute": {
  "type": "local_websocket",
  "connectionId": "local",
  "message": {
    "id": 101,
    "jsonrpc": "2.0",
    "method": "getChannels",
    "params": null
  },
  "awaitResponse": true,
  "responseMatch": {
    "$.id": 101
  }
}
```

Do not put a JSON-RPC `method` directly beside `type` and `connectionId`. At that level, `method` is transport metadata used by HTTP-style requests and is not part of the plain WebSocket message. The socket may connect successfully while the remote app receives a JSON-RPC object with no method and performs no command.

Use a distinct request id for each source or action and match the echoed response id. JSON-RPC notifications do not contain an id, so response matching prevents an unsolicited notification from being mistaken for the requested result.

For APIs that use normalized audio values, preserve the documented numeric range. Wave Link 3 volume and gain values use `0.0` through `1.0`, not `0` through `100`.

Plain JSON message:

```json
"execute": {
  "type": "local_websocket",
  "connectionId": "local",
  "message": {
    "messageType": "TriggerHotkeyRequest",
    "data": {
      "hotkeyID": "{hotkey_id}"
    }
  }
}
```

Channel-style message:

```json
"execute": {
  "type": "local_websocket",
  "connectionId": "local",
  "channel": "nodes",
  "payload": {
    "event": "payload",
    "type": "{state_node_type}",
    "id": "{state_node_id}",
    "payload": {
      "event": "set",
      "state": "{state_id}"
    }
  }
}
```

Optional:

```json
"awaitResponse": true
```

If `awaitResponse` is true, SheevChat waits for a matching response. Otherwise it sends and closes shortly after.

## Local HTTP Execute

```json
"execute": {
  "type": "local_http",
  "connectionId": "local",
  "url": "/api/action",
  "method": "POST",
  "headers": {
    "Content-Type": "application/json"
  },
  "body": {
    "id": "{item_id}",
    "mode": "{mode}"
  }
}
```

Supported methods are `GET`, `POST`, and `PUT`. `method` defaults to `GET` when omitted.

If `url` is relative, it is resolved against the connection `defaultUrl`.

For non-GET actions, `bodyEncoding` accepts the same values as HTTP source requests: `json`, `form`, `form_urlencoded`, `urlencoded`, `text`, or `raw`.

## Action Response Ingestion

Imported HTTP and WebSocket actions can map useful response values into the current action sequence.

Use `execute.responseMap` to extract values from the action response:

```json
"execute": {
  "type": "local_http",
  "connectionId": "local",
  "url": "/api/status",
  "method": "GET",
  "responseMap": {
    "current_scene": "$.scene.name",
    "streaming": "$.streaming"
  }
}
```

Set `query: true` when the main purpose of the action is to read state and produce variables for later actions. Query actions do not behave differently at runtime yet; the flag helps SheevChat label the action clearly in builders and library details.

Each mapped value can be one of three shapes.

Simple path string:

```json
"current_scene": "$.scene.name"
```

Fallback path array:

```json
"current_scene": ["$.scene.name", "$.currentProgramSceneName", "$.name"]
```

Full extraction object:

```json
"current_scene": {
  "paths": ["$.scene.name", "$.currentProgramSceneName"],
  "default": "Unknown",
  "transform": "string"
}
```

Accepted extraction object fields:

| Field | Required | Type | Notes |
|---|---:|---|---|
| `path` | no | string | One JSON-style path to try. Use either `path` or `paths`. |
| `paths` | no | array of strings | Fallback paths. SheevChat uses the first path that returns values. |
| `default` | no | any JSON value | Used when no path returns a value. |
| `transform` | no | string | One of `first`, `string`, `text`, `number`, `boolean`, `join`, `json`. Defaults to `first`. |
| `separator` | no | string | Used by `join`; defaults to `, `. |

Transforms:

| Transform | Result |
|---|---|
| `first` | Keep the first matched raw value. |
| `string` / `text` | Convert the first matched value to text. |
| `number` | Convert the first matched value to a number; uses `default` if conversion fails. |
| `boolean` | Accepts true/false, 1/0, yes/no, on/off, connected/disconnected, ok/error-style values. |
| `join` | Joins all matched values with `separator`. |
| `json` | Serializes the first matched value as JSON text. |

Mapped values are available to later actions in the same command, automation, timer, or SheevPad button:

```text
{last.current_scene}
{last.streaming}
```

SheevChat also stores a namespaced copy using the library id. If the library id contains hyphens, an underscore alias is also created:

```text
{obs-tools.current_scene}
{obs_tools.current_scene}
```

The action id is also available as a namespace:

```text
{get_status.current_scene}
```

You may override the library namespace with `responseNamespace`:

```json
"responseNamespace": "obs"
```

Then later actions can use:

```text
{obs.current_scene}
```

### Namespace Best Practices

Prefer one stable, short namespace for the library or integration, such as `obs`, `vts`, or `veadotube`. Keep output keys descriptive enough to coexist in that namespace:

```text
{veadotube.boolean_value}
{veadotube.number_value}
{veadotube.current_state}
```

Avoid a different namespace for every query when the values belong to the same integration:

```text
{boolean_value.value}
{number_value.value}
{current_state.state_id}
```

Fragmented namespaces make sequences harder to remember, document, autocomplete, and refactor. Use a specialized namespace only when a library contains genuinely separate domains or when output-key collisions cannot be resolved clearly.

The `{last.key}` and `{action_id.key}` aliases remain useful for immediate chaining and action-specific access. A stable library namespace is the durable interface authors should document.

Use `responseTemplate` when the action itself should send a chat response after mapping values:

```json
"responseTemplate": "Current scene: {current_scene}"
```

`responseTemplate` uses action-library placeholders. It can read mapped keys directly, such as `{current_scene}`, and sequence values such as `{user}`.

Use `outputLabels` when you want the action builder and Action Library details to show friendlier names for produced values:

```json
"execute": {
  "type": "local_http",
  "connectionId": "local",
  "url": "/api/status",
  "method": "GET",
  "query": true,
  "responseNamespace": "obs",
  "responseMap": {
    "current_scene": "$.scene.name",
    "streaming": "$.streaming"
  },
  "outputLabels": {
    "current_scene": "Current scene",
    "streaming": "Streaming state"
  },
  "outputDescriptions": {
    "current_scene": "The current OBS scene name.",
    "streaming": "Whether OBS is currently streaming."
  },
  "outputTypes": {
    "current_scene": "string",
    "streaming": "boolean"
  }
}
```

Output metadata fields:

| Field | Type | Notes |
|---|---|---|
| `outputLabels` | object | Friendly names keyed by `responseMap` key. |
| `outputDescriptions` | object | Short human descriptions for produced variables. |
| `outputTypes` | object | Human-readable value types such as `string`, `number`, `boolean`, `array`, or an app-specific type. |
| `query` | boolean | Marks the action as a state/query action in SheevChat UI. |

Output metadata should agree with the value actually produced. If `outputTypes.min` is `number`, use a numeric default such as `0` or omit the default when absence is meaningful; do not use an empty-string default while advertising the output as a number.

Keep sequence outputs compact. Mapping a small object with `transform: "json"` can be appropriate for advanced workflows, but avoid capturing unbounded lists, logs, binary data, or large base64 payloads unless the action explicitly exists for advanced data retrieval. Prefer identifiers, counts, names, hashes, dimensions, URLs, or narrowly selected values. Large outputs increase memory use, make summaries noisy, and are rarely suitable for interpolation into later actions.

Outputs are displayed in SheevChat as produced variables. For the example above, users will see variables like:

```text
{last.current_scene}
{obs.current_scene}
{get_status.current_scene}
```

After an imported action runs, SheevChat also sets sequence helper variables:

```text
{lastActionId}
{lastActionLabel}
{lastActionNamespace}
{lastActionOutputKeys}
{lastActionOutputSummary}
{lastActionMissingOutputKeys}
{lastActionValueCount}
{lastActionValuesJson}
{lastActionResponseJson}
{lastActionRaw}
```

`{lastActionOutputSummary}` is useful for quick debug chat responses, but action packs should prefer named variables such as `{last.current_scene}` or `{obs.current_scene}` for stable workflows.

`{lastActionValuesJson}` contains the compact mapped values object. `{lastActionResponseJson}` / `{lastActionRaw}` contain a compact raw response snapshot capped for safety. These are intended for developer diagnostics, debug chat responses, and advanced workflow glue. Do not build normal user-facing workflows around raw response JSON when a named `responseMap` output can be used instead.

If a query/awaited action returns a response but maps no values, SheevChat emits a developer warning. If only some mapped values are missing and the missing specs do not define a `default`, SheevChat records their keys in `{lastActionMissingOutputKeys}` and logs a developer warning. Optional values should include a type-compatible `default` when absence is normal.

When an imported action is run through SheevChat's action-library test/run endpoint, the same warning is returned in `result.developerWarning`. This is intended for local pack development so authors can see broken `responseMap` paths without digging through server logs. Marketplace users should not see these diagnostics as chat responses.

## SheevChat Command Execute

Use this when the imported action should run an existing SheevChat command.

```json
"execute": {
  "type": "sheevchat_command",
  "command": "!points",
  "platform": "event"
}
```

`platform: "event"` means use the triggering event platform.

## Interpolation

SheevChat replaces placeholders in strings:

```text
{field_key}
```

Fields, source extras, defaults, and runtime values are merged into one flat value object.

Good:

```json
"id": "{state_node_id}"
```

Bad:

```json
"id": "{state_node.state_node_id}"
```

Interpolation is recursive through objects and arrays.

Example:

```json
{
  "payload": {
    "event": "{operation}",
    "state": "{state_id}",
    "nested": ["{one}", "{two}"]
  }
}
```

An exact placeholder preserves the runtime value's native JSON type. This includes strings, booleans, numbers, arrays, and objects:

```json
{
  "enabled": "{enabled}",
  "amount": "{last.amount}",
  "payload": "{last.payload}"
}
```

### Nested Parent and Child Identifiers

Some APIs return resources nested under parent devices and require both identifiers in later commands:

```json
{
  "inputDevices": [
    {
      "id": "device-id",
      "name": "Wave:3",
      "inputs": [
        {"id": "input-id", "name": "Wave:3"}
      ]
    }
  ]
}
```

Do not assume the child id equals the parent id. A source mapped to `$.result.inputDevices[]` can provide the device id and friendly device name, but a source mapped to `$.result.inputDevices[].inputs[]` cannot currently copy the parent device id into each child option. Until parent/child flattening is supported by the core source mapper, expose the recognized parent device as a dropdown and obtain the nested child id from a query action or an explicit advanced field. Document this limitation in the action instead of silently sending the wrong identifier.

When designing core support for this pattern, flatten each child row with selected parent fields before applying `map.value`, `map.label`, and `map.extra`. This permits one friendly option to carry both `device_id` and `input_id` safely.

If `enabled` is `true`, the sent value is the JSON boolean `true`, not the string `"true"`. If `last.amount` is `4`, the sent value is the JSON number `4`.

Configured field values may themselves contain a sequence placeholder. Exact placeholders are resolved recursively, so a field configured as `{last.amount}` can feed a typed value from an earlier action into the execute payload.

Placeholders embedded inside surrounding text always produce text:

```json
"message": "Current amount: {last.amount}"
```

## Boolean APIs

Use a `checkbox` field and an exact placeholder when the API requires a real boolean:

```json
{
  "key": "enabled",
  "label": "Enabled",
  "type": "checkbox",
  "default": true
}
```

Then use the field as the complete property value:

```json
"value": "{enabled}"
```

Do not invent a `toggle` field type. Use separate fixed actions with literal `true` and `false` only when that is a deliberate UX choice, not as a workaround for interpolation.

## Complete Local WebSocket Skeleton

```json
{
  "schemaVersion": 1,
  "id": "example-app",
  "name": "Example App",
  "version": "1.0.0",
  "publisher": "Example Publisher",
  "category": "Example Controls",
  "description": "Adds Example App actions to SheevChat.",
  "homepage": "https://example.com/docs",
  "trust": "third-party",
  "permissions": ["local_websocket"],
  "connections": [
    {
      "id": "local",
      "label": "Example App",
      "type": "local_websocket",
      "defaultUrl": "ws://127.0.0.1:12345",
      "test": {
        "url": "ws://127.0.0.1:12345"
      }
    }
  ],
  "sources": {
    "items": {
      "type": "connection_request",
      "connectionId": "local",
      "cacheMs": 3000,
      "request": {
        "messageType": "ListItemsRequest"
      },
      "map": {
        "value": "$.data.items[].id",
        "label": "$.data.items[].name"
      }
    }
  },
  "actions": [
    {
      "id": "trigger_item",
      "label": "Trigger Item",
      "description": "Triggers one item in Example App.",
      "connectionId": "local",
      "contexts": ["timer", "automation", "command", "pad"],
      "fields": [
        {
          "key": "item_id",
          "label": "Item",
          "type": "dynamic_select",
          "source": "items",
          "labelKey": "item_name"
        }
      ],
      "execute": {
        "type": "local_websocket",
        "connectionId": "local",
        "message": {
          "messageType": "TriggerItemRequest",
          "data": {
            "itemID": "{item_id}"
          }
        }
      }
    }
  ]
}
```

## Complete Instance Discovery Skeleton

```json
{
  "schemaVersion": 1,
  "id": "example-dynamic-app",
  "name": "Example Dynamic App",
  "version": "1.0.0",
  "publisher": "Example Publisher",
  "category": "Example Controls",
  "description": "Controls a local app with dynamic instance files.",
  "homepage": "https://example.com/docs",
  "trust": "third-party",
  "permissions": ["local_websocket"],
  "connections": [
    {
      "id": "local",
      "label": "Example Dynamic App",
      "type": "local_websocket",
      "discovery": {
        "type": "instance_file",
        "directory": ".example-app/instances",
        "map": {
          "time": "$.time",
          "id": "$.id",
          "server": "$.server",
          "name": "$.name",
          "version": "$.version",
          "language": "$.language"
        },
        "clientName": "SheevChat",
        "staleMs": 10000
      }
    }
  ],
  "sources": {
    "instances": {
      "type": "instance_file",
      "connectionId": "local",
      "cacheMs": 1000
    },
    "items": {
      "type": "connection_request",
      "connectionId": "local",
      "dependsOn": ["instance"],
      "cacheMs": 3000,
      "request": {
        "channel": "items",
        "payload": {
          "event": "list"
        }
      },
      "map": {
        "rows": "$.payload.entries[]",
        "value": "{id}",
        "label": "{name}",
        "extra": {
          "item_id": "$.id",
          "item_name": "$.name"
        }
      }
    }
  },
  "actions": [
    {
      "id": "trigger_item",
      "label": "Trigger Item",
      "description": "Triggers one item on the selected instance.",
      "connectionId": "local",
      "contexts": ["timer", "automation", "command", "pad"],
      "fields": [
        {
          "key": "instance",
          "label": "Instance",
          "type": "dynamic_select",
          "source": "instances",
          "labelKey": "instance_name"
        },
        {
          "key": "item",
          "label": "Item",
          "type": "dynamic_select",
          "source": "items",
          "dependsOn": ["instance"],
          "labelKey": "item_name"
        }
      ],
      "execute": {
        "type": "local_websocket",
        "connectionId": "local",
        "channel": "items",
        "payload": {
          "event": "trigger",
          "id": "{item_id}"
        }
      }
    }
  ]
}
```

## API Analysis Checklist

When converting an app API into a SheevChat manifest, answer these first:

1. Is the app controlled by local HTTP, local WebSocket, local UDP, OAuth/cloud API, hotkeys, files, or app launch?
2. Does the app use a fixed port or dynamic port?
3. If dynamic, does the app publish directory-scanned instance files, a single endpoint file, mDNS/DNS-SD service records, or another discoverable source?
4. What request proves the app is connected and protocol-compatible?
5. What API calls list user-selectable things?
6. Which list fields are stable IDs?
7. Which list fields are display names?
8. Do any dropdowns depend on earlier dropdowns?
9. Does the action payload require booleans/numbers as real JSON values?
10. Does the API return channel-prefixed WebSocket messages?
11. Can the WebSocket emit greetings, acknowledgements, events, or unrelated frames that require `responseMatch`?
12. Does the WebSocket require an `Origin` or other safe custom handshake header?
13. Does the API need authentication, pairing, signing, or secrets?
14. Are actions useful in commands, automations, timers, and SheevPad?

If the answer requires platform OAuth, request signing, binary protocol support, persistent subscriptions, or a custom SDK, do not fake it in JSON. Document the required SheevChat runtime feature. If the app uses local token/session/pairing authentication over HTTP, WebSocket, IPC, or UDP, use `connection.auth`.

## AI Prompt Template

Use this with an AI agent and an app API link.

```text
Create a SheevChat action library manifest for this app API:

[PASTE API DOC LINK]

Output a single valid JSON `.sheevactions` manifest using schemaVersion 1.

If I ask for a branded packaged library instead, create a `.sheevactionszip` package containing the same JSON as root-level `manifest.json` plus one optimized brand image file. The image file can have any name, but it must use `.png`, `.jpg`, `.jpeg`, `.webp`, `.svg`, or `.gif`. Do not place files outside the zip root tree.

Do not output Markdown. Do not output comments. Do not output explanations before or after the JSON.

Use only the SheevChat manifest schema below:

- Top-level `sources` must be an object keyed by source id, not an array.
- Every action `execute` must include `type`.
- Supported practical execute types are `local_websocket`, `local_http`, `local_ipc`, `local_udp`, `local_tcp`, `local_mqtt`, `oauth_api`, `hotkey`, `launch_application`, `read_file`, `write_file`, and `sheevchat_command`.
- Supported practical connection types are `local_websocket`, `local_http`, `local_ipc`, `local_udp`, `local_tcp`, `local_mqtt`, `hotkey`, `oauth`, and `none`.
- Supported field types are exactly: `text`, `textarea`, `number`, `select`, `dynamic_select`, `checkbox`, `color`, `file`, `hotkey`, `hidden`, and `info`. Never invent a field type. Use `checkbox`, not `toggle`, for a boolean input.
- Supported action contexts are exactly: `timer`, `automation`, `command`, and `pad`.
- Supported response transforms are exactly: `first`, `string`, `text`, `number`, `boolean`, `join`, and `json`.
- For `local_http`, use only `GET`, `POST`, or `PUT`. Do not use `PATCH`, `DELETE`, `HEAD`, or other methods.
- Use `instance_file`, `endpoint_file`, or `pipeline` discovery only inside `connection.discovery`.
- Instance discovery directories are relative to the user's home directory. Use values such as `.veadotube/instances`. Never use `%USERPROFILE%`, `$HOME`, `~`, an absolute path, or a drive letter.
- Endpoint-file discovery reads one safe relative JSON file. Use `root: "home"` or `root: "localAppData"` plus `file`, such as `Packages/AppName/LocalState/ws-info.json`; never use `%LOCALAPPDATA%`, an absolute path, a drive letter, or `..`.
- Endpoint-file URL templates must be WebSocket URLs such as `ws://127.0.0.1:{port}` and every placeholder must be mapped or built in.
- Pipeline discovery reads safe local JSON files and loopback discovery responses. Use `base: "programData"` for `C:\ProgramData` bootstrap files; never put absolute paths or environment variables in `file`.
- Pipeline HTTP steps must resolve to loopback `http://` or `https://` URLs. Use `allowSelfSignedLoopback: true` only for loopback HTTPS discovery endpoints with local certificates.
- Use `connection.websocket.origin` for WebSocket Origin requirements. Do not put `Origin`, `Host`, `Connection`, `Upgrade`, or `Sec-WebSocket-*` in `websocket.headers`.
- For OAuth packs, use `connection.type: "oauth"` plus `connection.oauth` with `authorizationUrl`, `tokenUrl`, optional `revokeUrl`, `clientId`, `authorization_code_pkce`, `redirectMode: "loopback"`, and the provider's `tokenAuthMethod`; never include client secrets, access tokens, or refresh tokens in the manifest. If the provider requires a client secret, declare it under `connection.credentials`.
- Use `connection.auth` for local app token/session authentication. Do not invent top-level auth fields.
- For auth, use `tokenRequest` + `tokenMap` to save local tokens, `sessionRequest` + `sessionMap` when each WebSocket/session must be authenticated before dropdowns or actions, and `pairing.startRequest` + `pairing.pollRequest` when a local app exposes a device-code/user-code pairing flow.
- Use `connection.preflight` for hello/register/client-key messages that must be sent before each source/action but do not return a token or authenticated status.
- For `local_mqtt`, use `connection.mqtt.topics.publish` / `connection.mqtt.topics.subscribe` allowlists, keep broker URLs local/LAN, put credentials in `connection.credentials`, and do not create MQTT query actions.
- For `local_tcp`, use `tcp://host:port` local/LAN URLs, declare the `local_tcp` permission, choose `raw` or `line` framing, cap payload/response sizes, and use `awaitResponse: false` only for fire-and-forget controls.
- Use `execute.type: "hotkey"` only for bounded Windows keystroke macros, declare the `hotkey` permission, and provide `execute.sequence` or `execute.keys`.
- Use `execute.type: "launch_application"` only for launching a user-selected local app/file, declare the `launch_application` permission, and provide `execute.path`.
- Use `execute.type: "write_file"` only for writing small UTF-8/JSON files in the library's scoped SheevChat storage folder, declare the `write_file` permission, and provide a safe relative `execute.path`.
- Use `execute.type: "read_file"` only for reading small UTF-8/JSON files from the library's scoped SheevChat storage folder, declare the `read_file` permission, provide a safe relative `execute.path`, and set `execute.parse` to `text` or `json`.
- Use `fallbackUrls` when a local desktop app can bind to one of several documented ports.
- Source response mapping must be inside `source.map`.
- `source.map.value`, `source.map.label`, `source.map.extra`, and `execute.responseMap` can use path strings, fallback path arrays, or extraction objects with `path`/`paths`, `default`, `transform`, and `separator`.
- Query-style actions can use `execute.responseMap` to store response values for later actions. Prefer `{last.key}` for the next action, or set `responseNamespace` for stable names like `{obs.current_scene}`.
- For query-style actions with `responseMap`, add `execute.outputLabels` for user-friendly produced-value names when labels are obvious from the API.
- Use `responseTemplate` only when the action should send its own chat response.
- Use flat placeholders like `{state_node_id}`, never nested placeholders like `{state_node.state_node_id}`.
- Auth values use `{auth.token}` or `{auth.your_key}` placeholders.
- Use `map.extra` to expose hidden stable values from dynamic dropdown selections.
- Use `source.map.childRows` when parent rows contain nested child arrays and the action needs both parent and child identifiers. Store parent values with paths such as `$.parent.id` in `map.extra`.
- When a source response contains multiple resource types, use `source.map.where` or separate typed sources so each action only offers compatible options.
- `source.map.where` must map row property/path keys directly to scalar exact-match values, such as `"where": { "type": "boolean" }`. Never invent filter operators or use shapes such as `{ "path": "$.type", "equals": "boolean" }`.
- Source option values must be unique. If ids may overlap across resource types, use a collision-safe value such as `{type}:{id}` and retain the raw id/type in `map.extra`.
- Include every field that can affect a source request or result in `source.dependsOn`. For per-instance cascading data, this normally includes both `instance` and the selected parent resource so cached options cannot leak across instances with identical child ids.
- If using WebSocket channel messages, remember SheevChat wraps responses as `{ "channel": "...", "payload": ... }`, so map paths usually start with `$.payload`.
- If using plain JSON WebSocket messages, do not add `channel`; SheevChat sends the JSON object as-is.
- For JSON-RPC over WebSocket, put the complete `{ id, jsonrpc, method, params }` envelope inside `request.message` or `execute.message`, never at the request/execute root.
- Awaited JSON-RPC sources and actions should use `responseMatch` against the echoed id, such as `{ "$.id": 101 }`.
- If the app uses dynamic ports or multiple instances, include an `instances` source and make every app action include an `instance` field first.
- Prefer stable IDs over display names for option values.
- Exact placeholders preserve their native JSON type. Use `"{enabled}"`, `"{last.amount}"`, or `"{last.payload}"` as the complete property value for booleans, numbers, arrays, or objects. A placeholder embedded in surrounding text always produces text.
- When the API supports useful read operations, add query actions rather than producing a write-only pack. Every query action must set `awaitResponse: true`, `query: true`, a stable `responseNamespace`, `responseMap`, `outputLabels`, `outputDescriptions`, and `outputTypes`.
- Prefer one stable integration namespace across related queries, with descriptive output keys such as `{veadotube.boolean_value}` and `{veadotube.current_state}`. Do not create a separate namespace for every action unless the domains are genuinely independent.
- Output types and defaults must agree. Do not describe an output as `number` while using an empty-string default.
- Keep produced sequence values compact. Do not map unbounded lists, logs, binary content, or large base64 payloads unless advanced bulk retrieval is the explicit purpose of the action.
- Prefer approachable combined actions with an operation dropdown for general users. Use separate set/push/pop/toggle-style actions when the requested audience is advanced or when separate actions materially improve clarity.

Before returning the JSON, perform a consistency check:

- Every enum value is in an allowlist above.
- Every `connectionId`, dynamic source, `dependsOn` key, `labelKey`, placeholder, and response path resolves to something defined by the manifest or API response.
- Every type-specific action uses a compatible filtered source.
- Every source option value is stable and collision-safe.
- Every source dependency that can alter the request/result is included in `dependsOn`, including `instance` for per-instance cascading sources.
- Every query waits for a response and documents all produced outputs.
- Related queries use a consistent namespace, output metadata agrees with defaults, and mapped values are reasonably bounded in size.
- No unsupported field, executor, connection, context, transform, HTTP method, path form, or invented manifest key is present.

Required output:

1. Valid import-ready JSON only.
2. At least one connection.
3. Any sources needed for dynamic dropdowns.
4. Useful actions for commands, automations, timers, and SheevPad.
5. Clear action labels and descriptions.
6. Query actions and reusable typed outputs when the API supports meaningful reads.

If the API cannot be represented with current SheevChat action library support, output a short JSON manifest with no unsupported invented fields and include only actions that can work.
```

## DevTools Testing Commands

List installed libraries:

```js
fetch('/api/action-libraries').then(r => r.json()).then(console.log)
```

Reload libraries:

```js
fetch('/api/action-libraries/reload', { method: 'POST' }).then(r => r.json()).then(console.log)
```

Test a connection:

```js
fetch('/api/action-libraries/example-app/connections/local/test', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({})
}).then(r => r.json()).then(console.log)
```

Resolve a source:

```js
fetch('/api/action-libraries/example-app/sources/items/resolve', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ values: {} })
}).then(r => r.json()).then(console.log)
```

Resolve a cascading source:

```js
fetch('/api/action-libraries/example-app/sources/hotkeys/resolve', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    values: {
      model_id: 'abc123'
    }
  })
}).then(r => r.json()).then(console.log)
```

Run an imported action:

```js
fetch('/api/action-libraries/example-app/actions/trigger_item/run', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    item_id: 'abc123'
  })
}).then(r => r.json()).then(console.log)
```

## Validation Checklist

Before sharing a manifest:

- `schemaVersion` is `1`.
- `id` values use only lowercase letters, numbers, hyphens, and underscores.
- `sources` is an object, not an array.
- Every `dynamic_select.source` exists in `sources`.
- Every field `dependsOn` references another field in the same action.
- Every `connectionId` exists in `connections`.
- Every `execute` has a valid `type`.
- Every field uses one of the documented field types. Boolean inputs use `checkbox`, not `toggle`.
- Every app action has the right `connectionId`.
- Source maps are inside `map`.
- Type-specific actions use filtered sources so incompatible resources cannot be selected.
- Every `where` entry maps a row property/path directly to a scalar exact-match value; it contains no invented operator objects.
- Source option values are unique; potentially overlapping ids use a composite value such as `{type}:{id}`.
- Cascading sources include every cache-varying dependency, including `instance` when results are instance-specific.
- Channel WebSocket response paths include `$.payload`.
- Nested channel payload response paths use `$.payload.payload` when needed.
- Placeholders are flat.
- Exact typed placeholders occupy the complete property value when the API requires a boolean, number, array, or object.
- Query actions use `awaitResponse`, `query`, `responseMap`, a stable namespace, and complete output metadata.
- Related query actions use one stable integration namespace unless separate domains justify otherwise.
- Output types agree with defaults, and mapped values avoid unnecessary bulk/base64 data.
- Instance discovery directories are home-relative and contain no environment variables, home aliases, drive letters, or absolute paths.
- Remote URLs include `remote_network` only when intentionally needed.

## Failure-Driven Troubleshooting

When a generated manifest fails, preserve the failing example and turn the cause into four things: a clearer validator message, a prompt constraint, a wiki entry, and a regression test. This keeps the builder guidance aligned with the actual runtime instead of relying on examples alone.

Local imports are the developer testing path. When a locally imported pack resolves a dynamic source to an empty option list, SheevChat surfaces a developer warning in the action builder with the likely failure area. Marketplace and catalog installs do not show these empty-list warnings because an end user may not have the target app connected, authenticated, or populated yet.

When an integration cannot be represented by the current manifest schema, classify the gap before giving up or making a fake pack:

1. Existing manifest capability
2. Reusable runtime transport/protocol capability
3. Trusted native-plugin capability
4. Application-specific core integration
5. Unsupported or unsafe

Document the decision. If the answer is reusable runtime capability, expand the runtime once and keep the app-specific behavior in the manifest where possible.

| Failure | Cause | Correction |
|---|---|---|
| `fields[n].type is unsupported: toggle` | `toggle` is not a SheevChat field type. | Use `checkbox` for a boolean input or `select` for named operations. |
| Instance discovery finds nothing with `%USERPROFILE%` in `directory` | Discovery directories are already resolved under the user's home directory; environment variables are not manifest syntax. | Use `.veadotube/instances`. |
| A boolean action lists number or state nodes | One heterogeneous source was reused without filtering. | Add `map.where`, such as `{ "type": "boolean" }`, or create separate typed sources. |
| Typed dropdowns are empty even though the API returned matching rows | `where` used an invented operator object such as `{ "path": "$.type", "equals": "boolean" }`. | Use a direct scalar match: `{ "type": "boolean" }` or `{ "$.type": "boolean" }`. |
| Local import warning says `empty_rows_path` | Either `map.rows` is wrong, or a WebSocket greeting, acknowledgement, status event, or unrelated response was selected before the intended reply. | Compare the reported top-level keys with the documented response. If they belong to another frame, add a stable `responseMatch`; otherwise update `map.rows` to the actual array path. |
| Local import warning says `empty_child_rows_path` | Parent rows were found, but `map.childRows` returned no nested children. | Check the child array path relative to one parent row, such as `$.inputs[]` or `$.outputs[]`. |
| Local import warning says `empty_where_filter` | Rows were found, but every row was removed by `map.where`. | Confirm each `where` key exists on the row and that the expected value exactly matches the app response. |
| Local import warning says `empty_value_mapping` | Rows passed filtering, but `map.value` produced empty or duplicate values. | Point `map.value` at a stable non-empty id, or use a template such as `{type}:{id}` when ids can collide. |
| Local import warning says `empty_instance_file` | Instance discovery found no active app instances. | Start the target app, confirm the instance files exist, and verify `connection.discovery.directory` plus `discovery.map.server`. |
| A dropdown loses or replaces an option with the same id | The API reuses ids across resource types and the source value is not unique. | Use `{type}:{id}` as the option value and keep raw fields in `map.extra`. |
| Cascading options come from the previously selected app instance | The source cache does not vary by every field that affects its result. | Add `instance` and every selected parent field to `source.dependsOn`. |
| A read action sends successfully but produces no sequence variables | The action does not wait for or map its response. | Add `awaitResponse: true`, `query: true`, `responseNamespace`, `responseMap`, and output metadata. |
| A JSON-RPC WebSocket connects but every command does nothing | The JSON-RPC `method` was placed at the execute root and treated as transport metadata, so it was not included in the socket message. | Put the entire `{ id, jsonrpc, method, params }` envelope inside `execute.message`; for sources, put it inside `request.message`. |
| A JSON-RPC query consumes a notification instead of its response | The request did not correlate the echoed JSON-RPC id, and notifications can arrive on the same socket without an id. | Give the request a stable id and add `responseMatch`, such as `{ "$.id": 101 }`. |
| A microphone dropdown knows the device but a command also needs a nested input id | The API requires parent and child identifiers. | Use `map.childRows` and store both identifiers in `map.extra`, such as `device_id: "$.parent.id"` and `input_id: "$.id"`. |
| Related query variables are difficult to remember | Each query invented a separate namespace. | Reuse one short library namespace and choose descriptive output keys. |
| A value is documented as a number but sometimes becomes empty text | The extraction default conflicts with `outputTypes`. | Use a type-compatible default or omit it when missing data should remain absent. |
| Sequences or output summaries become unusually large | A query mapped an unbounded list, log, binary value, or base64 payload. | Map compact identifiers/metadata or make bulk retrieval an explicitly advanced action. |
| An API receives `"true"` or `"4"` instead of typed JSON | The placeholder was embedded in surrounding text, or the configured value was text rather than a typed exact placeholder. | Use the exact placeholder as the complete property value, such as `"{enabled}"` or `"{last.amount}"`. |
| A dynamic dropdown has no usable label or request id | `labelKey` or `map.extra` does not expose the selected row data needed by later fields/actions. | Map stable ids and display names in `extra`, then reference those flat keys. |

Do not merely patch the one generated manifest. Update this section whenever a new failure reveals a reusable rule.

## Known Current Limits

The schema recognizes more values than the imported action runtime fully supports. Today, third-party packs should focus on:

- local HTTP
- local WebSocket
- local IPC with approved protocol presets
- local UDP
- generic instance file discovery
- endpoint-file, pipeline, and mDNS discovery
- static options
- connection-request options
- cascading dropdowns
- user-entered credentials and `{auth.<key>}` substitution
- local pairing/device-code auth flows through `connection.auth.pairing`
- declarative HMAC signing and Tuya-style canonical signing
- refresh-token lifecycle for manifest-expressed refresh requests
- OAuth loopback PKCE login/token storage, refresh-token renewal, best-effort token revocation, and `oauth_api` bearer-token requests
- SheevChat command execution
- bounded Windows hotkey/macro execution
- bounded local application/file launching
- scoped read/write state files inside SheevChat-managed per-library storage
- manual local WebSocket, local HTTP poll, local UDP, and approved local IPC event subscription listening for developer/runtime diagnostics

These need additional runtime work before they can be considered generally supported for imported packs:

- arbitrary binary protocols without an approved preset
- event subscription auto-start, reconnect policy, trigger routing, and server/relay-backed webhook subscriptions
- custom SDKs
- arbitrary filesystem read/write outside SheevChat-managed per-library storage

If an app requires one of those, the action pack should document the gap rather than invent unsupported manifest fields.
