---
title: "wendy.json"
description: "wendy.json is the configuration file for a Wendy app."
---

# wendy.json

`wendy.json` is the configuration file for a Wendy app. It lives at the root of your project and describes the app's identity, target platform, required capabilities, and runtime behaviour.

## Example

```json
{
  "$schema": "https://wendy.dev/schemas/wendy.json",
  "appId": "my-app",
  "platform": "linux",
  "version": "1.0.0",
  "language": "swift",
  "entitlements": [
    { "type": "network" },
    { "type": "gpu" }
  ]
}
```

## Fields

### `appId` *(required)*

Unique identifier for the app.

```json
{ "appId": "my-app", "platform": "linux" }
```

### `version`

Version string for the app, e.g. `"1.0.0"`.

### `platform`

Target platform. One of:

| Value | Description |
|-------|-------------|
| `linux` | Linux edge device; the device architecture is inferred |
| `wendyos` | Compatibility alias for `linux`; passed to container builders as `linux` |
| `wendy-lite` | ESP32 target for native ESP-IDF or WASM apps |
| `darwin` | Native macOS app running through Headless Mac |
| `linux/arm64`, `linux/amd64`, etc. | Explicit Linux architecture target |

Use `"linux"` for WendyOS/Linux container targets. Omit to target the default Linux platform. Existing `"wendyos"` configs are accepted as an alias and resolve to `linux` before Docker or Apple Container builds.

Use `"darwin"` for native macOS targets managed by [Headless Mac](../../installation/wendy-agent-macos/). The CLI builds the app on a Mac development machine, syncs the build output to the Mac agent, and launches it as a native macOS process. Darwin apps run natively and non-containerized; they do not use the WendyOS Linux container runtime.

> **Headless Mac:** If the selected target is Headless Mac, `wendy run` rejects any `platform` value that does not resolve to `darwin` (for example, `linux/arm64` or `wendyos`). Set `platform: "darwin"` and use a native SwiftPM or Xcode project.

Minimal SwiftPM/Linux container configuration:

```json
{
  "$schema": "https://wendy.dev/schemas/wendy.json",
  "appId": "com.example.hello-linux",
  "version": "1.0.0",
  "language": "swift",
  "platform": "linux"
}
```

### `language`

Project language, e.g. `"swift"` or `"python"`. Used by the CLI to select the appropriate build toolchain.

### `debug`

Set to `true` to enable debug mode (default `false`). Injects debug tooling into the container via the `WENDY_DEBUG` build arg.

### `brewfile`

Optional Homebrew Bundle manifest for native macOS (`platform: "darwin"`) deployments. The path is relative to `wendy.json`; absolute paths and `..` components are not allowed.

```json
{
  "platform": "darwin",
  "brewfile": "Brewfile.wendy"
}
```

If `brewfile` is omitted and a `Brewfile.wendy` exists at the project root, `wendy run` auto-detects it for native SwiftPM and Xcode Mac deployments. A plain project-root `Brewfile` is left for developer-machine setup and is not applied to the target unless explicitly referenced. The CLI syncs the Wendy Brewfile to the target Mac and Wendy Agent runs `brew bundle --file <synced Brewfile>` before starting the app. Homebrew must already be installed on the target Mac; Wendy does not install Homebrew automatically. Linux/WendyOS container deployments ignore Brewfiles.

### `entitlements`

Array of capabilities the app requires. See [Entitlements](#entitlements-1) below.

### `frameworks`

Framework-level configuration, separate from `entitlements`. Where an entitlement grants access to a device capability, `frameworks` configures how a framework the app is built on is wired up on the device. ROS 2 is the only framework today.

```json
{
  "frameworks": {
    "ros2": {
      "domainId": 42,
      "rmw": "cyclonedds",
      "distro": "humble",
      "scope": "peripherals"
    }
  }
}
```

Every field is optional — `"frameworks": { "ros2": {} }` is a valid way to say "this is a ROS 2 app, use the defaults". The presence of `frameworks.ros2` is what marks a container as a ROS 2 app: the agent labels it, joins it to the right DDS domain, and starts the CLI sidecar that backs [`wendy device ros2`](../../integrations/ros2/).

#### `frameworks.ros2`

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `domainId` | integer (0–232) | derived from `appId` | `ROS_DOMAIN_ID` for the app. Omit it to get a stable value hashed from the app ID, which keeps the domain constant across restarts without colliding with unrelated apps. |
| `rmw` | string | `cyclonedds` | RMW middleware implementation. Accepts short names (`cyclonedds`, `fastrtps`/`fastdds`, `connextdds`, `gurumdds`) or full identifiers (`rmw_cyclonedds_cpp`, `rmw_fastrtps_cpp`, `rmw_connextdds`, `rmw_gurumdds_cpp`). |
| `distro` | string | `humble` | ROS 2 distribution the app targets, e.g. `humble`, `iron`, `jazzy`. Lowercase letters and digits, starting with a letter. Selects the matching CLI sidecar image. Only the shape is enforced, not a fixed list, so a new ROS 2 release is usable before Wendy ships explicit support for it. |
| `scope` | string | `device` | Who can see the app's topics. Each scope includes the ones before it; see the table below. |
| `discoveryScope` | string | — | **Deprecated**, use `scope`. `app` behaves as scope `device`, `host` as scope `network`. If both are set, `scope` takes precedence and `discoveryScope` is only read by older agents, so it must not be wider than `scope`: pair `network` with `host`, and the other scopes with `app`. Setting both warns. |

| `scope` | Who sees the topics | How |
|---------|---------------------|-----|
| `app` | Only this app (all its services) | Loopback discovery on the app's own domain, derived from `appId`. Do not set `domainId`. |
| `device` | Every app on the device that uses the same domain | Loopback discovery (`ROS_LOCALHOST_ONLY=1`). |
| `peripherals` | The device's apps and hardware attached to it, such as a robot or sensor on its own cable | Loopback reaches same-domain `device` apps; multicast discovery on the device's peripheral interfaces reaches physical wired interfaces other than the uplink that carries the default route. CycloneDDS only. On a device with nothing attached it behaves as `device`. |
| `network` | Anything on the device's networks, and the device's own apps | Subnet discovery on the device's network interface, plus loopback so the app also reaches `device` apps on the same domain (with host networking). |

Peripheral interfaces are detected, not configured: a robot on its own cable is found, but a robot on the same switch as the device's uplink, or one reached over Wi-Fi, is not; use `network` for those. A second ordinary network port without a default route counts as a peripheral interface.

`device`, `peripherals`, and `network` connect apps through the device's loopback, so they require host networking. The current runtime provides it when the app has no `network` entitlement, or when that entitlement uses mode `host` or `host-admin`; isolated modes such as `bridge` are rejected. Apps that share a bus through `device`, `peripherals` or `network` set the same `domainId`; without one each app gets its own domain.

> **CycloneDDS participant indexes:** Each CycloneDDS participant using `device`, `peripherals`, or `network` relies on the automatic participant-index range for its host and domain. CycloneDDS checks through `MaxAutoParticipantIndex` (default `9`). If one domain has enough participants to exhaust that range, split apps across explicit `domainId` values; Wendy does not yet expose this limit in its generated discovery config.

An out-of-range `domainId`, an unsupported `rmw`, a malformed `distro`, or an unknown `scope` is rejected when the config is parsed, rather than silently starting a container with no domain or middleware isolation.

For multi-service apps, declare `frameworks` per service under `services.<name>.frameworks` to give a service its own ROS 2 settings. A service without its own `frameworks` inherits the top-level one, so a stack of talker/listener services shares a domain by default.

```json
{
  "appId": "robot",
  "frameworks": { "ros2": { "rmw": "cyclonedds" } },
  "services": {
    "talker":   { "context": "./talker" },
    "bridge":   { "context": "./bridge", "frameworks": { "ros2": { "scope": "network" } } }
  }
}
```

Use [`wendy init --framework ros2`](../../advanced/clients/wendy-cli/commands/init/#frameworks) to scaffold this block, or add it by hand — `wendy run` picks it up on the next deploy. See [Wendy for ROS 2](../../integrations/ros2/) for inspecting and debugging the running system.

### `readiness`

Configures how the CLI determines when the app is ready after starting.

```json
{
  "readiness": {
    "tcpSocket": { "port": 8080 },
    "timeoutSeconds": 30
  }
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `tcpSocket.port` | integer (1–65535) | — | TCP port to probe |
| `timeoutSeconds` | integer | `30` | How long to wait before giving up |

For multi-service apps, declare `readiness` per service under `services.<name>.readiness` instead of (or in addition to) the top-level field. A top-level `readiness` becomes an app-level fallback that fires once after every service has started, rather than gating any single service — see [Readiness and lifecycle hooks](../../advanced/apps/wendy-services/#readiness-and-lifecycle-hooks) for the full scoping and attached/detached rules.

### `hooks`

Lifecycle commands to run at specific points during the app lifecycle.

```json
{
  "hooks": {
    "postStart": {
      "openURL": "http://${WENDY_HOSTNAME}:8080",
      "agent": "/app/post-start.sh"
    }
  }
}
```

| Field | Description |
|-------|-------------|
| `hooks.postStart.openURL` | URL to open in the developer's default browser after the app starts. Dispatched directly, without a shell, so it works uniformly on macOS, Linux, and Windows |
| `hooks.postStart.cli` | Command run on the developer's machine (through the platform shell) after the app starts |
| `hooks.postStart.agent` | Command run on the device after the app starts |

Prefer `openURL` over a `cli` command that shells out to a platform-specific opener (`open`, `xdg-open`, `start`) — those only work on one OS, and the CLI warns about them, suggesting `openURL` instead. When both are set, `openURL` fires first, then `cli` runs.

> **Note:** `hooks.postStart.agent` is executed directly on the device, not through a shell. Shell features such as pipes (`|`), redirects (`>`), command chaining (`;`, `&&`), and command substitution (`$(...)`) are **not** interpreted — they are passed through as literal arguments. If you need them, put the logic in a script file (e.g. `/app/post-start.sh`) and invoke that. `${WENDY_APP_ID}`, `${WENDY_HOSTNAME}`, `${WENDY_SERVICE_NAME}` (the declaring service's name; empty for single-container apps), and environment variables are still expanded.

For multi-service apps, declare `hooks` per service under `services.<name>.hooks` instead of (or in addition to) the top-level field. A top-level `hooks` becomes an app-level fallback that fires once after every service has started; its `postStart.agent` is ignored for multi-service apps, since there is no single app-level container start to trigger it — `wendy run` warns about this when it loads `wendy.json`. See [Readiness and lifecycle hooks](../../advanced/apps/wendy-services/#readiness-and-lifecycle-hooks) for the full scoping and attached/detached rules.

### `python`

Python-specific settings.

| Field | Description |
|-------|-------------|
| `python.sourceRoot` | Path to the Python source root directory |
| `python.container.sourceRoot` | Path to the Python source root inside the container image |

### `mcu`

An Arduino sketch for the board's microcontroller (Arduino UNO Q). `wendy run` compiles it, flashes the STM32, then deploys the app as usual. The layout matches Arduino App Lab: the `.ino` and a `sketch.yaml` in one folder.

```json
"mcu": { "sketch": "sketch" }
```

| Field | Description |
|-------|-------------|
| `mcu.sketch` | Sketch folder, relative to the project (required). It must hold a `sketch.yaml` that pins the platform, e.g. `arduino:zephyr (1.1.0)`. |
| `mcu.profile` | `sketch.yaml` profile to build. Default: its `default_profile`. |

- Wendy compiles with its own pinned `arduino-cli`, downloaded on first use. Your Arduino IDE setup is not touched.
- Supported cores: `arduino:zephyr` 1.0 and 1.1. If the sketch talks to Linux, list `Arduino_RouterBridge` in the profile; the core does not bundle it.
- The sketch is flashed before the container build. If the compile or the flash fails, the app is not deployed.
- The board needs a WendyOS and agent that can flash it; otherwise `wendy run` stops and asks you to run `wendy device update`.
- One board per run: `wendy run` refuses a project with `mcu` when `--device` lists several devices.
- With `--watch`, the sketch is compiled and flashed again only when a file in the sketch folder changes.
- The board holds one sketch. Deploying a second app with a sketch replaces the first app's sketch and stops that app. To go back, run `wendy run` in the first app's project again. `wendy device apps start <app>` never flashes: it is refused when the board does not hold that app's sketch. Removing an app also removes its sketch from the board. Stopping an app leaves its sketch running.
- `wendy build` compiles the sketch too; it needs no device.
- A `sketch.yaml` from Arduino App Lab works as it is. When it names no board, does not pin `arduino:zephyr`, or lists no libraries for a sketch that uses the Bridge, Wendy fills these in on a copy and prints a notice for each. Your file is not changed.
- To talk to the sketch from the app, add the [`arduino-bridge`](#arduino-bridge) entitlement.

### `resources`

Optional CPU, memory, and process-count ceilings the agent enforces on the container via cgroups. Edge devices are resource-constrained and often run several apps side by side, so capping a service keeps one busy or leaky app from starving its neighbours. Every field is optional. Omitting `memory` or `cpus` leaves that resource **unbounded** (the historical behaviour), so adding `resources` is backward compatible. `pids` is the exception: when omitted, a conservative default ceiling (4096) is applied as a fork-bomb guard — set it explicitly to raise or lower that ceiling.

```json
{
  "resources": {
    "memory": "512Mi",
    "cpus": "1.5",
    "pids": 256
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `resources.memory` | string | Hard memory limit. A number of bytes, optionally with a binary (`Ki`, `Mi`, `Gi`, `Ti`) or decimal (`K`, `M`, `G`, `T`) suffix — e.g. `"512Mi"`, `"1Gi"`. The container is OOM-killed if it exceeds this. |
| `resources.cpus` | string | Maximum number of CPU cores as a decimal — e.g. `"0.5"`, `"1.5"`, `"2"`. Enforced as a CFS quota over a 100 ms period (so `"1.5"` ⇒ 150 ms of CPU time per 100 ms). |
| `resources.pids` | integer | Maximum number of processes/threads the container may create. A cheap guard against fork bombs. **Defaults to 4096** when omitted; set a higher value for heavily-threaded workloads, or lower to tighten the cap. |

For multi-service apps, set `resources` at the top level as the default and/or per service under `services.<name>.resources`. App-level and service-level limits are merged **per field**: a field a service sets wins, and a field it leaves unset inherits the app-level value. This means a service can override one limit without silently dropping the others (e.g. an app-level PID cap stays in force even if a service only changes `memory`).

```json
{
  "appId": "fleet",
  "resources": { "memory": "1Gi", "pids": 512 },
  "services": {
    "web":    { "context": "./web" },
    "worker": { "context": "./worker", "resources": { "memory": "256Mi", "cpus": "0.5" } }
  }
}
```

Here `web` inherits the full app-level limits (`1Gi`, `pids: 512`). `worker` uses its own `256Mi` and `0.5` cores, and still **inherits** the app-level `pids: 512` it did not override.

### `env`

Environment variables injected into the container at start. These are deploy-time configuration, applied on top of whatever the image itself sets — unlike a Dockerfile `ENV`, changing them does not rebuild the image.

```json
{
  "appId": "my-app",
  "env": {
    "LOG_LEVEL": "info",
    "API_TOKEN": "${MY_API_TOKEN}"
  }
}
```

A value may reference an environment variable on the deploying machine as `${VAR}` (or `$VAR`); `wendy run` expands it at deploy time, which keeps secrets out of the file. An entry whose value expands to empty is dropped, so the container falls back to whatever the image sets rather than receiving an empty override.

Keys must be POSIX-portable environment variable names: letters, digits and `_`, not starting with a digit. The agent additionally reserves the `WENDY_`, `LD_` and `DYLD_` prefixes, and Wendy's own variables (`WENDY_APP_ID`, `WENDY_HOSTNAME`, and the OTel exporter settings) always win over an app-supplied value of the same name.

For multi-service apps, the top-level `env` is the default for every service and `services.<name>.env` overrides it **per key**, so a service can change one variable without dropping the rest:

```json
{
  "appId": "fleet",
  "env": { "LOG_LEVEL": "info", "REGION": "eu" },
  "services": {
    "web":    { "context": "./web" },
    "worker": { "context": "./worker", "env": { "LOG_LEVEL": "debug" } }
  }
}
```

`web` gets `LOG_LEVEL=info` and `REGION=eu`; `worker` gets `LOG_LEVEL=debug` and still inherits `REGION=eu`.

`wendy run --env KEY=VALUE` sets a variable for one run and overrides both.

### `$schema`

Optional URI pointing to the JSON Schema for editor autocompletion and validation. Set to `"https://wendy.dev/schemas/wendy.json"`.

---

## Entitlements

Entitlements grant the app access to hardware and system capabilities. Any capability not listed is unavailable to the app. They are code signed, preventing privilege escalation.

Run `wendy project` for a guided editor, or use `wendy project add`, `edit`, and `remove` with explicit flags. `wendy project validate` checks the result. Direct editing of `wendy.json` is also supported. See the [project editor](../../advanced/clients/wendy-cli/commands/project/).

### `network`

IP networking access.

```json
{ "type": "network" }
{ "type": "network", "mode": "host" }
{ "type": "network", "mode": "bridge" }
```

| `mode` | Description |
|--------|-------------|
| *(omitted)* | **Currently the same as `"host"`** — shares the host network stack, so every port the app binds is reachable on the device's real interfaces (LAN/internet). This implicit default is **deprecated**: see the note below. |
| `"host"` | Shares the host network stack (visibility: bind host ports, see interfaces). Does **not** grant the ability to reconfigure host networking. |
| `"host-admin"` | Host networking **plus** `CAP_NET_ADMIN` — allows reconfiguring interfaces, routes, and netfilter. Only request this if your app genuinely manages the network; it is a high-privilege capability. |
| `"none"` | Networking fully disabled — no namespace connectivity of any kind. |
| `"bridge"` | Isolated network namespace with a private IP, outbound internet via NAT, and working DNS — but **no** host/LAN-published ports. Use this for apps that need to reach the internet without exposing anything locally. Use Wendy Net networking for cross-device app access. |
| `"mesh"` | Private app networking through Wendy Net. Set `serviceCIDR` to `10.99.0.0/16`, the currently supported virtual service address range. `/16` fixes the first 16 address bits; this is not the device's LAN subnet. Publish incoming ports with `ports` to let apps on other devices connect. |

> **Security note:** `CAP_NET_ADMIN` (host network reconfiguration) is granted only by `"host-admin"`, never by plain `"host"`. Apps that previously relied on `CAP_NET_ADMIN` under `"host"` must switch to `"host-admin"`.

> **Deprecation notice:** an omitted `mode` maps to host networking today, and the agent logs a `WARN` at container create for any `network` entitlement without an explicit `mode`, flagging that its ports are publicly reachable. This default will change to isolated `"bridge"` networking in a future major release. If you want to keep host networking going forward, set `"mode": "host"` explicitly now; if you want isolated networking with outbound internet, opt in early with `"mode": "bridge"`.

Wendy Net UDP services declare `protocol: "udp"` on each published port:

```json
{
  "type": "network",
  "mode": "mesh",
  "serviceCIDR": "10.99.0.0/16",
  "ports": [{ "host": 47777, "container": 7777, "protocol": "udp" }]
}
```

The service listens on its container address (or `0.0.0.0:7777`). Other
Wendy Net apps send to this device's service address on UDP port `47777`. TCP remains
the default when `protocol` is omitted; TCP and UDP mappings with the same
host port are independent. Only a running app's explicitly published UDP
ports accept peer traffic. UDP DNS-SD announcements require a matching UDP
mapping and use service types ending in `._udp`.

#### mDNS service discovery

With isolated mesh networking, apps advertise their container IPv4 address and
container port using ordinary DNS-SD. Wendy projects the record with a local
mesh service address and the mapped host port. Clients must use that resolved
endpoint. Apps with their own mDNS sockets need no UDP 5353 mapping or `avahi`
entitlement. Apps using Avahi's D-Bus API can add the private `avahi` entitlement.

TXT values remain application data. IP addresses and extra ports embedded in
TXT are not rewritten, and each additional incoming port needs its own mapping.
See [service discovery](../../guides/wendy-net-services/) and the
[NVIDIA PAIR example](../../guides/nvidia-pair/) for a multi-port application.

#### UDP delivery limits

UDP messages can contain zero through 65,507 bytes. Delivery is unreliable:
applications must handle missing messages and retries. Large messages need more
bandwidth and are delivered only when complete; loss or congestion can drop the
whole message.
Large local messages may require the application to allow IPv4 fragmentation
on its own socket. Wendy Net does not change application DF policy or lower
the container interface MTU. Devices must support the same UDP message
protocol version; incompatible peers are rejected explicitly.
A source/destination tuple expires after 30 seconds without an app request;
its next request establishes a fresh authenticated flow. The first request after an idle period may take longer, especially over Bluetooth.
Use a WendyOS image with Wendy Net UDP support.

### `gpu`

Hardware-dependent GPU or board-telemetry access.

```json
{ "type": "gpu" }
```

| Host hardware | Grant |
|---------------|-------|
| NVIDIA Jetson | NVIDIA CDI specs, CUDA env vars, `/dev/nvidia*` |
| AMD (ROCm) | `/dev/kfd` (compute) and `/dev/dri/renderD*` (GPU), plus the `render`/`video` groups |
| Raspberry Pi | `/dev/vcio` (VideoCore mailbox) for board telemetry — power, voltage/current, temperature, throttling, Pi 5 PMIC ADC |
| Other | No hardware-specific grant |

On Raspberry Pi, `/dev/vcio` is bind-mounted only when present on the host; access is `rw` (no `mknod`).

### `npu`

Access to an on-SoC neural accelerator over FastRPC, for on-device inference.

```json
{ "type": "npu" }
```

| Host hardware | Grant |
|---------------|-------|
| Qualcomm (Hexagon DSP) | the non-secure `/dev/fastrpc-*` transport nodes and `/dev/dma_heap/system`, plus the `fastrpc`/`dmaheap` groups |
| Other | No hardware-specific grant |

The nodes are bind-mounted from the host, so the group ownership and POSIX ACL that authorise the open are preserved; access is `rw` (no `mknod`). The signed-PD `-secure` nodes are never granted. On a host with no FastRPC nodes the entitlement is inert, so it is safe to declare in an app that also deploys to a board without an NPU.

FastRPC identifies the board from the device-tree model and refuses a session without it, so the entitlement passes it in as `MACHINE_NAME`. The container keeps the default `/sys/firmware` mask, which also hides the DMI and ACPI trees.

It grants the device nodes plus the **driver-locked layer only** — what an app cannot obtain and what is tied to this host:

| Injected | Why |
| --- | --- |
| FastRPC transport, at `/opt/wendyos/npu/lib` | locked to the host kernel's driver |
| `/usr/share/qcom/conf.d/*.yaml` / `*.yml` and the board's `fastrpc_shell*` | locked to the board and its DSP firmware |
| The board's `libc++.so*` and `libc++abi.so*` under `/usr/share/qcom/<soc>/<vendor>/<board>/dsp/<domain>/` | Hexagon C++ runtime supplied with the board's DSP firmware |

Nothing is mounted into `/usr/lib` or `/usr/bin`. The transport prefix is prepended to `LD_LIBRARY_PATH`, so it wins over a bundled copy that may not match this kernel — that is deliberate. The board-locked files above, including the Hexagon C++ libraries, are bound at their canonical paths and **do** take precedence over an image's own copies of those same files; an app should not ship them. These DSP libraries are separate from any CPU-side C++ runtime the app needs in its base image.

Everything else is the app's to bundle and pin: `libQnn*`, `libQairt*`, `libGenie*`, the Hexagon skel matching the board's DSP arch, and any `genie-*`/`qnn-*` tools it runs. These are a free public SDK download. An app's skels are **not** shadowed; keep them outside `/usr/share/qcom` and point `ADSP_LIBRARY_PATH` at them, which resolves ahead of the board's tree.

The entitlement also sets `FASTRPC_PROCESS_ATTRS`, because the granted nodes only ever create an unsigned process domain. It is a bitmask: an app's valid decimal value is kept and the unsigned bit is OR'd into it, so set other flags freely. An absent, empty, or invalid value defaults to `8` (the unsigned bit).

Base image requirements: **glibc 2.38 or newer** (bookworm's 2.36 is too old), plus `libyaml-0.so.2` and `libbsd.so.0`, which the injected transport links against and which `debian:trixie-slim` does not ship — on Debian install `libyaml-0-2` and `libbsd0`.

### `camera`

Camera / V4L2 device access.

```json
{ "type": "camera" }
{ "type": "camera", "allowlist": ["/dev/video0"] }
{ "type": "camera", "user": "admin", "password": "secret" }
```

| Field | Description |
|-------|-------------|
| `allowlist` | Restrict access to specific device paths. Omit to allow all cameras. |
| `user` | Username for a registered IP camera. Ignored for local cameras. |
| `password` | Password for a registered IP camera. Ignored for local cameras. |

> **Security:** `user`/`password` set here are stored in **plaintext** in `wendy.json` and deployed as-is. Prefer the interactive `wendy device camera login` command, which keeps credentials out of the app config.

### `audio`

Microphone and speaker access.

```json
{ "type": "audio" }
```

### `bluetooth`

Bluetooth access.

```json
{ "type": "bluetooth" }
```

### `avahi`

For complete publication and browsing examples, see
[share and discover app services](../../guides/wendy-net-services/).

Provides a private Avahi DNS-SD service for an app with a private network
namespace. For example, a single-container app with outbound networking can use:

```json
{
  "entitlements": [
    { "type": "network", "mode": "bridge" },
    { "type": "avahi" }
  ]
}
```

Install the native Avahi or `avahi-compat` (`dns_sd.h`) client libraries in the
app image. The agent mounts a filtered D-Bus endpoint at `/var/run/dbus` and sets
`DBUS_SYSTEM_BUS_ADDRESS`; it prepares networking and waits for the private
daemon before releasing the application entrypoint. Missing host binaries,
missing IPv4 networking or a daemon readiness failure prevents startup. Host
networking is refused. Net apps also need their usual `mesh` network entitlement
and isolated network configuration.

Avahi and Bluetooth entitlements cannot be combined in one container because
both use `/var/run/dbus`. The current bridge supports root D-Bus clients, matching
Wendy's application OCI identity. Applications that drop to a different UID must
keep their Avahi client in a root process; non-root client authentication through
this proxy is not supported.

A daemon that exits during an app run is retried with a bounded restart budget.
If that budget is exhausted, or its private bus/proxy exits, the app continues
running with DNS-SD unavailable and the agent logs the failure. Restart the app
to recreate its private service.

After an agent restart, private discovery is rebuilt for surviving app containers;
clients must reconnect to D-Bus. A redeploy rejected during validation preserves
the running app's discovery. For an accepted replacement, the agent retires the
old private service and prepares the new one before releasing the new entrypoint.

DNS-SD hostnames and service names are discovery metadata, not authenticated device
identities.

### `nan`

Let a trusted app manage its own Wi-Fi Aware discovery and direct connections.
Ordinary Wendy Net networking does not need this entitlement. For direct control
and IP traffic, include both grants:

```json
{
  "entitlements": [
    { "type": "nan" },
    { "type": "network", "mode": "host" }
  ]
}
```

The agent prepares a separate Wi-Fi Aware data interface for the app. Your app
must create and manage its own discovery and connection sessions.

| Environment variable | Use |
| --- | --- |
| `WENDY_NAN_SOCKET` | Wi-Fi Aware control socket for commands and events. |
| `WENDY_NAN_NDI` | App-specific data interface for IP traffic. |
| `WENDY_NAN_CLIENT_DIR` | Shared directory where your control client must bind its reply socket. A private `/tmp` socket cannot receive replies from the host. |

Only the Wi-Fi Aware socket is exposed, not the host's ordinary Wi-Fi controls.
However, the grant does not filter commands: a read-only mount still allows radio
control. Grant it only to trusted apps. Do not issue radio-wide start, stop, or
reset commands while other apps or Wendy Net use the radio.

If the Wi-Fi service restarts, the agent restarts monitored apps to refresh their
socket access. Redeploy apps with restart policy `no` yourself. Stopping or deleting
the app removes its private data interface while leaving shared Wi-Fi Aware
available to other users.

### `persist`

Persistent storage that survives container restarts.

```json
{
  "type": "persist",
  "name": "my-app",
  "path": "/data"
}
```

| Field | Description |
|-------|-------------|
| `name` | Shared namespace (app ID). Apps with the same name can share storage. |
| `path` | Mount path inside the container, e.g. `"/data"`. |

### `usb`

USB device access.

```json
{ "type": "usb" }
```

### `i2c`

I2C bus access.

```json
{ "type": "i2c", "device": "/dev/i2c-1" }
```

| Field | Description |
|-------|-------------|
| `device` | I2C device path (required). |

### `serial`

Serial tty (UART) device access — e.g. a USB-serial adapter or servo bus (`pyserial`/termios). This is how apps do **UART**. See the [Serial / UART guide](../../device/entitlements/#serial--uart-entitlement).

```json
{ "type": "serial", "device": "ttyACM0" }
```

| Field | Description |
|-------|-------------|
| `device` | Bare USB tty node name, matching `ttyACM0` / `ttyUSB0` (required). USB-only; on-board UARTs (`ttyAMA`, `ttyS`) are not supported. Not a path. |

### `gpio`

GPIO pin access.

```json
{ "type": "gpio" }
{ "type": "gpio", "pins": [17, 27] }
```

| Field | Description |
|-------|-------------|
| `pins` | Pin numbers to expose. Omit to grant access to all GPIO chips. |

### `arduino-bridge`

Talk to the microcontroller's sketch through Arduino's router (Arduino UNO Q). The agent mounts `/run/arduino-router` and sets `APP_SOCKET`, which Arduino's Python package `arduino-router-bridge` reads. A value you set in `env` wins.

```json
{ "type": "arduino-bridge" }
```

No fields. On other boards, and on older WendyOS, `wendy run` refuses it before building.

```python
import os
from arduino.router_bridge import Bridge

bridge = Bridge(os.environ["APP_SOCKET"])
bridge.connect(timeout=5)
bridge.provide("climate", lambda celsius, humidity: print(celsius, humidity))  # sketch: Bridge.notify("climate", c, h)
bridge.call("buzz", 880, 200)                                                  # sketch: Bridge.provide_safe("buzz", buzz)
```

### `spi`

SPI device access via `spidev` (`/dev/spidev<bus>.<chipselect>`). Takes no options — grants the SPI subsystem as a whole. See the [SPI guide](../../advanced/device/entitlements/#spi).

```json
{ "type": "spi" }
```

> **Security note:** unlike `serial`/`i2c` (scoped to one node's `major:minor`), `spi` is a **whole-major** grant — an app with it can reach every SPI bus on the host.

### `input`

HID input device access (barcode scanners, keyboards, etc.).

```json
{ "type": "input" }
```

### `mcp`

Registers the container as a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server. When this entitlement is present, the wendy agent exposes the container's tools through `wendy mcp serve` so that AI assistants (Claude Desktop, etc.) can call them automatically.

```json
{ "type": "mcp", "port": 3000 }
```

| Field | Type | Description |
|-------|------|-------------|
| `port` | integer | TCP port on which the container's MCP server listens (required). |

The container must serve the [MCP Streamable HTTP](https://modelcontextprotocol.io/specification) transport on `0.0.0.0:<port>`. See the [MCPExample](../../advanced/Examples/MCPExample/) for a complete Python reference implementation.

> **Note:** The `mcp` entitlement is typically combined with `{ "type": "network", "mode": "host" }` so that the agent can reach the container's MCP port over loopback.

> **Result size cap:** `wendy mcp serve` enforces a 100,000-byte ceiling on
> results returned by container MCP tools, matching the default used by
> Wendy's built-in tools. Oversized results are replaced with a truncation
> envelope such as `{"truncated":true,"max_bytes":100000,"note":"…"}`.
> Design tools to return focused, paginated, or summarized output to avoid
> hitting this limit.

### `http`

Declares the app's primary HTTP port. The agent reports it over gRPC (`AppContainer.http_port`) for any client — including `wendy device apps` and remote management apps — to discover and open. An attached `wendy run` uses it automatically: it waits for the port to accept connections before printing "App reachable at ..." and opens it in your default browser, with no extra `hooks.postStart` configuration required. If readiness times out, the CLI warns but does not print the success message or perform this automatic browser open.

```json
{ "type": "http", "port": 8080 }
```

A detached agent deployment includes declared HTTP ports in its
[reported endpoints](../../advanced/clients/wendy-cli/commands/run/#detached-output), using
host loopback forwarding for user-networked VMs. Reporting does not check HTTP
health or open a browser. A TCP readiness port alone does not declare an HTTP
endpoint; an explicit hostname-based HTTP(S) `openURL` can also supply one.

> **Networking:** Browser reachability currently requires `{ "type": "network", "mode": "host" }`. Bridge mode provides outbound NAT only and does not publish host/LAN ports. A `network.ports` mapping in mesh mode serves mesh-peer traffic; it does not make the URL reachable from the developer's browser.

If the app configures an explicit hostname-templated `hooks.postStart.openURL`, that URL wins for display and browser opening. Otherwise the HTTP entitlement port wins, followed by `readiness.tcpSocket.port`. An explicit readiness TCP port remains the probe target, so an app can probe port `9000` while advertising and opening its HTTP UI on port `8080`.

### `display`

Present to a locally-attached monitor as a Wayland client (GPU-accelerated) — the app or shell draws directly to the screen, no web browser involved.

```json
{ "type": "display" }
```

The container receives:

- `/dev/dri` (GPU render nodes); cgroup access is `rw`, no `mknod`.
- Membership in the `video` and `render` groups.
- The WendyOS compositor's Wayland socket, exposed via `WAYLAND_DISPLAY` / `XDG_RUNTIME_DIR`.

On NVIDIA Jetson the GL/EGL userspace is injected from the host through the same CDI path as `gpu`; on Raspberry Pi the app's own mesa works against the vc4 kernel driver.

| Constraint | |
|------------|--|
| At most one `display` per app | enforced at validation |
| Display-enabled image | the Wayland socket is present only on display-enabled WendyOS images; on a headless image the entitlement is accepted but nothing renders |

> **Security:** apps **without** `display` never receive `/dev/dri` — the default GPU/display sandbox is unchanged.

### `notifications`

Allows an app to send operator-facing Wendy Notifications through its private
app connection.

```json
{ "type": "notifications" }
```

The agent/daemon mounts `/run/wendy/system` read-only and injects
`WENDY_SYSTEM_SOCKET=/run/wendy/system/system.sock`. There is one socket per
app, shared by that app's entitled service containers and by future app-facing
API capabilities; it is not one socket per capability. The public Swift
API is `WendyNotification.send(_:)` in WendyKit, so normal apps do not call
gRPC directly.

The request supplies one or more user, organization team, or organization role
selectors, plus title, body, severity, deep link, a caller-chosen
`notification_id` UUID v4 resource identity, and optional structured metadata.
After successful creation, every reuse of its canonical UUID—including identical,
changed, or differently cased requests—returns `ALREADY_EXISTS`; it does not
replay success. A local validation or rate-limit rejection occurs before Cloud
and leaves that UUID valid for retry. Selector categories have union semantics.
The agent accepts at most 100 selector entries, then normalizes and deduplicates
them; Cloud resolves at most 10,000 recipients. An app cannot supply its own
app, device, or organization identity; the agent and Cloud derive those from
trusted device identity.

The socket is recreated after an agent restart, so running containers reconnect
on their next call without a redeploy.

> **Security:** `notifications` exposes only entitled app-facing APIs. It does
> not expose `WENDY_AGENT_SOCKET` or any app/device administration RPC.

### `episode-write`

Allows an app to write into the device's recorded dataset: it submits structured
events and predictions to the episode recorder through an app-private Unix
socket.

```json
{ "type": "episode-write" }
```

The agent mounts `/run/wendy/data` read-only and injects
`WENDY_DATA_SOCKET=/run/wendy/data/data.sock`. The protocol accepts records up
to 64 KiB and acknowledges them as `buffered`, `recorded`, or `rejected`.
Buffered records provide application pre-roll and can trigger deployed Wendy
Data campaigns — which is why the grant is stronger than plain logging, since
triggers match on application event names and prediction attributes, so an app
holding it can start recordings. The socket is restored from container labels
after agent restart; the entitlement never exposes the administrative agent
socket, and grants no sensor read access.

To record custom payloads, declare named streams inside the entitlement:

```json
{
  "type": "episode-write",
  "streams": {
    "temperature": {
      "mode": "durable",
      "mediaType": "application/json",
      "storage": { "maxBytes": 67108864, "retentionSeconds": 86400 }
    }
  }
}
```

The agent injects `WENDY_DATA_STREAM_DIR`; each stream uses
`<directory>/<stream>.sock`. Lightweight streams accept native payload bytes.
Durable streams use Protobuf envelopes, acknowledge after local storage sync,
and retain records even when no episode is active. They reject new records
when their journal is full, so producers must buffer and retry.

See [Wendy Data](../../data/#record-data-from-your-app) for the recording workflow,
the [stream reference](../../advanced/clients/wendy-cli/commands/data/#application-recording-streams)
for retention and export, and the
[WendyRecording examples](https://github.com/wendylabsinc/WendyOS/tree/main/Examples/WendyRecording)
for time-series schemas and senders.

### `admin`

Grants the container the wendy-agent's **full gRPC over a local unix socket** (`/run/wendy/agent.sock`, exposed as `WENDY_AGENT_SOCKET`) — with **no authentication**.

```json
{ "type": "admin" }
```

An app with `admin` can start, stop, and delete apps and read all device data locally. The socket is bind-mounted **only** into containers that declare `admin` — that mount is the entire trust boundary — and it is never reachable off-device (a unix socket, not TCP). At most one `admin` per app.

> **Security:** `admin` is a privileged, deliberate grant equivalent to local device control. Grant it only to fully-trusted first-party apps (e.g. the WendyOS shell). Requires an agent build that serves the local socket.

---

### `build`

Runs a container image builder (BuildKit) inside the app container.

```json
{ "type": "build" }
```

Grants `CAP_SYS_ADMIN` and un-denies the `unshare` / `clone(CLONE_NEWUSER)` syscalls a nested builder needs (the kernel-module and `kexec` denials are kept). **Privileged-equivalent: a container→host escape surface.** Used so a device can build apps for itself. Grant only to fully-trusted, first-party apps. At most one per app; takes no parameters.

## Compose-based projects

If your project uses a `docker-compose.yml` instead of a single container, you don't need a `wendy.json`. `wendy run` detects the compose file automatically and each service gets a generated app config derived from its `ports`, `network_mode`, and `volumes` declarations.

See [Multi-Service Apps with Docker Compose](../../advanced/apps/compose/) for details.

---

> **Deprecated:** `{ "type": "video" }` — use `camera` instead.
