# Quickstart
Source: https://docs.wendy.dev/latest/
---
title: Quickstart
description: Install Wendy and watch a simulated robot move, then connect your own device and deploy an app.
---
## What is Wendy?
Wendy builds your code, deploys it to a robot or edge device, and streams its logs
back to your computer. Start by watching a simulated Go2 move in your browser.
You do not need physical hardware or a Wendy Cloud account for the tour.
Wendy has three parts:
- **On your device: WendyOS**, an open source Linux operating system for NVIDIA Jetson and Raspberry Pi. Already running Linux? Install Wendy Agent instead.
- **On your computer: the `wendy` CLI**, which sets up devices and deploys your apps.
- **Optional: Wendy Cloud**, for deploying to and managing devices from anywhere.
Write your app in Python, Swift, Rust, C++, TypeScript, or anything else that runs in a container.
Install the CLI below, then take the robot tour. If you already have hardware,
you can go straight to [device setup](#2-set-up-a-device) and deploy a small app.
## 1. Install the Wendy CLI
```bash
curl -fsSL https://install.wendy.dev/cli.sh | bash
wendy --version
```
```powershell
winget install WendyLabs.Wendy --source winget
```
The managed robot VM tour needs macOS or Linux. On Windows, follow device setup
below or try the [local Docker robot demo](https://wendy.dev/quickstart?target=simulator).
If `wendy --version` is unavailable after installation, open a new terminal and retry.
## Try the robot tour
`wendy tour` and `wendy run` check Docker and QEMU, and offer to install what is
missing: Docker Desktop and QEMU through Homebrew on macOS, or QEMU through
apt-get or dnf on Linux. On Linux, install and start
[Docker](https://docs.docker.com/get-started/get-docker/) with Buildx first.
Python runs inside the container, so you do not need it on your computer.
Allow four virtual CPUs, 4 GiB of VM memory, and space for the downloaded OS and
container images. Apple Silicon runs the ARM64 VM with hardware acceleration;
x86 computers use slower emulation. The initial downloads and builds can take
several minutes. See [virtual-device setup](./installation/wendyos-virtual-machine/)
for more details.
```bash
wendy tour
```
Choose the recommended simulated robot path. The tour creates a Go2 virtual
device, opens its 3D viewer, and starts a five-second walk. Watch the robot move
in the browser. The tour verifies a change in position and stops the movement
before reporting success.
The movement settings are in `~/Documents/wendy-robot-tour/tour.json`. Change
`turn_rps` to `0` to walk straight, then repeat the tour with the same virtual device:
```bash
wendy tour --simulator
```
The VM and its viewer remain available after the check. Use the exact
`wendy vm stop` command printed by the tour when finished. To keep a separate
starter, pass `--project DIR`. You can add a coding agent or cloud connection later.
## 2. Set up a device
For guided hardware setup, run `wendy tour --hardware`, or choose a path below.
The app below builds with Docker and Buildx. On macOS, `wendy run` offers to
install Docker Desktop; on Linux, install and start Docker yourself. The hardware
guides list the storage, power, and cables needed for each board. Installing
WendyOS erases the selected target storage.
No hardware needed. On macOS and Linux, Wendy creates the simulator for you in
step 4 and offers to install QEMU if it is missing.
On Windows, run the Linux Wendy CLI and your container builder inside WSL2 for
this quickstart; Wendy offers to install QEMU there as on Linux. Native Windows
[foreground VMs](./installation/wendyos-virtual-machine/#windows-foreground-vms)
support device inspection, but local app builds and automatic app-port forwarding
require macOS or Linux.
This generic virtual device runs your app without a simulated robot or physical
camera, GPU, or GPIO. Use the tour above for a moving Go2.
[More about virtual devices](./installation/wendyos-virtual-machine/)
For Jetson Orin Nano and AGX Orin developer kits, use full USB recovery to update
boot firmware and the OS together:
1. Fit the NVMe SSD in the board, or use onboard eMMC on AGX Orin. Connect the
board's recovery USB-C port to your computer with a data cable and connect power.
2. Enter recovery mode using your board's [connection and button instructions](./installation/wendyos-nvidia-jetson-orin-nano/#full-usb-recovery).
3. Run `wendy install`, choose your Jetson and **full USB recovery**, then review
the storage to erase. No external NVMe adapter is needed for this path.
4. Keep power and USB attached. Follow the installer's reboot instruction, then
run `wendy discover` and confirm that the expected board appears.
The separate rootfs-only workflow requires compatible boot firmware and does
not update it. Follow the full guide before choosing that advanced path.
[Full guide](./installation/wendyos-nvidia-jetson-orin-nano/)
1. With the Thor off, connect your computer to the USB-C port next to HDMI. Connect power to the other USB-C port.
2. Enter recovery mode: press Power, hold the middle Force Recovery button, tap Reset, then release Force Recovery.
3. Run `wendy install` and choose the Thor. It flashes over USB, so you need no external drive.
4. When flashing finishes, power-cycle the Thor. After 1–2 minutes, confirm that `wendy discover` lists it.
[Full guide](./installation/wendyos-nvidia-jetson-agx-thor/)
For Raspberry Pi 5, 4, or 3:
1. Insert the microSD card into a reader connected to your computer.
2. Run `wendy install`, choose your Pi, then carefully select the card to erase.
Configure Wi-Fi now if you will use it. Cloud enrollment is optional.
3. Put the card in the powered-off Pi. A Pi 4 or 5 can connect through a USB-C
data cable if the computer supplies enough power. Otherwise use the Pi's
power supply and Ethernet or Wi-Fi. A Pi 3 connects over the network.
4. After 1–2 minutes, confirm that `wendy discover` lists it.
[Full guide](./installation/wendyos-raspberry-pi-5/)
Use any Linux machine, such as Ubuntu 22.04 or later on ARM64 or x86-64. The video uses an NVIDIA DGX Spark. Run this on that machine to install the agent with network access, so your development machine can reach it:
```bash
curl -fsSL https://install.wendy.dev/agent.sh | bash -s -- --network-access network
```
Keep it on the same network as your development machine, and confirm that `wendy discover` lists it. [Full guide](./installation/linux/)
Using a [Mac](./installation/wendy-agent-macos/), an [ESP32](./installation/wendy-lite-esp32/), or a [Unitree G1](./installation/wendy-agent-unitree-g1/)? Each has its own guide.
For local discovery, allow **Local Network** access on macOS. On Linux, run
`wendy discover` in a terminal and accept the host USB-C network setup prompt
when using that connection. A successfully written image is only the first
step; wait until your expected device answers before deploying an app.
## 3. Create an app
```bash
wendy init hello-wendy --target wendyos --language node --template simple-api --assistant skip --git-init no
cd hello-wendy
```
This creates a small TypeScript web server. You do not need Node.js installed, because Wendy builds it in a container.
## 4. Run it
```bash
wendy run
```
Select your device and press Enter.
For the simulator, press Tab to open the **Simulator** tab, press **c** to create it, and choose **Generic WendyOS**. The first run downloads the WendyOS image, so it takes a few minutes.
Wendy builds the app, deploys it, and streams its logs. When the app is ready, Wendy prints `App reachable at` with its URL and opens it in your browser:
```json
{"message":"hello-world"}
```
That response came from your device. Edit `src/index.ts`, then run `wendy run --watch` to redeploy every time you save.
Stuck? See [troubleshoot your first deployment](./errors/).
## Next steps
Enroll a device and deploy remotely. Existing users can follow the Legacy tab and migration guide.Find the tools for sensors, data collection, BI, mobile operation, and AI development.Connect the hosted Wendy plugin to your devices through a CLI session.Tutorials for Python, Swift, Rust, C++, TypeScript, and Mojo.Configure local links, share Internet access, and connect apps across devices.Give your app access to device hardware.Capture sensor data, application events, and model results around a trigger.Set breakpoints in VS Code while your app runs on the device.Devices, `wendy.json`, and the build and deploy loop.
---
# Detailed reference
Source: https://docs.wendy.dev/latest/advanced/
---
title: "Detailed reference"
description: "Generated and low-level WendyOS reference documentation."
---
# Detailed reference
Behavioral explanations and lower-level WendyOS documentation live here.
## Reference areas
- [Wendy CLI](./clients/wendy-cli/global-flags.md)
- [App configuration](./apps/wendy.json.md)
- [Wendy Cloud](./cloud/requirements.md)
- [WendyOS internals](./wendyos/requirements.md)
- [Development](./development/)
---
# Multi-Service Apps with Docker Compose
Source: https://docs.wendy.dev/latest/advanced/apps/compose/
---
title: "Multi-Service Apps with Docker Compose"
description: "Wendy supports running multi-service applications defined in a standard docker-compose.yml (or compose.yml) file. This is the recommended approach when your app needs more than one"
---
# Multi-Service Apps with Docker Compose
Wendy supports running multi-service applications defined in a standard `docker-compose.yml` (or `compose.yml`) file. This is the recommended approach when your app needs more than one container, for example an API service alongside a database, or a perception pipeline with several processing stages.
## How it works
When you run `wendy run` in a directory that contains a compose file, Wendy automatically detects it as a compose project. Each service is built, pushed to the device's embedded container registry, and started in dependency order. No `wendy.json` is required; each service carries its own configuration, and an optional companion `wendy.json` in the same directory is merged in when present (see [Readiness probes and postStart hooks](#readiness-probes-and-poststart-hooks)).
Supported compose file names (checked in order, first found wins):
- `docker-compose.yml`
- `docker-compose.yaml`
- `compose.yml`
- `compose.yaml`
A compose file takes precedence over a `Dockerfile` in the same directory. To force the single-container Docker path instead, pass `--build-type docker` or `--dockerfile `.
## Quickstart
```
my-project/
docker-compose.yml
api/
Dockerfile
main.py
worker/
Dockerfile
worker.py
```
```yaml
# docker-compose.yml
services:
api:
build: ./api
network_mode: host
worker:
build: ./worker
network_mode: host
depends_on:
- api
```
```sh
wendy run
```
Wendy selects a device, builds each service image, pushes them to the device, and starts them concurrently with interleaved log output. Each service's lines are prefixed with a color-coded, column-aligned service name. Log lines are never interleaved mid-line; each line is written atomically:
```
[api] Listening on :8080
[worker] Connected to api
[worker] Processing item 1
```
Service builds run in parallel, while the device-intensive image preparation
phase is limited to two services at a time so a high `--max-concurrency` value
does not overload device storage. A later run with unchanged source and runtime
configuration skips image building, transfer, and preparation when the service's
container and its content are still present on the device; missing or
unverifiable content fails closed and performs a normal preparation. Set
`WENDY_PUSH_SKIP=0` to disable this optimization while diagnosing builds.
Press **Ctrl-C** to stop all services. Services are stopped in reverse dependency order; the CLI prints `Stopping ...` for each service and then a final `Stopped N service(s).` summary.
## Detached mode hint
When all services start in detached mode (`--detach`), the CLI prints a hint showing how to stream logs:
```
Run 'wendy device logs' to stream logs.
```
## Service fields
Wendy honours the following compose fields. Fields not listed here are ignored.
| Field | Description |
|-------|-------------|
| `build` | Build context: a path string or a `{ context, dockerfile, args }` mapping. `dockerfile` may name a `Dockerfile`, `Containerfile`, a dot/hyphen variant of either, or a Stagefile (`prod.stagefile.yaml`), and must resolve inside the build context. See [Build files](#build-files) for how the default is chosen when `dockerfile` is omitted. Services without `build` use a pre-built image via `image`. |
| `image` | Pre-built image to pull and run on the device (e.g. `redis:7-alpine`). Public image names are normalised to their fully-qualified form automatically. |
| `command` | Override the container's default command. Accepts a string (shell-split) or a YAML sequence. |
| `environment` | Environment variables to inject. Parsed from key-value maps or `KEY=VALUE` lists. Applied in order: image env → compose env → Wendy system vars → framework vars (e.g., ROS2) → OTEL vars. OCI last-wins semantics apply. |
| `ports` | Records `host:container` mappings on a `network` entitlement. This does not implement bridge DNAT or publish an isolated container to the host browser. |
| `network_mode: host` | Adds a `host` network entitlement. |
| `volumes` | Named volumes are created as `persist` entitlements. Host bind mounts (paths starting with `.` or `/`) are silently skipped. |
| `depends_on` | Dependency order: list or condition-map form. Services are created in dependency order; detached starts follow the same order, but the condition-map's own health-check conditions (e.g. `condition: service_healthy`) are not evaluated; ordering is purely topological. To gate a service's own postStart hook on its readiness instead, see [Readiness probes and postStart hooks](#readiness-probes-and-poststart-hooks). |
| `restart` | Restart policy: `no`, `on-failure`, `always`, `unless-stopped`. Overridden by CLI flags if specified. |
| `x-wendy` | Wendy-specific per-service extensions: `readiness` (a readiness probe) and `hooks` (postStart hooks). Same camelCase schema as `wendy.json`'s top-level `readiness`/`hooks` fields. See [Readiness probes and postStart hooks](#readiness-probes-and-poststart-hooks) below. |
The following fields are recognized but intentionally ignored, each with a warning at deploy time: `devices`, `privileged`, `cap_add`, `security_opt`, `ipc`, `pid`, `shm_size`, `healthcheck`, `profiles`, `secrets`, and `extra_hosts`. Hardware access goes through Wendy entitlements in a companion `wendy.json` instead. Anything else (such as `networks`, `deploy`, `entrypoint`, `env_file`, or long-form volume syntax) is ignored silently.
## Build files
A service's `dockerfile` may name a Stagefile as well as a Dockerfile. Stagefiles are compiled to a Dockerfile before the image build, so nothing else about the service changes:
```yaml
services:
api:
build:
context: ./api
dockerfile: prod.stagefile.yaml
```
When `dockerfile` is omitted, Wendy resolves the default from the build context, in order:
1. `build.stagefile.yaml` — the canonical Stagefile.
2. Otherwise the first `.stagefile.yaml` in the context, alphabetically. A context that holds only variants has no conventional default, and falling through to `Dockerfile` would silently ignore the Stagefiles plainly sitting there. Name one explicitly with `dockerfile:` to pick a different variant.
3. Otherwise `Dockerfile`.
Two services may share one build context and point at different build files. Each Stagefile compiles to its own artifact (`Dockerfile.generated` for the canonical one, `Dockerfile.generated.` otherwise), so concurrent service builds in a shared context never overwrite each other's compiled Dockerfile. See [Stagefile naming](../clients/wendy-cli/commands/build.md#stagefile-naming) for the filename rules.
`docker compose build` itself knows nothing about Stagefiles, so `wendy build` compiles them first and passes a generated override file pointing the affected services at their compiled Dockerfile. Running plain `docker compose build` on the same project — without Wendy — will fail on a `dockerfile:` that names a Stagefile.
## Networking
Set `network_mode: host` for services that must share host ports, use multicast, pass CLI readiness probes, or be reached from the developer's browser.
Omitting `network_mode` while declaring `ports` currently creates a legacy mode-less network entitlement, which maps to host networking with a deprecation warning; the mappings do not create an isolated bridge or DNAT rules. To request isolated outbound networking, declare `{ "type": "network", "mode": "bridge" }` in a companion `wendy.json`, without relying on Compose `ports:` for inbound access. Bridge mode supplies a private namespace, DNS, and outbound NAT only. `network.ports` mappings on mesh entitlements are for mesh-peer traffic, not host/LAN browser ingress.
## Volumes
Named volumes declared in `volumes:` become persistent storage on the device and survive container restarts and re-deployments. Two services sharing a volume name share the same storage.
```yaml
services:
producer:
build: ./producer
volumes:
- shared-data:/data/out
consumer:
build: ./consumer
volumes:
- shared-data:/data/in
# Declare named volumes at the top level so the file stays valid for
# plain `docker compose` too. Wendy itself only reads the `services:`
# block; the service-level references above are what create the storage.
volumes:
shared-data:
```
> Host bind mounts (e.g. `./local-path:/container/path`) are not supported on device; they are skipped.
## Readiness probes and postStart hooks
A compose service can declare its own readiness probe and postStart hooks under `x-wendy`, using the same camelCase schema as `wendy.json`'s `readiness`/`hooks` fields:
```yaml
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
x-wendy:
readiness:
tcpSocket:
port: 3000
timeoutSeconds: 30
hooks:
postStart:
openURL: "http://${WENDY_HOSTNAME}:3000"
```
`readiness` here gates only `frontend`'s own `postStart` hook; it never delays any other service's startup (`depends_on` ordering is unaffected). `hooks.postStart.agent` runs directly on the device host and is triggered only by the declaring service's own container start.
A companion `wendy.json` in the same directory can also declare `services..readiness` / `services..hooks` for a service. When both are present, the companion wins wholesale per field: its `readiness` and `hooks` each replace the `x-wendy` value entirely rather than merging with it.
A companion `wendy.json`'s *top-level* `readiness`/`hooks` or `http` entitlement act instead as an app-level fallback: they fire once after every service in the project has started, rather than gating any single service. Inherited top-level HTTP remains in every service's container create configuration, but it is not executed once per service. A companion `services..entitlements` HTTP declaration is service-scoped; if both scopes declare HTTP, both actions run at their respective scopes. `Examples/WendyMC` in the repository uses a top-level `{ "type": "http", "port": 8080 }` entitlement with `readiness.timeoutSeconds: 180`, so the CLI preserves that timeout and opens the UI once after both services start. A top-level `hooks.postStart.agent` is the one exception: a compose app has no single app-level container start to trigger it, so it is ignored (with a warning); declare an agent-side hook under a service's `x-wendy.hooks` or the companion's `services..hooks` instead.
In attached mode, each service's readiness→postStart sequence runs asynchronously right after that service's start is acknowledged, so a slow or failing probe never delays starting the next service; Ctrl-C cancels any in-flight readiness wait and kills `cli` hook child processes. In detached mode none of this runs: no readiness wait, no `App reachable` line, and no host-side `postStart` action — only the agent-side `postStart.agent` hooks run, on the device. With `wendy run --watch`, each container's `openURL` and `cli` actions run once per session after its first successful readiness check; later saves do not repeat them, while a failed or canceled attempt may retry after a later deploy. `--watch --detach` skips these actions. A non-cancellation timeout in attached mode warns without failing the command: explicitly configured multi-service hooks still run, while `App reachable` and any synthesized HTTP browser open are suppressed. Cancellation suppresses all three. An explicit readiness TCP port remains the probe target; presentation prefers a hostname-templated `openURL`, then the HTTP entitlement port, then the readiness port. See [Watch mode](../clients/wendy-cli/commands/run.md#watch-mode) for details.
Hook commands may reference `${WENDY_HOSTNAME}` (the device host), `${WENDY_APP_ID}`, and `${WENDY_SERVICE_NAME}` (the declaring service's name; empty for the app-level fallback). Windows-style `%VAR%` forms are accepted too.
For a compose file with more than one service, `WENDY_APP_ID` is the project directory name and `WENDY_SERVICE_NAME` is the service name. A **single-service** compose project without a companion `wendy.json` uses `-` as `WENDY_APP_ID` (for example, `myproj-web`) and leaves `WENDY_SERVICE_NAME` empty. When a companion file is present, its `appId` is used and grouped naming applies even when there is only one service.
> Readiness probes and browser URLs dial the device host, not the container directly, so browser-reachable HTTP currently requires `network_mode: host`. Bridge mode provides outbound NAT only, and mesh port mappings serve mesh peers rather than the developer's browser.
## Flags
All `wendy run` flags work with compose projects:
| Flag | Description |
|------|-------------|
| `--deploy` | Build and create all containers but do not start them. |
| `--detach` | Start all containers but do not stream logs. See [Readiness probes and postStart hooks](#readiness-probes-and-poststart-hooks). |
| `--restart-unless-stopped` | Set restart policy to `unless-stopped` for all services (overrides per-service setting). |
| `--restart-on-failure` | Set restart policy to `on-failure` for all services. |
| `--no-restart` | Disable restart for all services. |
| `--debug` | Enable debug logging during build and run. |
| `--yes` / `-y` | Accept all device-selection prompts automatically. |
| `--watch` | Redeploy on changes and stream app logs between deploys. Requires a Wendy agent target when attached; use `--watch --detach` for provider targets. |
## Limitations
- Headless Mac is not supported. `wendy run` rejects compose projects before any registry or Docker setup when the selected target is Headless Mac. Target a Linux/WendyOS device to use compose.
- Wendy-specific hardware access entitlements such as `gpu`, `display`, `camera`, `audio`, `bluetooth`, `usb`, `i2c`, `gpio`, `spi`, `input`, and `serial` are not inferred from compose fields.
- Host networking does not imply shared IPC or shared `/dev/shm`; ROS 2 shared-memory transport requires an app shape that can explicitly share namespaces.
- Linux containers on macOS require a target WendyOS device; local Docker Desktop compose is used as a fallback when no device is targeted.
- Compose `extends`, `profiles`, and `secrets` are not supported.
- `wendy fleet run` does not support compose projects yet; deploy to one device at a time.
- `wendy run --service` applies only to `wendy.json` `services` maps; the compose path always deploys every service.
- `wendy build` for a compose project shells out to `docker compose build`. The Apple Container builder is not supported for compose; use `--builder docker`.
---
# Swift
Source: https://docs.wendy.dev/latest/advanced/apps/macos/swift/
---
title: "Swift"
description: "WendyOS developer documentation."
---
---
# OCI
Source: https://docs.wendy.dev/latest/advanced/apps/OCI/
---
title: "OCI"
description: "WendyOS developer documentation."
---
---
# template.schema.json
Source: https://docs.wendy.dev/latest/advanced/apps/template-schema/
---
title: "template.schema.json"
description: "template.schema.json is an optional file inside a Wendy project template that defines multi-phase, conditional configuration questions. Answers are collected interactively during w"
---
# template.schema.json
`template.schema.json` is an optional file inside a Wendy project template that defines multi-phase, conditional configuration questions. Answers are collected interactively during `wendy init` and become template variables available for rendering any file in the template (Dockerfile, compose.yml, source files, etc.).
It lives alongside `template.json` at the root of a template directory.
## Example
```json
{
"phases": [
{
"id": "project-type",
"title": "Project Type",
"questions": [
{
"id": "INFERENCE_MODE",
"label": "Local inference or cloud?",
"type": "radio",
"required": true,
"options": [
{ "value": "local", "label": "Local (on-device)" },
{ "value": "cloud", "label": "Cloud (API)" }
]
}
]
},
{
"id": "local-config",
"title": "Local Model",
"when": { "questionId": "INFERENCE_MODE", "equals": "local" },
"questions": [
{
"id": "LOCAL_MODEL",
"label": "Which local model?",
"type": "radio",
"required": true,
"options": [
{ "value": "whisper_base", "label": "Whisper Base" },
{ "value": "vosk", "label": "Vosk" }
]
}
]
},
{
"id": "cloud-config",
"title": "Cloud Provider",
"when": { "questionId": "INFERENCE_MODE", "equals": "cloud" },
"questions": [
{
"id": "CLOUD_PROVIDER",
"label": "Model provider?",
"type": "radio",
"required": true,
"options": [
{ "value": "openai", "label": "OpenAI" },
{ "value": "deepgram", "label": "Deepgram" }
]
},
{
"id": "API_TOKEN",
"label": "API token",
"type": "input",
"required": true,
"secret": true
}
]
}
]
}
```
## How answers become template variables
Every question `id` becomes a variable accessible in Go template syntax (`{{.VARIABLE_NAME}}`) across all files the template engine renders. This works identically to variables declared in `template.json`.
For **radio** and **input** questions the variable holds the selected string value:
```dockerfile
{{if eq .INFERENCE_MODE "local"}}
RUN apt-get install -y ffmpeg
{{end}}
```
For **checkbox** questions two forms are available:
| Variable | Type | Example |
|----------|------|---------|
| `{{.FEATURES}}` | Comma-separated string | `"gps,camera"` |
| `{{.FEATURES_gps}}` | Boolean | `true` |
```dockerfile
{{if .FEATURES_gps}}
RUN pip install gpsd-py3
{{end}}
```
## Fields
### `phases` *(required)*
Array of phase objects. Phases are evaluated in order; a phase is skipped entirely when its `when` condition is not met.
---
### Phase object
#### `id` *(required)*
Unique identifier for the phase. Used internally; not shown to the user.
#### `title`
Optional heading printed before the phase's questions are shown.
#### `questions` *(required)*
Array of question objects (see below).
#### `when`
Condition that must be true for this phase to be shown. When absent the phase is always shown. See [Conditions](#conditions).
---
### Question object
#### `id` *(required)*
Variable name. Must be unique across all questions in the schema. The answer is stored in `vals[id]` and available as `{{.ID}}` in template files.
Convention: use `UPPER_SNAKE_CASE` to distinguish schema variables from variables declared in `template.json`.
#### `label` *(required)*
Human-readable question text shown in the interactive prompt.
#### `type` *(required)*
| Value | UI | Answer stored as |
|-------|----|-----------------|
| `radio` | Single-select list | Selected option's `value` string |
| `checkbox` | Multi-select checklist | Comma-separated string + per-option booleans |
| `input` | Text prompt | Trimmed string |
#### `options`
Array of option objects. Required for `radio` and `checkbox`. Ignored for `input`.
| Field | Description |
|-------|-------------|
| `value` | The value stored when selected |
| `label` | Human-readable display text |
| `description` | Optional secondary description (shown in picker) |
| `size` | Optional metadata column (e.g., model size) |
| `parameters` | Optional metadata column (e.g., parameter count) |
| `comments` | Optional metadata column (e.g., additional notes) |
When `size`, `parameters`, or `comments` are present, the picker displays a multi-column table view instead of a simple list.
#### `required`
When `true` and `type` is `input`, the prompt rejects empty input.
#### `default`
Default value pre-filled in the prompt. Only used for `input` questions.
#### `secret`
When `true`, hints that the value is sensitive (e.g. an API token). Currently rendered as a standard text prompt; masking is planned.
#### `when`
Condition that must be true for this question to be shown. When absent the question is always shown within its phase. See [Conditions](#conditions).
---
## Conditions
A condition controls whether a phase or question is presented, based on a previously collected answer. The condition references the `id` of an earlier question.
```json
{ "questionId": "INFERENCE_MODE", "equals": "local" }
```
### Condition operators
Exactly one of the following must be set:
#### `equals`
Show when the referenced answer equals the given string exactly.
```json
{ "questionId": "INFERENCE_MODE", "equals": "local" }
```
#### `in`
Show when the referenced answer equals any value in the array.
```json
{ "questionId": "INFERENCE_MODE", "in": ["local", "hybrid"] }
```
#### `contains`
Show when the referenced answer (a comma-separated checkbox result) includes the given value.
```json
{ "questionId": "FEATURES", "contains": "gps" }
```
---
## Evaluation order
Phases are evaluated top-to-bottom. Within a phase, questions are evaluated top-to-bottom. A `when` condition on a question is tested against **all answers collected so far**, including answers from the current phase that appeared before it.
This means you can gate a question on the answer to a question in the same phase, as long as the gating question appears first.
---
## Using variables in template files
The template engine is Go's `text/template`. All standard actions are available: `{{if}}`, `{{range}}`, `{{with}}`, etc.
**Common patterns:**
```dockerfile
# Conditional block
{{if eq .INFERENCE_MODE "local"}}
RUN apt-get install -y libsndfile1
{{end}}
# Nested conditions
{{if eq .INFERENCE_MODE "cloud"}}
{{if eq .CLOUD_PROVIDER "openai"}}
ENV OPENAI_KEY={{.API_TOKEN}}
{{end}}
{{end}}
```
```yaml
# compose.yml topology
services:
app:
build: .
{{if eq .INFERENCE_MODE "local"}}
model-server:
image: ollama/ollama:latest
{{end}}
```
```toml
# pyproject.toml dependencies
dependencies = [
{{if eq .INFERENCE_MODE "local"}}
"openai-whisper>=20240930",
{{end}}
{{if eq .INFERENCE_MODE "cloud"}}
"openai>=1.0",
{{end}}
]
```
---
## Relationship to template.json
`template.json` and `template.schema.json` serve complementary roles:
| File | Purpose | Question types |
|------|---------|----------------|
| `template.json` | Simple scalar variables (port numbers, version strings, app ID) | Free text / integer / boolean |
| `template.schema.json` | Structured, conditional decisions that reshape the generated project | Radio, checkbox, input with phases |
Variables from both files are merged into a single map before rendering. If the same `id` appears in both, the `template.json` variable wins (it is collected first).
---
## JSON Schema
A formal JSON Schema for this file format is not currently published alongside these docs.
Add it to your `template.schema.json` to get editor validation:
```json
{
"$schema": "https://wendy.dev/schemas/template.schema.json",
"phases": [ ... ]
}
```
---
# Multi-Service Apps with wendy.json
Source: https://docs.wendy.dev/latest/advanced/apps/wendy-services/
---
title: "Multi-Service Apps with wendy.json"
description: "When your project needs more than one container managed through a wendy.json file (rather than a docker-compose.yml), declare a services map in wendy.json. wendy run detects the ma"
---
# Multi-Service Apps with `wendy.json`
When your project needs more than one container managed through a `wendy.json` file (rather than a `docker-compose.yml`), declare a `services` map in `wendy.json`. `wendy run` detects the map and automatically orchestrates a parallel multi-service build and deployment. `wendy build` detects the same map and builds the images locally without deploying — see [Building without deploying](#building-without-deploying).
## `wendy.json` structure
```json
{
"appId": "com.example.myapp",
"platform": "linux",
"services": {
"db": {
"context": "db"
},
"api": {
"context": "api",
"dependsOn": ["db"],
"entitlements": [
{ "type": "network", "mode": "host" }
]
},
"frontend": {
"context": "frontend",
"dependsOn": ["api"]
}
}
}
```
### `services` map
Each key is a service name. Each value is a `ServiceConfig` object:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `context` | string | **yes** | Build context directory, relative to `wendy.json`. Must be a relative path and must not contain `..` components. |
| `entitlements` | array | no | [Entitlements](../device/entitlements.md) to apply to this service's container. Same schema as the top-level `entitlements` field. |
| `dependsOn` | array of strings | no | Names of other services in this `services` map that must be created before this one. All referenced names must exist in the same map. |
| `env` | object | no | Environment variables for this service's container. Overrides the top-level [`env`](./wendy.json.md#env) per key, so a service can change one variable without dropping the rest. |
| `readiness` | object | no | Readiness probe for this service. Same schema as the top-level `readiness` field; see [Readiness and lifecycle hooks](#readiness-and-lifecycle-hooks). |
| `hooks` | object | no | Lifecycle hooks for this service. Same schema as the top-level `hooks` field. |
| `resources` | object | no | CPU/memory/PID ceilings for this service. Overrides the top-level [`resources`](./wendy.json.md#resources) per field. |
| `frameworks` | object | no | Framework configuration (e.g. ROS 2) for this service, replacing the top-level `frameworks`. |
| `simulation` | object | no | Explicit environment and entitlement overrides for a matching managed robot VM. See [Simulation backends](#simulation-backends). |
### Validation rules
`wendy.json` validation rejects the following:
- A service with an empty or missing `context`.
- A `context` that is an absolute path.
- A `context` that contains `..` path components.
- A `dependsOn` entry that references a service name not present in the `services` map.
- A service `entitlements` entry with an unknown or missing type.
- A service `persist` entitlement missing `name` or `path`, or with a non-absolute or `..`-containing `path`.
- A service `network` entitlement with a `mode` other than `"host"`, `"host-admin"`, `"none"`, `"bridge"`, or `"mesh"` (the same set the top-level entitlement accepts).
- A service `i2c` entitlement with a device not in `i2c-N` format.
- A service `serial` entitlement with a device not matching the USB-only `ttyACM0` / `ttyUSB0` (`tty*N`) pattern.
- A service `mcp` entitlement with a port outside the range 1–65535.
- More than one `mcp` entitlement within a single service's `entitlements` array.
`ValidateJSON` additionally warns on deprecated entitlement types and unknown entitlement keys within service-level `entitlements` arrays, using the same rules applied to the top-level `entitlements` field.
## Simulation backends
A service can use hardware drivers on a physical device and an app-authored
backend on a managed robot VM. Declare the simulator grants explicitly:
```json
{
"appId": "com.example.robot",
"services": {
"camera": {
"context": "camera",
"entitlements": [{ "type": "usb" }],
"simulation": {
"profile": "rosmaster-r2",
"entitlements": [{ "type": "network", "mode": "host" }],
"env": { "R2_SIMULATOR_URL": "http://127.0.0.1:8890" }
}
}
}
}
```
The camera image in this example must implement both its USB driver and the
HTTP simulator backend selected by `R2_SIMULATOR_URL`. Wendy does not translate
hardware SDK calls or emulate USB/serial firmware.
`profile` must be `rosmaster-r2`, `go2`, `g1` or `so101`. `entitlements` is required and
must contain at least one grant. On the matching managed VM, it replaces the
service's normal entitlements; `env` overlays the service environment per key.
CLI `--env` values retain precedence. A different managed robot profile is an
error. Physical devices use the normal configuration. Services without an
override keep their existing configuration.
Selection happens after resolving the managed VM and before building and
deploying. It changes the in-memory configuration only. The manifest, build
context and Dockerfile stay the same. See the
[R2 exploration tutorial](../../../guides/tutorials/python/r2-explorer/) for a standalone
app that uses the simulator's lidar, camera and control APIs.
## Readiness and lifecycle hooks
Any service in the `services` map may declare its own `readiness` probe and `hooks.postStart`, using the same schema as the top-level `readiness`/`hooks` fields:
```json
{
"appId": "com.example.stack",
"services": {
"db": { "context": "db" },
"cache": { "context": "cache" },
"api": { "context": "api", "dependsOn": ["db", "cache"] },
"frontend": {
"context": "frontend",
"dependsOn": ["api"],
"readiness": {
"tcpSocket": { "port": 3000 },
"timeoutSeconds": 30
},
"hooks": {
"postStart": {
"openURL": "http://${WENDY_HOSTNAME}:3000"
}
}
}
}
}
```
Only a service that declares `readiness`, `hooks`, or a service-level `http` entitlement runs the readiness→postStart sequence; `db`, `cache`, and `api` above are unaffected.
### Scoping
- `readiness` gates only the declaring service's own `postStart` hook — it never delays other services' startup order (`dependsOn` ordering is a separate mechanism).
- A service-level `http` entitlement waits, announces, and opens only for that service. A top-level `http` entitlement remains in every container's create configuration but runs as an app-level action once, after every service starts. If both scopes declare HTTP, both actions run at their respective scopes.
- An explicit `readiness.tcpSocket.port` remains the probe target even when the same scope declares HTTP. The displayed/opened URL prefers an explicit hostname-templated `openURL`, then the HTTP port, then the readiness port; probing `9000` while opening `8080` is supported.
- `hooks.postStart.agent` runs directly on the device host and is delivered only with the declaring service's own container start call; starting another service does not trigger it.
- Hook commands may reference `${WENDY_HOSTNAME}` (the device host), `${WENDY_APP_ID}`, and `${WENDY_SERVICE_NAME}` — the declaring service's name, empty for single-container apps and for the app-level fallback below. Windows-style `%VAR%` forms are accepted too.
### App-level fallback
A top-level `readiness`/`hooks` or `http` entitlement in `wendy.json` acts as an app-level fallback: it fires once after every service has started, rather than gating any single service. Both the fallback and a service's own lifecycle configuration fire if both are declared. Two exceptions:
- A top-level `hooks.postStart.agent` is ignored for multi-service apps — there is no single app-level container start to trigger it. `wendy run` warns about this when it loads `wendy.json`; declare it under `services..hooks` instead.
- The fallback is skipped when `wendy run --service` selects a subset of services, since "every service has started" can't be guaranteed on a partial run.
### Attached vs. detached
In attached mode, each service's readiness→postStart sequence fires asynchronously right after that service's start is acknowledged, so a slow or failing probe never delays starting the next service. Ctrl-C cancels any in-flight readiness wait and kills `cli` hook child processes. If the run ends on its own — every service's log stream closes — while a hook (per-service or the app-level fallback) is still waiting on readiness, that hook is suppressed rather than fired, so `wendy run` never opens a browser onto a stack that has already exited. In detached mode none of this runs: no readiness wait, no `App reachable` line, and no host-side `postStart` action. Only the agent-side `postStart.agent` hooks, carried on each start RPC, still run on the device. With `wendy run --watch`, each service's `openURL` and `cli` actions run once per session after its first successful readiness check; later saves do not repeat them, while a failed or canceled check may retry after a later deploy. `--watch --detach` skips these actions. A non-cancellation readiness timeout in attached mode warns but does not fail the command: explicitly configured multi-service `postStart` hooks still run, while `App reachable` and any HTTP-entitlement-synthesized browser open are suppressed. Cancellation suppresses the warning, announcement, and hook.
An ordinary detached agent deployment also reports configured app and service
HTTP endpoints. With `--json`, it emits one group result with
`readiness: "not_checked"`; services may have zero or several endpoint entries.
A partial `--keep-going` deployment returns non-zero without a whole-group
success result. See [`wendy run` — Detached output](../clients/wendy-cli/commands/run.md#detached-output).
## How `wendy run` handles multi-service projects
When `wendy.json` defines a `services` map, `wendy run` routes to the multi-service pipeline:
1. **Parallel build** — all service images are built and pushed concurrently, with up to 4 simultaneous builds by default. Override with `--max-concurrency`. In interactive terminals a per-service spinner displays each service's status (`waiting` → `building…` → `built (Xs)` / `failed`). While a service builds, its row names the Dockerfile step currently running and, where that step's output exposes it, appends live progress after a `·` separator:
⠹ api [4/9] RUN pip install -r requirements.txt · 61% 128.0MB/797.3MB 95.2MB/s
In non-interactive terminals plain log lines are printed instead.
2. **Ordered container creation** — containers are created one at a time in topological dependency order. A service listed in another service's `dependsOn` is created first.
3. **Start and stream** — all containers are started and their combined stdout/stderr is multiplexed to the terminal. Each line is prefixed with `[serviceName]`.
Press **Ctrl-C** to stop all services. The CLI cancels all streams and stops each service concurrently before exiting.
### Container naming
Each service container ID follows the `{appId}_{serviceName}` convention (`_`
is the separator because `/` is not permitted in containerd container IDs). For
example, with `appId: "com.example.myapp"` and service `"api"`, the containerd
container ID is `com.example.myapp_api`.
> **Note:** Single-container apps (no `serviceName` in the top-level
> `wendy.json`) are unaffected — their container ID remains the bare `appId`.
## Building without deploying
`wendy build` detects the same `services` map and builds every service (or a `--service` closure) into the local image store, tagged `-:latest`, without deploying: nothing is pushed to a registry and nothing is created or started on a device. Useful for CI and pre-flight checks — confirm every service builds before running `wendy run`:
```sh
wendy build
wendy build --service api
```
See [`wendy build`](../clients/wendy-cli/commands/build.md#multi-service-manifests) for the full flag reference (`--service`, `--max-concurrency`, `--builder`, `--gpu-arch`, `--debug`).
## Filtering with `--service`
To build and run only a specific service (and its transitive dependencies):
```sh
wendy run --service api
```
This resolves `api` and all services reachable through its `dependsOn` graph. Services outside this subset are not built or started. Passing an unknown service name returns an error immediately.
## Flags
All standard `wendy run` flags apply. The following are particularly relevant for multi-service projects:
| Flag | Description |
|------|-------------|
| `--service ` | Build and run only the named service and its transitive `dependsOn` dependencies. |
| `--build-host ` | Build the selected services on another device, which delivers each image to the target. See [Remote build host](#remote-build-host). |
| `--deploy` | Build and create all containers but do not start them. |
| `--detach` | Start all containers but do not stream logs. See [Attached vs. detached](#attached-vs-detached). |
| `--keep-going` | Deploy services that build successfully instead of aborting the whole group on the first build/push failure. |
| `--max-concurrency ` | Max service images to build+push at once. 0 = default limit of 4. |
| `--watch` | Redeploy changed services while continuing to show logs from the whole app. Use `--watch --detach` to skip logs and `openURL`/`cli` actions. |
## Example layout
```
my-project/
wendy.json
db/
Dockerfile
api/
Dockerfile
frontend/
Dockerfile
```
```sh
wendy run # builds and starts all three services
wendy run --service api # builds db and api only (frontend excluded)
```
## Crash-looping services
When a service within a group crashes and the agent's restart policy is
automatically restarting it, that service's individual entry in
`wendy device apps list` shows a red `↻` **crash-looping** state (nested under
the group header). The top-level app entry stays `Running` as long as at least
one service is up; it flips to `Crash-looping` only when every service is down
and the restart policy is still restarting at least one of them.
`wendy device logs --app ` surfaces crash output from all service
members of the group, so a crash-looping service's logs are reachable without
naming the individual service.
## Remote build host
`wendy run --build-host ` builds the selected services on another device instead of on your machine. Each service goes to the build host as its own build context, and the host delivers each image straight to the target as `-:latest`, the same image a local build produces. `--service`, `--max-concurrency`, and `--keep-going` behave as they do for a local build. See [Remote build host](../clients/wendy-cli/commands/run.md#remote-build-host) for setup and requirements.
Two things differ from a local build:
- **One target per run.** `--device a,b,c` is refused for a multi-service project, because its services are created and started as a group against one device. Run `wendy run` once per device instead.
- **No push-skip.** A local build skips a service whose inputs are unchanged and whose layers the device already holds. A remote build cannot prove that, so every selected service is sent to the build host on each run; the host's BuildKit cache and chunked delivery keep an unchanged service fast. Under `--watch`, a service already confirmed unchanged and running in the session is still left alone.
## Limitations
- Log output is multiplexed with a `[serviceName]` prefix on each line. Per-service log stream routing is not yet available.
- Containers are created one at a time in dependency order, not atomically as a group. Grouped atomic creation is planned as a follow-up.
- Headless Mac is not supported. `wendy run` rejects multi-service `wendy.json` projects when the selected target is Headless Mac, before any build or registry operation. Target a Linux/WendyOS device for multi-service workloads.
---
# wendy.json
Source: https://docs.wendy.dev/latest/advanced/apps/wendy.json/
---
title: "wendy.json"
description: "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 beh"
---
# 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 ` 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..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`](../clients/wendy-cli/commands/init.md#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..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](./wendy-services.md#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..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](./wendy-services.md#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 |
### `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..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..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](../clients/wendy-cli/commands/project/index.md).
### `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 service address range. 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`.
#### 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////dsp//` | 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. |
### `spi`
SPI device access via `spidev` (`/dev/spidev.`). Takes no options — grants the SPI subsystem as a whole. See the [SPI guide](../device/entitlements.md#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:`. 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](../clients/wendy-cli/commands/run.md#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
`/.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](./compose.md) for details.
---
> **Deprecated:** `{ "type": "video" }` — use `camera` instead.
---
# Architecture
Source: https://docs.wendy.dev/latest/advanced/architecture/
---
title: "Architecture"
description: "Wendy Labs is the infrastructure for Physical AI, targeting Edge (AI) devices such as robotics, IoT and satellites."
---
Wendy Labs is the infrastructure for Physical AI, targeting Edge (AI) devices such as robotics, IoT and satellites.
### Problem
Before Wendy, it's very hard to install, develop and deploy these devices.
- Setting up the OS, drivers and tooling is manual, and requires peripherals such as a keyboard, mouse and monitor
- Running on a device is a custom effort, whether through rsync, zip files, or containers
- Scaling your solution to many devices is not standardised either
In cloud services, all of these are solved through pre-baked images and container infrastructure. Wendy brings that to the edge.
### Solution
Wendy Labs has three main components to achieve this mission:
- WendyOS
- Wendy-Agent
- Cloud
[WendyOS](../../#2-set-up-a-device) is a pre-built OS for specific hardware (NVIDIA Jetson — Orin Nano, AGX Orin, AGX Thor —, Raspberry Pi, ...) that packages all the tools you need to build & run apps.
Wendy-Agent is a daemon, akin to Docker, that manages and runs your apps. It also exposes a way to remotely control the machine and upload apps.
[Wendy Cloud](https://cloud.wendy.dev/login) is a tool for managing your Wendy devices, viewing (crash) logs, provisioning updates and more.
### Agent & CLI
The Agent comes in two flavours, Linux and macOS, for running apps on these platforms respectively.
The [Wendy CLI](../../reference/cli/) enables your developers to manage these devices, locally through LAN, BLE or over USB.
The CLI also manages enrolled devices through Wendy Cloud, including deployment
and remote access.
## Wendy Net routing
Wendy Net can carry traffic through intermediate devices when the destination is
not a direct neighbor. Its routing engine selects paths independently of whether
the links use Ethernet, Wi-Fi, or Bluetooth.
For contributors, the engine is a separate Go library in `go/babel`. It uses the
Babel routing protocol; carrier integration and application services belong to
the agent. See the [library guide](https://github.com/wendylabsinc/WendyOS/blob/main/go/babel/README.md)
for its API and the [testing guide](../development/testing.md#routing-engine-tests)
for the separate module checks.
---
# Container lifecycle identity
Source: https://docs.wendy.dev/latest/advanced/architecture/container-lifecycle/
---
title: "Container lifecycle identity"
description: "A start request can name an app, while its task is stored under a service container ID such as demorelay. Resolve the request through ResolveAppContainerIDs before changing restart"
---
# Container lifecycle identity
A start request can name an app, while its task is stored under a service
container ID such as `demo_relay`. Resolve the request through
`ResolveAppContainerIDs` before changing restart bookkeeping. Do not derive
IDs by appending or stripping a suffix: legacy single-container apps keep
bare IDs, and an exact container name wins over an app alias.
After a successful start (streamed, attached, or grouped):
- Clear the persisted stopped-by-user label on each actual container ID.
- Clear its explicit-stop marker and register its persisted or requested
restart policy under that same ID.
- Retire the request-name monitor entry when it is only an alias.
- If resolution fails, log the failure and skip bookkeeping rather than
introducing an unresolvable monitor entry.
The stop path resolves the same IDs before marking explicit stop, and boot
reconciliation registers persisted container IDs. Keeping those identities
consistent prevents an old app alias from remaining eligible for restart
while the CLI reports its service stopped. The `NO` restart policy must
remove the resolved registration as well as any request alias. Already queued
restart work must also refuse to start after its registration is removed.
Regression tests in `container_service_test.go` exercise start/stop using
both name forms, attach, persisted stop labels, alias retirement, failed
resolution, and disabling restarts. Existing monitor tests cover the rule
that explicit stop wins over automatic restart scheduling.
---
# Enroll Wendy Lite in Wendy Cloud
Source: https://docs.wendy.dev/latest/advanced/cli/lite-enrollment/
---
title: "Enroll Wendy Lite in Wendy Cloud"
description: "Enroll your Wendy Lite board to manage it remotely with the Wendy CLI. You need a USB data cable, current Wendy Lite firmware, a Wi-Fi connection for the board, and a Wendy Cloud a"
---
# Enroll Wendy Lite in Wendy Cloud
Enroll your Wendy Lite board to manage it remotely with the Wendy CLI. You need
a USB data cable, current Wendy Lite firmware, a Wi-Fi connection for the board,
and a Wendy Cloud account with permission to enroll devices.
## 1. Connect and sign in
Connect the board to your computer over USB, then run:
```sh
wendy cloud login
wendy discover
```
Find the board's USB selector in discovery. On macOS it looks like
`wendy-lite:/dev/cu.usbmodemXXXX`; use the selector shown for your board.
Enrollment requires USB even if the board is already reachable over Wi-Fi or
Bluetooth. Configure its Wi-Fi before continuing:
```sh
wendy device wifi connect --device wendy-lite:/dev/cu.usbmodemXXXX
```
Choose a network and enter its password when prompted.
## 2. Enroll the board
```sh
wendy cloud enroll-device \
--device wendy-lite:/dev/cu.usbmodemXXXX \
--name lite-desk
```
Replace the selector and name. If you omit `--device`, choose the USB connection
in the device picker. Select your organization when prompted. The CLI checks the
firmware, sends the enrollment settings, reboots the board, and waits for it to
install its device certificate. Keep USB connected until the command finishes.
The board creates its private key itself.
For hosted Wendy Cloud, the CLI selects the connection and certificate settings
automatically. You do not need to build the CLI or supply certificate files.
If the CLI says the firmware does not support enrollment, update the board
using the [Wendy Lite installation guide](../../../installation/wendy-lite-esp32/).
## 3. Check that it is online
```sh
wendy cloud discover
```
Wait for your board to appear online, then select it for your next command.
Cloud presence can take a little longer than enrollment. The board needs Wi-Fi
and internet access to connect to Wendy Cloud.
Over LAN and Bluetooth, the CLI requires the board's certificate to name the
device UUID the board advertises. Firmware that does not advertise it over
Bluetooth is looked up in the selected Cloud organization by its hardware ID, so
sign in first. Over LAN, update firmware that advertises no device identity.
## If enrollment does not finish
- **The board is already enrolled or has a pending enrollment:** let the pending
operation finish. To start over, remove its enrollment over USB as shown below.
- **An error reports a reserved asset:** Cloud has created a device record, but
the board has not finished enrolling. Keep the reported asset ID for cleanup;
a Cloud record alone does not mean the board is ready.
- **The board is enrolled but offline:** check its power, Wi-Fi and internet
connection. Read its console for a more specific error:
```sh
wendy device logs --device wendy-lite:/dev/cu.usbmodemXXXX
```
Press Ctrl+C to stop reading. The Lite console does not support the WendyOS
app filters, `--tail`, or `--no-follow` options.
## Remove or change the enrollment
Sign in to the organization the board is currently enrolled in, connect it over
USB, and run:
```sh
wendy device unenroll --device wendy-lite:/dev/cu.usbmodemXXXX
```
Confirm the removal. The command removes Cloud access, clears the board's local
enrollment, and reboots it. Wi-Fi and app configuration are preserved. You can
then enroll it again, including in another organization.
If Cloud removal succeeds but the USB reset fails, reconnect the board and rerun
the same command. Firmware without reset support, or that does not report its
enrollment identity, must be updated first. If several Cloud records carry the
board's hardware ID, the command lists them and removes none; remove each with
`wendy cloud device unenroll --device `, then run it again.
Bluetooth and LAN connections cannot clear a Lite board's enrollment.
If the board is unavailable, remove its Cloud access without contacting it:
```sh
wendy cloud discover --all --json
wendy cloud device unenroll --device
```
This leaves the board's local enrollment in place. Before re-enrolling that
board, connect it over USB and run the normal `wendy device unenroll` command.
See [unenrollment options](../../../advanced/clients/wendy-cli/commands/device/unenroll/).
## Advanced: self-hosted deployments
Use settings supplied by your Wendy administrator. `--cloud-grpc` selects the
Cloud endpoint of a saved sign-in. Hosted Wendy Cloud needs no overrides.
| Option | When to use it |
| --- | --- |
| `--broker-host`, `--broker-port` | Your deployment uses a separate Lite relay. |
| `--csr-url` | Your administrator supplies a custom certificate enrollment URL. |
| `--device-roots`, `--https-roots` | Your administrator supplies custom CA certificate bundles. Provide both files together. |
| `--time-url`, `--tsa-roots` | Your administrator supplies a custom signed-time service and its trusted CA bundle. |
The default signed-time service is Sigstore. Keep firmware current so it can
verify that service. Certificate bundles are saved on the board for later boots.
To replace an enrolled board's trust settings, unenroll it over USB and enroll
again with the administrator's settings.
---
# Usage analytics
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/analytics/
---
title: "Usage analytics"
description: "Control whether Wendy sends anonymous usage analytics:"
---
# Usage analytics
Control whether Wendy sends anonymous usage analytics:
```sh
wendy analytics status
wendy analytics disable
```
Run `wendy analytics enable` to opt in again. `WENDY_ANALYTICS=false` and CI also
turn collection off; `status` shows the effective setting. Analytics failures do
not change a command's result. Diagnostic reports are not uploaded automatically.
## Event reference
The details below describe the event fields for administrators reviewing data
collection. Normal use does not require configuring these events.
## How it works
When analytics are enabled, each tracked event is serialised to JSON and sent via an HTTP POST request. Delivery is best-effort: a network failure, timeout, or non-2xx response never changes the command's result.
The CLI sends `cli_seen` before command execution or argument validation, including help, version, unknown commands, and invalid flags. Unknown commands use the last recognized command path; the unknown text is never sent. `command_executed` records the outcome before optional update or completion prompts. Help and version invocations do not count as successful product operations. Pending events are flushed before sudo process replacement or a Windows elevation launch.
MCP tool calls emit `command_executed` events as each call finishes, using a command name such as `wendy mcp device_info` and `command_root` of `mcp`. The duration and success flag describe the individual call, including tool results that report an error and panics recovered by the MCP server. Calls to app-provided tools use the fixed name `wendy mcp container_tool` so app and tool names stay private. Tool arguments and result content are never sent. The same analytics opt-out and CI settings apply to MCP calls.
`wendy doctor` and `wendy update --check` skip analytics entirely. These read-only
invocations also skip update and syntax-error cache writes and routine sudo
ownership cleanup.
Installation plans and privileged internal helpers also skip analytics.
`wendy analytics disable` does not send an event about disabling analytics.
An unreadable or malformed config prevents telemetry initialization, so a stored
opt-out cannot be accidentally ignored.
`wendy analytics status` reports the effective state, including CI detection,
the environment opt-out, and an analytics initialization failure. A saved enabled
preference does not override these conditions.
## Endpoint
Events are posted to:
```
https://wendy-cli-telemetry-114319063177.us-central1.run.app/v1/telemetry/events
```
## Event payload
Every event is an anonymous JSON object. The fields sent are:
| Field | Type | Description |
|-------|------|-------------|
| `schema_version` | integer | `2` for the lifecycle-aware event contract |
| `event_id` | string | Random event UUID, allowing the receiver to discard duplicate delivery |
| `occurred_at` | string | UTC event time recorded by the CLI, independent of delivery time |
| `attempt_id` | string | Random UUID shared by one invocation or tour and its child operations |
| `anonymous_id` | string | Stable random UUID stored in `~/.wendy/` — never tied to a real identity |
| `event` | string | Event name, e.g. `"command_executed"` |
| `command_name` | string | Canonical command path, e.g. `"wendy device wifi connect"` |
| `command_root` | string | Top-level command token |
| `duration_ms` | integer | Command duration in milliseconds |
| `success` | boolean | Whether the command completed without error |
| `error_class` | string | Bounded enum describing the error category (never free-form error text) |
| `cli_version` | string | CLI version string |
| `os` | string | Operating system (`GOOS`) |
| `arch` | string | CPU architecture (`GOARCH`) |
| `is_dev_build` | boolean | `true` when `cli_version` is `"dev"` or has a `-dev` suffix |
| `properties` | object | Event-specific string values, sent only by events that define them (see [Deploy events](#deploy-events)). At most 32 allowlisted entries of up to 64 bytes, or 128 bytes for canned remediation; invalid values are discarded |
> **Privacy note:** Flag values, positional arguments, file paths, hostnames, image and app names, and error message text are never included in telemetry payloads. Only the fields in this table are sent, and `properties` holds only the bounded values documented below.
## Activation events
The installer emits `install_started`, `install_progress`, and `install_finished`. The outcome is `completed`, `failed`, or `cancelled`; declining an erase confirmation counts as cancellation. `image_written` records the completed media write, before optional provisioning. `device_boot_verified` requires the first-boot verifier to contact the expected agent and pass its requested version, board, identity and enrollment checks. The verifier does not check application readiness.
Runs emit `run_started`, `run_progress`, and `run_finished`. `app_started` is emitted immediately when the agent acknowledges container start, before attached logs end. `app_ready` is emitted only after the attached run's configured readiness check succeeds. A failed check emits `app_readiness_failed` with a canned diagnostic even when the attached command continues streaming logs. Detached runs do not claim readiness. Multi-service, Compose, fleet and provider paths do not yet all report these application milestones; command success must not substitute for a missing acknowledgement.
Lifecycle properties include the current `stage` and `step`, `outcome`, `entrypoint` (`cli`, `mcp`, or `tour`), interactive-terminal status, duration and a subprocess exit code when available. Run diagnostics retain `unknown` for checks not actually observed, including authentication and build prerequisites. Recognized project and selected/reachable target checks become true only after their corresponding operations succeed. Tour events may include the selected route (`physical`, `simulator`, or `existing_device`). Failures include a stable `error_code` and an `error_detail` selected from fixed remediation sentences. Unknown failures use `step_failed` and a diagnostic-report instruction; they do not assert a root cause. No arbitrary error text is sent.
MCP tools use an attempt ID per call. The run subprocess inherits that ID. Durable installation jobs preserve their attempt ID across worker processes and first-boot verification. IDs do not identify people or join browser sessions to CLI installations. Installation method is currently `unknown`; acquisition source and OS version are unavailable and are not inferred from shell environment variables.
Delivery remains best-effort, with no persistent offline queue. A start without a terminal event has an unknown outcome; it is not proof of abandonment. Analytics remain disabled by the same opt-out and CI settings for every event.
Legacy `first_run` means the first completed CLI invocation, and `install_completed` describes the Homebrew postinstall hook. These are not device setup or first deployment events. Version 2 marks delivered milestones as successful. Historical events keep their original semantics.
`first_deploy_success` records the first acknowledged application start,
rather than merely a successful CLI exit. Managed-robot
provisioning does not emit it, and a successful command exit alone is insufficient.
## Deploy events
`wendy run` and `wendy watch` send one `deploy_completed` event per deploy, in addition to the command's `command_executed` event. It is sent once a deploy path has been chosen; earlier failures, such as an invalid `wendy.json` or an unreachable device, are covered by `command_executed` alone. Managed-robot provisioning builds are never reported.
`duration_ms` and `success` describe the deploy up to the agent's confirmation that the container started, not the log session that follows. Paths without that confirmation (compose, multi-service, fleet builds, and fast-path runs that reuse a running container) cover the whole command.
Its `properties` hold only numbers and fixed values. A phase is reported only once it happened.
| Property | Description |
|----------|-------------|
| `deploy_transport` | How the image reached the device: `chunk`, `registry`, `fastpath`, `buildhost`, `multiservice`, `native`, `xcode`, `swiftpm`, `swift`, or `compose` |
| `deploy_started` | `true` when the agent confirmed the container started |
| `deploy_fallback` | Why a registry push was used: `unimplemented`, `transport`, or `other` after a failed chunk upload; `darwin_agent`, `deploy_only`, `chunking_off`, or `not_attempted` when no chunk upload was tried |
| `deploy_target_platform` | Target platform, such as `linux/arm64` or `linux/arm/v7`; unrecognized values are sent as `other` |
| `deploy_device_type` | Board type reported by the device's OS, such as `jetson-orin-nano` |
| `deploy_build_ms`, `deploy_push_ms`, `deploy_upload_ms`, `deploy_device_prepare_ms`, `deploy_start_ms` | Chunk deploys: build, whole push, upload, device-side image preparation, and container start times in milliseconds |
| `deploy_bytes_sent`, `deploy_chunks_sent`, `deploy_chunks_total`, `deploy_layers_total`, `deploy_layers_reused` | Chunk deploys: bytes and chunks uploaded, chunks and layers in the image, and layers the device already had |
| `deploy_compression` | Chunk deploys: `gzip` or `none` |
| `deploy_build_push_ms` | Registry deploys: build and push time in milliseconds |
| `deploy_chunk_attempt_ms` | Time spent on a chunk upload that fell back to a registry push |
| `deploy_image_bytes` | Image size in bytes |
## Error categories
Failed commands report a fixed `error_class`. Shared operations use the same
categories across commands, including `run`, `watch`, `build`, and device commands.
| Category | Scenario |
|----------|----------|
| `build_failed` | An image, Swift, Xcode, or provider build failed. Fused build-and-push commands also use this category when the subprocess cannot distinguish the two stages. |
| `builder_unavailable` | Docker, Apple Container, a Swift toolchain, or builder setup is unavailable or failed to start. |
| `tool_not_found` | A required executable could not be found. |
| `config_invalid` | Invalid run options, missing build files, unsupported project detection, or failed loading/validation of `wendy.json`. |
| `cli_usage` | Flag parsing rejected an unknown flag, missing value, invalid value, or invalid syntax. |
| `project_target_mismatch` | The project cannot run on the selected target, architecture, or host platform. |
| `no_device` | No target was selected or no matching/enrolled device was found. |
| `device_ambiguous` | Multiple devices match and an explicit name or ID is required. |
| `device_offline` | The selected cloud device, or all enrolled cloud devices, are reported offline. |
| `device_unreachable` | No agent is listening, no authenticated endpoint answered, or a selected device has no reachable IP agent. |
| `simulator_unavailable` | The selected simulator could not be made available. |
| `device_identity_mismatch` | The selected device's identity does not match its saved pin. |
| `device_org_mismatch` | The device belongs to an organization for which the CLI has no credentials. |
| `device_tls_rejected` | The device TLS handshake rejected the CLI's credentials. |
| `device_auth_required` | A provisioned agent requires authentication that the CLI could not satisfy. |
| `auth_required`, `auth_session_ambiguous` | Cloud login is missing, or multiple sessions require an explicit choice. |
| `auth_certificate_failed` | Cloud certificate issuance/renewal returned a structured failure. |
| `registry_auth`, `registry_unavailable` | Required registry credentials/transport are unavailable, or the Mac agent has no reachable registry. |
| `transfer_failed` | Chunk upload, file sync, or a separate image push failed. |
| `container_start_failed` | The agent's create/start stream ended before confirming completion. |
| `readiness_timeout` | The application's TCP readiness probe did not succeed in time. |
| `user_cancelled`, `context_canceled`, `context_deadline` | A picker or operation was cancelled, or its context deadline expired. |
| `network_dns`, `network_timeout`, `network_refused`, `network_unreachable`, `connection_closed` | A typed network error identifies DNS, timeout, refused connection, unreachable network/host, or reset/broken pipe. |
| `permission_denied`, `file_not_found`, `disk_full` | An underlying OS error identifies a local resource failure. |
| `unexpected_eof`, `process_failed` | An unclassified operation ended with a truncated stream or a subprocess exit failure. |
| `other` | The error has no recognized category or typed cause. Its message is still never sent. |
gRPC errors use `grpc_unavailable`, `grpc_deadline`, `grpc_unimplemented`,
`grpc_invalid_argument`, `grpc_not_found`, `grpc_already_exists`,
`grpc_permission_denied`, `grpc_resource_exhausted`, `grpc_failed_precondition`,
`grpc_aborted`, `grpc_out_of_range`, `grpc_internal`, `grpc_data_loss`,
`grpc_unauthenticated`, or `grpc_unknown`. gRPC cancellation uses
`context_canceled`; unrecognized future status codes use `grpc_other`.
Cancellation and deadlines take precedence, followed by known command categories,
then gRPC and OS/network causes. Wrapped and joined errors retain their causes.
For a multi-service failure with several categories, the classifier reports the
first matching category in its fixed priority order, not a majority vote.
These categories apply to events sent by updated clients. Historical events that
only stored `other` or `grpc_other` cannot be reclassified without additional
failure data.
The CLI stores a private, timestamp-only `activation-state.json` beside `analytics_id` after a successful install operation. A later `run_started` clears the waiting window. On a subsequent CLI launch, it emits `next_launch_no_run_24h` and `next_launch_no_run_7d` when each threshold has elapsed. Each threshold is acknowledged only after successful HTTP delivery, so failed sends can retry on later launches. These are observations of returning installations; they do not classify absent users as abandoned. The check follows analytics opt-out and creates no state when disabled. Delivery remains best-effort, and concurrent CLI processes can duplicate a threshold observation.
## Opting out
Analytics can be disabled in several ways:
1. Set the environment variable to override the saved preference:
```sh
WENDY_ANALYTICS=false wendy
```
2. Save the disabled preference with:
```sh
wendy analytics disable
```
3. CI detection automatically disables analytics when a standard CI environment variable has a non-empty value. Even `CI=false` disables analytics. There is no opt-in escape hatch in CI.
---
# Disable
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/analytics/disable/
---
title: "Disable"
description: "WendyOS developer documentation."
---
---
# Enable
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/analytics/enable/
---
title: "Enable"
description: "Enables analytics, unless WENDYANALYTICS=false is set in the environment."
---
Enables analytics, unless `WENDY_ANALYTICS=false` is set in the environment.
---
# wendy auth default
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/auth/default/
---
title: "wendy auth default"
description: "Shows or clears the current auth context."
---
# `wendy auth default`
Shows or clears the current **auth context**.
## Usage
```sh
wendy auth default [--clear]
```
## Description
With no flags, prints the current context and the session it names
(` (org N — endpoint)`). If the current context points at a session
that no longer exists (stale), it is automatically cleared and a warning is
printed.
Pass `--clear` to unset the current context. With no current context set,
resolution falls back to an interactive picker (or errors in a non-interactive
environment).
## Flags
| Flag | Description |
|------|-------------|
| `--clear` | Unset the current context. |
## Examples
```sh
# Show the current context
wendy auth default
# Clear the current context
wendy auth default --clear
```
## See also
- [`wendy auth use`](./use.md) — switch the current context
- [`wendy auth rename`](./rename.md) — rename a context
---
# wendy auth login
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/auth/login/
---
title: "wendy auth login"
description: "Sign in to Wendy Cloud and save access to your organization. This command is equivalent to wendy cloud login, the recommended entry point."
---
# `wendy auth login`
Sign in to Wendy Cloud and save access to your organization. This command is
equivalent to [`wendy cloud login`](../cloud/login.md), the recommended entry point.
```sh
wendy cloud login
```
Complete sign-in in your browser, select an organization if asked, and return to
the terminal. Wendy saves the credentials needed for later commands. You can
supply your email first with `--email you@example.com`.
## Auth contexts
A **context** is a saved sign-in for one organization. The first is named
`default`. Sign in again and choose another organization to add a second context;
your current context stays selected.
```sh
wendy auth status
wendy auth use
wendy auth use
```
`status` lists your sign-ins and marks the current one. `use` opens a picker;
providing a name selects it directly. Use [`wendy auth rename`](./rename.md) to
make a saved context easier to recognize.
Commands use the current context, or the only saved context if there is just one.
With several contexts and none selected, interactive commands show a picker.
Scripts must select a context first or pass `--cloud-grpc` to select its endpoint.
If several contexts share an endpoint, the current matching context takes priority.
Wendy normally refreshes your session automatically. If a command asks you to
sign in again, rerun `wendy cloud login`; you do not need to log out first.
## Service-account login
Use a service account for unattended Cloud API calls in scripts and CI.
Ask your organization administrator to create one and provide a one-time
enrollment secret and its sign-in URL.
1. Register a key on the machine that will use it:
```sh
wendy auth service-account enroll \
--issuer https://auth.wendy.dev/realms/ \
--output wendy-service-account.json
```
2. Sign in using the saved key:
```sh
wendy cloud login --service-account wendy-service-account.json
```
The secret is used once. The key file is private: keep it out of source control
and give it only to the intended CI job. You can instead store its contents in
`WENDY_SERVICE_ACCOUNT_KEY` and run `wendy cloud login`. Explicit personal-login
flags take precedence over that variable.
Service accounts obtain new access tokens automatically while their key remains
authorized. They do not receive the personal device certificate used by direct
device and Lite relay connections. Disabling an account prevents new tokens;
already issued tokens remain usable until expiry, normally up to one hour.
## Advanced: alternate environments
Production Wendy Cloud is the default. Your administrator may provide different
settings for development or a self-hosted deployment.
| Flag | Purpose |
| --- | --- |
| `--development` | Use Wendy's development environment. |
| `--issuer` | Use a particular organization sign-in URL. |
| `--client-id` | Use an administrator-registered login client. |
| `--auth` | Override the sign-in service address. |
| `--cloud`, `--cloud-grpc` | Override the dashboard and Cloud API endpoints. |
| `--resource` | Override the Cloud API authentication audience. |
| `--pki-identity-endpoint`, `--pki-resource` | Override the certificate service URL and audience. |
| `--api-key` | Use local authentication. |
| `--legacy` | Use the older dashboard sign-in flow. |
Current personal sign-ins support both device access and signed Cloud changes.
Legacy and local-API-key sign-ins cannot authorize all current Cloud operations;
sign in with the default browser flow if a command requires it.
See [certificate renewal](./refresh-certs.md) for connection troubleshooting.
---
# Logout
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/auth/logout/
---
title: "Logout"
description: "Removes all stored authentication credentials from ~/.wendy/config.json. After logging out, commands that connect to a provisioned device on the local network will return an author"
---
Removes all stored authentication credentials from `~/.wendy/config.json`. After logging out, commands that connect to a provisioned device on the local network will return an authorization error and prompt you to run `wendy auth login`.
---
# wendy auth refresh-certs
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/auth/refresh-certs/
---
title: "wendy auth refresh-certs"
description: "Renew the certificate Wendy uses to connect to your organization's devices. Wendy normally renews it automatically near expiry; use this command when a connection reports an expire"
---
# `wendy auth refresh-certs`
Renew the certificate Wendy uses to connect to your organization's devices.
Wendy normally renews it automatically near expiry; use this command when a
connection reports an expired or incompatible certificate.
## Renew and retry
```sh
wendy auth status
wendy auth refresh-certs
```
Then retry the command that failed. Renewal applies only to your current saved
sign-in. To use another one, select it first:
```sh
wendy auth use
wendy auth refresh-certs
```
To renew every saved sign-in explicitly:
```sh
wendy auth refresh-certs --all
```
With `--all`, each result is reported separately. The command exits with an
error if any renewal fails, even when others succeed.
## If renewal is refused
Sign in again to obtain a fresh certificate:
```sh
wendy cloud login
```
An expired certificate, an older sign-in, or an organization's renewal limits
can require a new login. Interactive runs may offer this directly. If a
connection fails while you have several saved sign-ins, follow the named-context
instruction rather than renewing an unrelated organization.
If fresh sign-in does not help, retry the failing command with
`WENDY_TLS_DEBUG=1` and share the diagnostics with your administrator.
## Advanced: self-hosted certificate renewal
Certificate-only sign-ins need a renewal service in your deployment. Wendy
normally derives it from the saved certificate service address. If your
deployment uses a different address, your administrator can provide
`WENDY_PKI_RENEW_ENDPOINT`, for example
`https://renew.example.com/v1/renew`. A Cloud endpoint alone does not identify a
certificate renewal service.
## Related
- [Sign in to Wendy Cloud](../cloud/login.md)
- [Choose a saved sign-in](./use.md)
---
# wendy auth rename
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/auth/rename/
---
title: "wendy auth rename"
description: "Renames an auth context. Context names are how wendy auth use selects a session."
---
# `wendy auth rename`
Renames an auth **context**. Context names are how [`wendy auth use`](./use.md)
selects a session.
## Usage
```sh
wendy auth rename [old]
```
## Description
With **two** arguments, renames the context `` to ``. With **one**
argument, renames the current context to `` (see
[`wendy auth default`](./default.md) for the current context).
The first login is always named `default`; rename it once you add a second
organization so both contexts have meaningful names. If the renamed context is
the current one, it stays current under its new name.
Renaming onto an existing context name, or to an empty name, is refused.
## Flags
_None._
## Examples
```sh
# Rename the default context after adding a second org
wendy auth rename default acme
# Rename a specific context
wendy auth rename org-42 staging
# Rename the current context
wendy auth rename prod
```
## See also
- [`wendy auth use`](./use.md) — switch the current context
- [`wendy auth default`](./default.md) — show or clear the current context
- [`wendy auth login`](./login.md)
---
# wendy auth use
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/auth/use/
---
title: "wendy auth use"
description: "Switches the current auth context — the session cloud and device commands use when several are stored and no --cloud-grpc flag is given."
---
# `wendy auth use`
Switches the current **auth context** — the session cloud and device commands use when several are stored and no `--cloud-grpc` flag is given.
> **Note:** The common auth flow — [`login`](../cloud/login.md),
> [`logout`](../cloud/logout.md), and [`status`](../cloud/status.md) — is
> surfaced under `wendy cloud`. Advanced context management (`use`, `rename`,
> `default`, `refresh-certs`) remains under `wendy auth`, which is hidden from
> the top-level help but fully functional.
## Contexts
Every login is stored as a named context. The first login is always named
`default` (you are never prompted to name it), so single-org users never deal
with names at all. Additional logins get a derived name (the realm, or `org-`);
rename any of them with [`wendy auth rename`](./rename.md). Each context owns its
own operator certificate; switching context switches the identity every command
uses.
## Usage
```sh
wendy auth use [context]
```
## Description
With a **context** argument, `wendy auth use` makes that context current and
persists the choice in `~/.wendy/config.json`. With no argument in an
interactive terminal, a picker is shown instead.
The argument is primarily a **context name** (see [`wendy auth status`](../cloud/status.md)).
For backward compatibility it also accepts the legacy selectors:
- A plain integer — matched against the organization ID of a **legacy session's**
certificate. A session whose certificate carries a tenant UUID (any OIDC login)
is never matched by integer; select it by name.
- Any other string — a case-insensitive substring of the gRPC endpoint or
dashboard URL, or an exact match against a session's tenant UUID.
An ambiguous selector (multiple matches) lists the candidates and errors. A
selector that matches nothing also errors. Contexts without certificate material
are rejected; re-run `wendy auth login` to refresh them.
## Flags
_None._
## Interactive picker keys
When the picker is shown (e.g. `wendy auth use` with no argument, or any cloud
command that reaches the multi-context branch):
| Key | Action |
|-----|--------|
| `↑` / `↓` | Navigate contexts |
| `enter` | Use this context for the current invocation only |
| `d` | Switch to the highlighted context (equivalent to `wendy auth use`); on the current context, clear it |
| `q` / `Ctrl+C` | Cancel |
The current context is marked with `✦`.
## Examples
```sh
# Switch by context name
wendy auth use acme
# The default context
wendy auth use default
# Legacy: switch by org ID or endpoint substring
wendy auth use 7
wendy auth use prod.example.com
# Interactive picker (TTY only)
wendy auth use
```
## See also
- In the Cloud tab of `wendy discover` or an interactive device picker, press
`o` to switch organizations, including organizations without local
credentials.
- [`wendy auth rename`](./rename.md) — rename a context
- [`wendy auth default`](./default.md) — show or clear the current context
- [`wendy auth login`](./login.md)
---
# Build
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/build/
---
title: "Build"
description: "Builds your WendyOS app. Leverages wendy.json for metadata. If wendy.json is missing, it prompts you to generate one."
---
Builds your WendyOS app. Leverages `wendy.json` for metadata. If `wendy.json` is missing, it prompts you to generate one.
The build command is mainly used to verify your app can build/compile.
## Flags
| Flag | Default | Description |
|---|---|---|
| `--build-type` | auto-detected | Build type to use when multiple project markers are present: `docker`, `swift`, or `python`. |
| `--dockerfile` | auto-detected | Build file to build from: a `Dockerfile`, `Containerfile`, a dot/hyphen variant of either (`Dockerfile.prod`), or a Stagefile (`prod.stagefile.yaml`). A bare filename in the project directory, not a path. Shows a selection menu when multiple build files exist — see [Selecting a build file](#selecting-a-build-file). |
| `--builder` | auto | Image builder to force for Dockerfile/Containerfile builds: `docker`, `apple-container`, or `buildkit`. `buildkit` runs the verification build in a containerd-backed BuildKit worker. |
| `--stagefile-backend` | `dockerfile` | Stagefile compiler backend: `dockerfile` or experimental direct `llb`. |
| `--gpu-arch` | from the device | GPU architecture a Stagefile `cuda:` stage targets; taken from the device when one is selected. |
| `--debug` | `false` | Build compiled languages unoptimized instead of the release default — see below. |
| `--service ` | all services | Build only the named service and its dependencies (multi-service projects) — see [Multi-service manifests](#multi-service-manifests). |
| `--max-concurrency ` | `0` | Multi-service: max service images to build at once (0 = default limit of 4). |
### `--debug`
`--debug` switches compiled-language stages from the release default to an unoptimized build:
- **Stagefile projects** — Swift stages emit `swift build -c debug` and Rust stages drop `--release`. It overrides the profile a stage declares, whichever Stagefile is being built — the canonical `build.stagefile.yaml` or a variant named with `--dockerfile`. Go and Node (`npm`/`yarn`/`pnpm`) stages have no optimization level to switch and are unaffected.
- **Compose projects** — applies to services built from a Stagefile.
- **Swift packages without a build file** — the host cross-compile runs `swift build -c debug`.
Hand-written Dockerfiles are not rewritten, and `wendy build` passes no build-args, so `--debug` has no effect on them here. To gate a hand-written Dockerfile on it, branch on the `WENDY_DEBUG` build-arg that [`wendy run`](run.md) passes (see the build-arg table below).
The same flag on [`wendy run`](run.md) and `wendy fleet run` additionally enables debug logging, and on `fleet run` host networking. On `wendy build` it only selects the build configuration.
## Manifest detection
`wendy build` scans the project directory for a build manifest in the following priority order:
1. `wendy.json` with a non-empty `services` map — multi-service build. Takes precedence unconditionally, even over a root-level Compose file, Dockerfile, or other marker below; each service resolves its own build file independently inside its `context` directory, using the same per-directory Stagefile-before-Dockerfile rule as the Stagefile entry below. See [Multi-service manifests](#multi-service-manifests).
2. `docker-compose.yml` / `compose.yml` — multi-service Compose project
3. `build.stagefile.yaml` / `.stagefile.yaml` — Stagefile, compiled to a digest-pinned Dockerfile at build time. Checked ahead of plain Dockerfiles: a Stagefile is the more explicit build signal, and a project that has one usually keeps a generated Dockerfile beside it.
4. `Dockerfile` / `Containerfile` (or dot/hyphen variants) — container image build
5. `Package.swift` — Swift Package Manager project
6. `*.xcodeproj` — Xcode project (macOS targets only)
7. `requirements.txt` / `setup.py` / `pyproject.toml` — Python project (Dockerfile auto-generated)
If multiple manifests are present you can override detection with `--build-type`; a `services` map is the exception — it always wins, and `--build-type`/`--dockerfile` are rejected outright rather than used to pick something else (see [Multi-service manifests](#multi-service-manifests)).
A Stagefile is a YAML build descriptor that compiles to a real Dockerfile with guarantees a hand-written one does not get by default: base images are digest-pinned through a committed lockfile, each install step is emitted with the correct flags and a scoped cache mount, the `.dockerignore` is derived from the declared copy paths, and there is no raw-shell escape hatch. The compiled Dockerfile and its paired `.dockerignore` are written next to the source as build output — see [Stagefile naming](#stagefile-naming) for the artifact names. Most projects under [`Examples/`](https://github.com/wendylabsinc/wendyos/tree/main/Examples) use this format and are worth reading as reference.
The default `--stagefile-backend=dockerfile` keeps this established path. To
exercise the experimental backend, pass `--stagefile-backend=llb` (or set
`WENDY_STAGEFILE_BACKEND=llb`): Wendy lowers the same resolved graph directly
to BuildKit LLB and skips Dockerfile interpretation. The direct backend keeps
the same lockfile, variants, `--debug`, GPU target, ROS 2 options, OCI
chunk-diff, and registry/mTLS push paths. It requires Docker or a BuildKit
daemon and is incompatible with Apple Container. `wendy build` supports it for
a single Stagefile image, but not for Compose; remote `--build-host` builds
also remain Dockerfile-only.
## Multi-service manifests
A `wendy.json` with a non-empty `services` map builds one **local** image per service instead of a single image — the same manifest [`wendy run`](run.md) detects and orchestrates, but building only. Nothing is pushed to a registry and nothing is created or started; no device is required.
Each image is tagged `-:latest`. For example, an app with `appId: "sh.wendy.examples.isolatedservices"` and services `api` and `worker` produces:
```
sh.wendy.examples.isolatedservices-api:latest
sh.wendy.examples.isolatedservices-worker:latest
```
Each service resolves its own build file independently inside its own `context` directory — Stagefile preferred over Dockerfile/Containerfile, the same rule [Manifest detection](#manifest-detection) uses per directory. Because there is no single build file or type to pick across services, `--dockerfile` and `--build-type` are not supported here and return an error.
Build only one service and its dependencies with `--service`:
```sh
wendy build --service api
```
`--service ` resolves `` plus every service reachable through its `dependsOn` graph; services outside that subset are not built. An unknown service name returns an error immediately.
Up to 4 service images build in parallel by default. Override with `--max-concurrency `; `0` restores the default limit of 4.
`--builder`, `--gpu-arch`, and `--debug` behave exactly as in a single-image build and are applied per service. Docker loads each result into its local image store, Apple Container uses its implicit store, and BuildKit stores each verification result in its private worker. As with single-image builds, `wendy build` passes no build-args, so a hand-written Dockerfile's `ARG WENDY_PLATFORM` / `ARG WENDY_DEBUG` take their declared defaults here.
With no device selected, the target platform defaults to `linux/arm64`; selecting a device uses that device's own platform instead, same as single-image builds.
A `services` map takes precedence over any root-level build marker present in the same project — see [Manifest detection](#manifest-detection).
## Selecting a build file
A project may hold several container build files at once — `Dockerfile`, `Dockerfile.prod`, `Containerfile`, `build.stagefile.yaml`, `gpu.stagefile.yaml`. When more than one is present and the shell is interactive, `wendy build` shows a picker listing all of them, Stagefiles first. In CI (or any non-interactive shell) there is no picker, so name the file explicitly:
```sh
wendy build --dockerfile gpu.stagefile.yaml
wendy build --dockerfile Dockerfile.prod
```
`--dockerfile` is accepted by `wendy run`, `wendy watch`, and `wendy fleet run` too, and takes a Stagefile on all of them. `--build-type` is a coarser control: it picks the *project type* (`docker`, `swift`, `python`), not the file — Stagefiles build through the `docker` type.
### Stagefile naming
A Stagefile is named `.stagefile.yaml`, where `` matches `[A-Za-z0-9][A-Za-z0-9_-]*` — letters, digits, underscores and hyphens, **no dots**. `build.stagefile.yaml` is the canonical name: it leads the picker and is the one chosen wherever a default is needed.
The no-dots rule is what keeps `build.stagefile.lock.yaml` out of the family — a lockfile is a build artifact, not a rival build file. It also means a name like `my.app.stagefile.yaml` is *not* recognised as a Stagefile and will not appear in the picker; use `my-app.stagefile.yaml` instead.
Each Stagefile keeps its own artifacts next to it, so two variants never overwrite each other's output:
| Source | Compiled Dockerfile | Lockfile |
|---|---|---|
| `build.stagefile.yaml` (canonical) | `Dockerfile.generated` | `build.stagefile.lock.yaml` |
| `prod.stagefile.yaml` | `Dockerfile.generated.prod` | `prod.stagefile.lock.yaml` |
| `gpu.stagefile.yaml` | `Dockerfile.generated.gpu` | `gpu.stagefile.lock.yaml` |
Everything in the `Dockerfile.generated*` namespace is a build artifact: it is excluded from manifest detection and from the `wendy watch` rebuild triggers, and it is worth adding to `.gitignore`. Lockfiles are the exception — commit them, they pin the digests your build resolved.
## Compatibility
| Manifest | Required host | Notes |
|---|---|---|
| `wendy.json` `services` map | Docker Desktop, Apple `container` on Apple silicon macOS, or a containerd-backed BuildKit daemon | Local build only — no push, no device required; each service resolves and builds independently with `--builder docker`, `--builder apple-container`, or `--builder buildkit`. See [Multi-service manifests](#multi-service-manifests) |
| `.stagefile.yaml` | Same as `Dockerfile` | Compiled to `Dockerfile.generated[.]` and built through the Dockerfile path by default, so every builder below applies unchanged; `--stagefile-backend=llb` solves the same graph directly with Docker/BuildKit. |
| `Dockerfile` / `Containerfile` | Docker Desktop, Apple `container` on Apple silicon macOS, or WendyOS | Local Docker builds use `docker buildx`; `--device apple-container` uses `container build`; WendyOS device builds can select `--builder docker` or `--builder apple-container` |
| `Package.swift` | macOS or Linux | Requires a host Swift toolchain |
| `*.xcodeproj` | macOS only | Built with `xcodebuild`; `Brewfile.wendy` is the auto-detected target-agent Brewfile for native Mac runs |
## Build paths
### Dockerfile and Containerfile projects
`wendy build` invokes an image builder targeting the device's CPU architecture. [`wendy run`](run.md) passes the following build-args so the Dockerfile or Containerfile can adapt to the target hardware — declare them with `ARG` to use them. `wendy build` compiles the same image but passes no build-args, so a Dockerfile that branches on them takes its `ARG` defaults here:
On Apple silicon Macs with [Apple `container`](https://github.com/apple/container)
installed, Wendy tries Apple Container first for Dockerfile and
Containerfile builds when `--builder` is omitted. If Apple Container is
unavailable or the build fails, Wendy falls back to Docker. Use
`--builder docker` to force Docker, or `--builder apple-container` to require
Apple Container:
```sh
wendy --device my-wendy.local build
```
Wendy automatically checks for the `container` CLI and offers to install it via Homebrew if missing, and starts the `system` and `builder` services if they are not running.
To bypass both Docker and Apple Container explicitly, select the `buildkit`
backend. If the optional Local Build Service is running from the Wendy menu-bar
app, Wendy uses its socket at `/runtime/buildkitd.sock`. You can
target another BuildKit daemon by setting the endpoint explicitly:
```sh
WENDY_BUILDKIT_HOST=unix:///path/to/buildkitd.sock \
wendy build --builder buildkit
```
The verification result is committed to that worker's private containerd
store; it is not used to run Mac applications. Apple `container` remains the
primary Mac application runtime. `BUILDKIT_HOST` and buildctl's normal local
daemon default are also honored; the Wendy-prefixed variable takes precedence.
The host must have `buildctl` installed.
If Apple Container reports an empty build context for a project under `/tmp` or
`/private/tmp`, Wendy returns an error with the known workaround: move the
project to a non-`/tmp` directory and retry.
For local-only Dockerfile or Containerfile builds on the Mac itself, select the
local provider with `--device apple-container`. Compose projects still require
Docker for local provider runs.
| Build-arg | Values | Notes |
|---|---|---|
| `WENDY_PLATFORM` | `nvidia-jetson` \| `generic` | Platform tier derived from the device type |
| `WENDY_DEBUG` | `true` \| `false` | Set when `--debug` is passed. [`wendy project optimize`](project/optimize.md) flags it when it's declared (`ARG`/`ENV`) but no `RUN` step branches on it — gate your optimization level on it so debug builds aren't shipped to release. |
| `WENDY_DEVICE_TYPE` | e.g. `jetson-agx-orin` | Raw device type; absent when unknown |
| `WENDY_HAS_GPU` | `true` \| `false` (hardware presence) | Absent on older agents |
| `WENDY_HAS_CUDA` | `true` \| `false` (host CUDA support) | Falls back only to an explicit NVIDIA vendor on older agents |
| `WENDY_GPU_VENDOR` | e.g. `nvidia`, `qualcomm` | Absent when no GPU is reported |
| `WENDY_JETPACK_VERSION` | e.g. `6.0` | Jetson only |
| `WENDY_JETPACK_MAJOR` | e.g. `6`, `7` | Jetson only; JetPack major for per-generation base-image selection |
| `WENDY_CUDA_VERSION` | e.g. `12.6` | Jetson only |
| `WENDY_GPU_ARCH` | e.g. `sm_87` | GPU architecture identifier; absent when no GPU is reported |
Example — selecting a base image by platform:
```dockerfile
ARG WENDY_PLATFORM=generic
FROM ${WENDY_PLATFORM}-base-image
```
### Swift Package Manager projects
- **macOS target**: builds locally with `swift build` and syncs the binary to the device.
- **Container target (WendyOS / Docker)**: uses [swift-container-plugin](https://github.com/apple/swift-container-plugin) to cross-compile the app for the target device architecture. The plugin takes a Swift base container image and appends your compiled executable and bundle resources. The cross-compile runs in **release** configuration by default; pass `--debug` to build in debug configuration instead.
Swift projects that build through a Stagefile (`build.stagefile.yaml`) instead follow the Stagefile path, where `--debug` overrides the stage's declared profile.
### Xcode projects
Builds with `xcodebuild`. Xcode project support exists for native Mac packages that cannot be built correctly with SwiftPM alone, for example packages that need Xcode-only resource or shader build steps.
Wendy passes `-skipMacroValidation` and `-skipPackagePluginValidation` so `xcodebuild` can run from a headless CLI/agent session. Xcode's macro/plugin prompts are an interactive consent layer on top of SwiftPM's build-time code and package-plugin sandbox model; headless Wendy builds treat invoking the build as consent, similar to CLI build tools. Only use Xcode projects with trusted, pinned package dependencies.
For native Mac runs, if a `Brewfile.wendy` is present in the project root, Wendy applies it on the target Mac before starting the app. A plain project-root `Brewfile` is left for developer-machine setup unless explicitly referenced by `wendy.json`.
## Build progress output
In an interactive terminal, image builds render a live list of Dockerfile steps. BuildKit runs steps concurrently, so several can advance at once; each running step gets a dim sub-line showing whatever progress its output exposes — a compiler's `[525/1027]` counter, `pip`/`apt`/`wget` download percentages, or the byte counter and transfer rate BuildKit reports for base-image pulls and build-context transfer. Steps that produce no recognised progress fall back to their last line of output. Finished steps collapse to a single line.
When stdout is not a TTY (CI, piped output), a plain-text renderer is used instead. It prints a heartbeat line for each running step every 15 seconds so a long silent step is distinguishable from a hung build.
Progress detail for Docker builds comes from `docker buildx --progress=rawjson`, which requires buildx 0.13 or newer. The CLI probes the installed buildx version once per run and falls back to plain progress on older versions; set [`WENDY_BUILD_PROGRESS`](../global-flags.md#environment-variables) to override the choice.
## Post-build optimization hint
After a **slow incremental build** — one that reused cached layers but still took longer than ~50 seconds — `wendy build` runs a quick static [`wendy project optimize`](project/optimize.md) scan and, if it finds fixable build-config issues, prints them and offers to apply the safe fixes:
```
This incremental build took 1m4s. A quick scan found 2 build-config issue(s):
…
Apply 2 safe fix(es) now? (takes effect on your next build) [Y/n]
```
The fixes take effect on your **next** build; the build that just completed is unchanged. This nudge is purely interactive and is a **no-op in CI / non-interactive shells**. A separate one-line tip pointing at `wendy project optimize` is throttled to at most once per day per project.
## Platform support for Swift projects
`wendy build` for Swift packages requires a host Swift toolchain and is supported on **macOS and Linux hosts only**. On Windows, `wendy build` returns an error for Swift projects. The recommended alternative is to provide a `Dockerfile` or `Containerfile` and use `wendy run`, which routes through the image build path on all platforms.
## Remote build host
Delegating a build to another device is a [`wendy run`](run.md#remote-build-host) feature, not a `wendy build` one, and `wendy build --build-host` returns an error saying so.
The reason is that the two commands mean different things by "build". `wendy run --build-host` has a target device: the build host builds the image and pushes it straight into *that* device's registry over the mesh. `wendy build` has no target — it leaves an image behind on the machine that built it — so a remote build would deposit the image on the build host and nowhere useful, which is worse than not offering the flag.
Use `wendy run --build-host ` instead. To make a device willing to accept builds in the first place, see [`wendy device build-host`](device/build-host.md).
CUDA-selecting Dockerfiles must use `WENDY_HAS_CUDA`. `WENDY_HAS_GPU`
reports hardware presence, including Broadcom and other GPUs without CUDA.
Device info exposes `gpuCapabilities`, one entry per detected GPU with its
`vendor`, `path`, and `computeBackends` (`cuda`, `rocm`, `metal`). A GPU whose
backend list is empty has no supported backend; no entries at all on a device
that reports a GPU means an older agent. An NPU runtime such as `qnn` is
reported separately, in `npuBackends`. `containerStorage` identifies the filesystem used by
containerd; the existing disk scalar fields continue to describe the root filesystem.
---
# Clear
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/cache/clear/
---
title: "Clear"
description: "Immediately clears all caches, removing the artifacts from disk."
---
Immediately clears all caches, removing the artifacts from disk.
---
# List
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/cache/list/
---
title: "List"
description: "Lists all local caches used by Wendy: downloaded OS images and the per-app build caches (buildx/ and ocilayout/) that make warm rebuilds and chunk-diff deploys fast."
---
Lists all local caches used by Wendy: downloaded OS images and the per-app build caches (`buildx/` and `ocilayout/`) that make warm rebuilds and chunk-diff deploys fast.
Pass `--json` to receive a JSON array on stdout instead of the human-readable output described below.
## Output structure
- **OS images** (`os-images/`) — each image file is listed as a separate row prefixed `os-images/`.
- **Build caches** (`buildx/` and `ocilayout/`) — each per-app sub-directory is listed as a separate row named `/-`, e.g. `ocilayout/com.example.myapp-linux_arm64` (app ID and platform are lower-cased, with characters outside `a-z`, `0-9`, `-` and `.` replaced by `_`). Build-cache rows also carry a last-built age. The root-level shared store inside each of these directories (`blobs/`, `ingest/`, index files) is folded into the summary line rather than listed, because it cannot be deleted without taking every app cache in that store down with it.
- **Everything else** — listed as a single top-level row.
Entries are sorted largest-first so the biggest reclaimable items are at the top, in both interactive and non-interactive output.
> **Build-cache sizes are per-link.** Automatic maintenance hardlinks a layer shared by several apps into each of their directories. A per-row size (and the `sizeBytes` / `size` JSON fields) counts that layer once per link, so build-cache rows can sum past the real on-disk total. The summary line's "on disk" figure is the dedup-aware total — the same number the automatic size cap is enforced against.
When `--json` is **not** passed:
- In an **interactive terminal** (stdin and stdout are both TTYs), an interactive picker is shown listing all cached items. Build-cache entries show their last-built age alongside the size (e.g. `2.0 GB · built 3h ago`); other entries show only the size. When build caches are present, a summary line showing real on-disk usage and the cap appears above the picker. Selected items can be removed.
- In a **non-interactive** context (pipe, CI, etc.), a plain text list is printed, one entry per line. Build-cache rows use the format ` (, built )`; all other rows use ` ()`. When build caches are present, a summary line is printed after the list:
```
ocilayout/com.example.myapp-linux_arm64 (2.0 GB, built 3h ago)
os-images/wendyos-raspberry-pi-5-0.10.4.img (4.5 GB)
Build cache: 83.0 GB on disk (1.2 GB shared) · cap 100.0 GB
```
"on disk" is the dedup-aware total; "shared" is the size of the root-level shared store (`blobs/`, `ingest/`) that is never evicted on its own; "cap" is the current size cap (see [Automatic maintenance](#automatic-maintenance)).
When the cache is empty (or the cache directory does not exist), the command prints `Cache is empty.` in human-readable mode, or an empty JSON array (`[]`) with `--json`.
## JSON output
### `wendy cache list --json`
Each element of the returned array represents one cached item. The summary line is not included in JSON output.
```json
[
{
"name": "os-images/wendyos-raspberry-pi-5-0.10.4.img",
"path": "/Users/you/Library/Caches/wendy/os-images/wendyos-raspberry-pi-5-0.10.4.img",
"sizeBytes": 4831838208,
"size": "4.5 GB"
},
{
"name": "ocilayout/com.example.myapp-linux_arm64",
"path": "/Users/you/Library/Caches/wendy/ocilayout/com.example.myapp-linux_arm64",
"sizeBytes": 2147483648,
"size": "2.0 GB",
"lastBuilt": "3h ago"
}
]
```
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Display name of the cache entry. OS images are prefixed with `os-images/`; per-app build caches with `buildx/` or `ocilayout/` |
| `path` | string | Absolute path to the cached file or directory |
| `sizeBytes` | integer | Size in bytes. For build-cache entries this is the per-link size and can exceed the entry's share of real disk usage (see the note above) |
| `size` | string | Human-readable size string |
| `lastBuilt` | string | How long ago a build last wrote to this cache (e.g. `"12m ago"`, `"3h ago"`, `"2d ago"`). Only present on build-cache entries, and omitted when the directory holds no layers yet |
> **Note:** `path` is a full local filesystem path and may include the current username or CI runner cache location. Redact it before forwarding command output to shared logs or support channels.
Returns `[]` when the cache is empty.
### `wendy os cache list --json`
Each element represents one cached OS image file:
```json
[
{
"name": "wendyos-raspberry-pi-5-0.10.4.img",
"sizeBytes": 4831838208,
"size": "4.5 GB"
}
]
```
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Filename of the cached OS image |
| `sizeBytes` | integer | Size in bytes |
| `size` | string | Human-readable size string |
Returns `[]` when no cached OS images are found.
## Automatic maintenance
The build caches are bounded automatically. After a `wendy run` deploy that exports through the persistent OCI layout (the chunk-diff path used with the Docker builder), Wendy:
1. **Deduplicates** content-identical layers across the `buildx` and `ocilayout` stores by hardlinking them, so a layer shared by several apps — or held in both stores — costs one copy instead of N.
2. **Evicts** whole per-app cache directories, least-recently-built first, until the dedup-aware on-disk total is at or under the size cap. Evicting a directory only frees a layer when it held the layer's last remaining link.
3. **Bounds the builder's own store** by running `docker buildx prune --max-used-space` against the OCI-export builder (best-effort, with a 60-second budget).
No manual action is needed. Maintenance is best-effort: a failure leaves the caches as they are for the next run to reclaim.
### `WENDY_BUILD_CACHE_MAX_BYTES`
Set this environment variable (in bytes) to override the default size cap of 100 GiB. A value of `0` or a negative number disables eviction and the builder-store prune; deduplication still runs.
### Eviction safety guards
A per-app cache directory is never evicted while it may be in use:
- **Lock protection** — a directory whose layout lock is held by another `wendy` process (for the whole of its build → read → push) is skipped, however old its contents look. It becomes eligible again once that process releases the lock.
- **Active-window protection** — a directory that a build wrote a layer into within the last 10 minutes is skipped as a likely in-progress build.
- **Own build** — the deploying process always pins its own app's directory.
### `wendy cache dedup`
A hidden `wendy cache dedup [--max ]` command runs the same on-disk maintenance on demand, for manual reclaim or debugging. It is not shown in `wendy cache --help` because the automatic path is the intended one.
- `--max` sets the size cap for this run (defaults to `WENDY_BUILD_CACHE_MAX_BYTES` or 100 GiB). Pass `0` to deduplicate only, without evicting anything.
- It only ever deduplicates or deletes cache content, honours the same safety guards as automatic maintenance, and is safe to run at any time.
- It prints `Deduplicated , evicted .` on completion.
---
# wendy cloud discover
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/cloud/discover/
---
title: "wendy cloud discover"
description: "Lists enrolled compute devices visible to the authenticated Wendy Cloud organization."
---
# `wendy cloud discover`
Lists enrolled compute devices visible to the authenticated Wendy Cloud organization.
## Usage
```sh
wendy cloud discover [flags]
```
## Description
`wendy cloud discover` queries the Wendy Cloud asset service for devices registered in your organization. By default **only online devices** (those with an active broker presence) are returned. Pass `--all` to include devices that are currently offline.
In an interactive terminal the command launches a **live TUI** that refreshes the device list every 10 seconds and fetches the running agent version for each device concurrently. In non-interactive environments (pipes, CI, or when `--json` is passed) a JSON array is written to stdout instead.
When no devices match the filter, a contextual message is shown:
- **Default (online only):** `No online devices found. Use --all to include offline devices.`
- **With `--all`:** `No enrolled devices found.`
## Interactive TUI
The TUI displays a table with the following columns for each device:
| Column | Description |
|--------|-------------|
| Name | Device name |
| Type | Hardware device type |
| Version | Running agent version (fetched live; `—` while loading) |
The device's IP address is not shown as a column — cloud devices are reached by
name/ID through the broker tunnel. The address is still included in the
clipboard/JSON output (see below).
### Keyboard shortcuts
| Key | Action |
|-----|--------|
| `↑` / `↓` | Navigate the device list |
| `enter` | Copy the selected device's info as JSON to the clipboard |
| `a` | Copy all listed devices as JSON to the clipboard |
| `u` | Update the selected device's agent binary to the latest release |
| `q` / `Ctrl+C` | Quit |
### In-place device update (`u`)
Pressing `u` on a highlighted device downloads the latest agent release from GitHub, uploads it to the device via the broker tunnel, and waits for the agent to restart (up to 60 seconds). A status message is shown during the update and the version column refreshes automatically on completion.
## JSON output
When run non-interactively (output is piped) or with `--json`, a JSON array is written to stdout. Each element contains:
| Field | Type | Description |
|-------|------|-------------|
| `id` | string or integer | UUID string for principal-based Cloud v2 sessions; numeric asset ID for legacy v1 sessions (omitted when zero). |
| `name` | string | Device name (may be empty for devices enrolled without a name). |
| `type` | string | Human-readable hardware device type. |
| `address` | string | IP address reported by the cloud. |
| `version` | string | Running agent version. Omitted when it could not be determined. |
Principal-based sessions use the Cloud v2 API for interactive discovery and JSON
output, including when output is piped. Clipboard copies preserve device UUIDs,
and interactive version probes, updates, and picker selections use v2 tunnels.
The standalone `cloud tunnel --device` command still resolves legacy numeric IDs.
Example:
```json
[
{"id": 42, "name": "playful-reed", "type": "Raspberry Pi 5", "address": "192.168.1.10", "version": "0.10.4"},
{"id": 43, "name": "", "type": "Raspberry Pi 5", "address": "192.168.1.11"}
]
```
The `id` field is the primary mechanism for addressing a device that was enrolled without a name — pass it to `wendy cloud tunnel --device `.
## Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--all` | `false` | Include offline devices in the results. When omitted only devices with an active broker presence are shown. |
| `--broker-url` | `$WENDY_BROKER_URL` (or derived from cloud endpoint) | Tunnel broker `host:port`. When empty the CLI derives the address from the cloud gRPC endpoint. |
| `--cloud-grpc` | `""` | Cloud gRPC endpoint. Overrides session selection. When multiple sessions are stored and no default is set, an interactive terminal shows a session picker; a non-interactive environment errors. |
## Examples
Show only online devices (default):
```sh
wendy cloud discover
```
Show all enrolled devices, including offline ones:
```sh
wendy cloud discover --all
```
Point at a specific cloud gRPC endpoint:
```sh
wendy cloud discover --cloud-grpc grpc.cloud.wendy.dev:443
```
Output JSON (non-interactive / scripting):
```sh
wendy cloud discover --json
# or pipe to force non-interactive mode:
wendy cloud discover | cat
```
## See also
- [`wendy cloud tunnel`](./tunnel.md) — open a tunnel to a discovered device.
---
# wendy cloud login
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/cloud/login/
---
title: "wendy cloud login"
description: "Sign in to Wendy Cloud to enroll devices, deploy remotely, and manage your organization's devices."
---
# `wendy cloud login`
Sign in to Wendy Cloud to enroll devices, deploy remotely, and manage your
organization's devices.
```sh
wendy cloud login
```
Complete sign-in in the browser. Choose your organization if prompted, then
return to the terminal. Wendy saves your sign-in and uses it automatically for
later commands.
## Check or change your sign-in
```sh
wendy auth status
wendy auth use
```
Sign in again to add another organization without replacing your current one.
Use `wendy auth use` to choose between saved sign-ins. See
[sign-in contexts](../auth/login.md#auth-contexts) for details.
## Other sign-in options
```sh
wendy cloud login --email you@example.com
wendy cloud login --service-account ./wendy-service-account.json
wendy cloud login --legacy
```
`--email` supplies your email before opening the browser. A service-account key
supports unattended Cloud API access; it does not provide a personal device
certificate. See [service-account setup](../auth/login.md#service-account-login).
## Existing Cloud v1 devices
`wendy cloud login` signs in to Cloud v2. For devices still enrolled in Cloud
v1, `wendy cloud login --legacy` opens the legacy dashboard at `cloud.wendy.sh`.
The flag is hidden from ordinary CLI help but remains supported. It cannot be
combined with `--email`, `--issuer`, `--service-account`, or `--api-key`.
Signing in to v2 does not move v1 devices. See the [Cloud guide](../../../../../../cloud/)
for the Cloud v2 and Legacy workflows, and [Moving to Cloud v2](../../../../../../cloud-migration/)
before changing an existing device's enrollment.
## Advanced flags
Hosted Wendy Cloud uses production settings by default. Use overrides only
when your administrator supplies them.
| Flag | Description |
| --- | --- |
| `--development` | Use Wendy's development environment. |
| `--issuer` | Sign in through a particular organization sign-in URL. |
| `--service-account` | Service-account key file. `WENDY_SERVICE_ACCOUNT_KEY` can supply its contents instead. |
| `--cloud` | Override the dashboard URL, normally `https://cloud.wendy.dev`. |
| `--cloud-grpc` | Override the Cloud API endpoint, normally `api.wendy.dev:443`. |
| `--legacy` | Use the Cloud v1 dashboard sign-in flow for existing v1 devices. |
`wendy auth login` remains an equivalent command. See its
[reference](../auth/login.md) for additional options and saved sign-ins.
---
# wendy cloud logout
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/cloud/logout/
---
title: "wendy cloud logout"
description: "Removes the stored Wendy Cloud auth session. Equivalent to wendy auth logout."
---
# `wendy cloud logout`
Removes the stored Wendy Cloud auth session. Equivalent to `wendy auth logout`.
## Usage
```sh
wendy cloud logout
```
## Description
`wendy cloud logout` clears the stored mTLS session from `~/.wendy/config.json`.
It reuses the same implementation as the (now hidden) `wendy auth logout` and
remains the surfaced entry point for signing out.
## See also
- [`wendy cloud login`](./login.md)
- [`wendy cloud status`](./status.md)
---
# wendy cloud mcp
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/cloud/mcp/
---
title: "wendy cloud mcp"
description: "Lets a web chat (ChatGPT, Claude) use Wendy through your own CLI session."
---
# `wendy cloud mcp`
Lets a web chat (ChatGPT, Claude) use Wendy through your own CLI session.
## Connect a web chat
1. Sign in and start a Wendy session in your terminal:
```sh
wendy cloud login
wendy cloud mcp
```
2. Add the Wendy connector in your web chat and sign in to the same organization.
3. Ask the chat to check Wendy's status. Compare the pairing code in the chat
with the code in your terminal, then accept only if they match.
4. Keep the terminal open. Press Ctrl+C when you want the chat's access to end.
Your organization must allow MCP and Hosted MCP. One client connection can pair
with each running CLI session; another client needs a separate session.
## Choose what the chat can do
By default, built-in device and Cloud read operations are available and ask for
terminal confirmation. To approve these reads automatically:
```sh
wendy cloud mcp --allow-read
```
To also permit deployment, while still confirming each deployment:
```sh
wendy cloud mcp --allow-read --allow-tool run
```
Host access, local discovery, and app-provided tools need explicit permission;
`--allow-read` does not approve them. The CLI reports unknown tool names and lists
the available choices. A call cannot switch to another Cloud organization or
endpoint behind your back.
Use `--allow-all` only when you want all supported operations, including host
access and device changes, to run without per-call confirmation. Pairing remains
a separate step. `--auto-accept` skips matching-code approval for ChatGPT's known
client only; it does not change tool permissions.
If pairing reports an incompatible tool catalog, update the Wendy CLI and restart
the session. The chat receives the tools supported by the hosted service; it may
not expose every tool in a newer CLI yet.
## Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--cloud-grpc` | `""` | Cloud gRPC endpoint of the session to use. |
| `--allow-tool` | none | Also allow these tools by name, comma-separated. Each call requires confirmation unless it qualifies for `--allow-read`. |
| `--allow-read` | `false` | Skip confirmation for built-in read-only device and Cloud tools that do not access the host. |
| `--allow-all` | `false` | Allow every supported relay tool without per-call confirmation. |
| `--auto-accept` | `false` | Automatically accept pairing from ChatGPT's known OAuth client. Tool permissions and confirmations are separate. |
## Examples
Pair a web chat with read-only access, confirming each call:
```sh
wendy cloud mcp
```
Allow built-in device and Cloud reads without confirmation:
```sh
wendy cloud mcp --allow-read
```
Also let it deploy, confirming each deploy:
```sh
wendy cloud mcp --allow-read --allow-tool run
```
Allow every supported tool without further tool confirmations after pairing:
```sh
wendy cloud mcp --allow-all
```
Automatically pair ChatGPT and allow built-in device and Cloud reads without confirmation:
```sh
wendy cloud mcp --auto-accept --allow-read
```
---
# wendy cloud status
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/cloud/status/
---
title: "wendy cloud status"
description: "Shows the currently active Wendy Cloud auth session. Equivalent to wendy auth status."
---
# `wendy cloud status`
Shows the currently active Wendy Cloud auth session. Equivalent to `wendy auth status`.
## Usage
```sh
wendy cloud status
```
## Description
`wendy cloud status` reports the stored Wendy Cloud session — the dashboard URL,
gRPC endpoint, certificate state, and the session key algorithm with the login
path that minted it (`Key: ML-DSA-65 (OIDC login)` or `Key: ECDSA P-256 (legacy login)`). It reuses the same implementation as the
(now hidden) `wendy auth status` and is the surfaced way to check who you are
logged in as.
When more than one session is stored, use [`wendy auth use`](../auth/use.md) to
inspect and set the default.
## See also
- [`wendy cloud login`](./login.md)
- [`wendy cloud logout`](./logout.md)
---
# wendy cloud tunnel
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/cloud/tunnel/
---
title: "wendy cloud tunnel"
description: "Forwards a local TCP or UDP port to a service on a cloud-enrolled device. Cloud v2 forwards any port with updated Cloud, CLI, and Agent versions; legacy sessions use the legacy bro"
---
# `wendy cloud tunnel`
Forwards a local TCP or UDP port to a service on a cloud-enrolled device. Cloud
v2 forwards any port with updated Cloud, CLI, and Agent versions; legacy
sessions use the legacy broker.
## Usage
```sh
wendy cloud tunnel :[/udp] [flags]
```
## Description
`wendy cloud tunnel` fetches the list of **online** compute devices from Wendy Cloud and either connects directly when only one device is available, selects the device named or identified by `--device`, or prompts you to choose one interactively.
The local listener binds to `127.0.0.1`. Traffic is forwarded through the cloud broker to `127.0.0.1:` on the device. The remote service must be listening on that address or on all device interfaces.
The protocol defaults to TCP. Append `/udp` to forward UDP datagrams, or `/tcp`
to select TCP explicitly. A single port uses the same port locally and remotely.
Ports must be between 1 and 65535.
### Cloud v2 ports
Cloud v2 forwards TCP or UDP to any device port with an updated Cloud service
and CLI and Agent [2026.10.22-211023](https://github.com/wendylabsinc/WendyOS/releases/tag/2026.10.22-211023)
or newer. Older v2 versions forward only TCP port 50052 for Wendy Agent and TCP
port 22 for SSH. Cloud selects the relay, so `--broker-url` is not accepted for
v2 sessions. Port 22 requires an actual SSH server; release Jetson and Raspberry
Pi images do not include one.
Use the [Cloud guide](../../../../../../cloud/) to choose the Cloud v2 or Legacy workflow.
See the [tunnel reference](../../../../../../advanced/cloud/tunnel/) for UDP limits and
catalog errors, and [Moving to Cloud v2](../../../../../../cloud-migration/) before moving an
app-port integration.
Keep the command running while using the forward. Press Ctrl+C to close it; the remote service keeps running.
### Selecting a device
Use a device name or UUID for Cloud v2. Legacy sessions also accept a numeric
asset ID. Run [`wendy cloud discover --json`](./discover.md) in the intended
context to obtain the selector. Numeric IDs from a v1 fleet do not identify
devices in v2.
When `--device` is omitted and multiple devices are online:
- In an **interactive terminal**, the cloud discover TUI opens in picker mode.
- In a **non-interactive environment** (pipe/CI), the command exits with an error that enumerates the candidates as `id=name` pairs (unnamed devices show as `(unnamed)`), so you can re-run with a working `--device`:
```
multiple cloud devices found; rerun with --device (42=playful-reed, 43=(unnamed))
```
## Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--cloud-grpc` | `""` | Cloud gRPC endpoint. Overrides all session selection. When multiple sessions are stored and no default is set, an interactive terminal shows a session picker; a non-interactive environment errors. |
| `--device` | `""` | Device name or v2 UUID; numeric asset IDs are legacy-only. With multiple online devices, an interactive terminal shows the picker and a non-interactive call requires a selector. |
| `--broker-url` | `WENDY_BROKER_URL`, otherwise the cloud endpoint's broker address | Legacy-only broker override. Cloud v2 selects its authorized relay. |
## Examples
### Cloud v2
Sign in, check the active context, and forward the Wendy Agent service:
```sh
wendy cloud login
wendy cloud status
wendy cloud tunnel 15052:50052 --device playful-reed
```
An authenticated Agent client can use this transport. It is not a browser URL
and does not bypass the service's authentication.
With the updated versions above, forward an application port or a UDP service:
```sh
wendy cloud tunnel 8080:3000 --device playful-reed
wendy cloud tunnel 9000:9000/udp --device playful-reed
```
### Legacy
Select an existing v1 session with `wendy cloud login --legacy` and check
`wendy cloud status`. If needed, choose its saved context with
`wendy auth use `.
Forward local TCP port 8080 to an existing HTTP service on device port 80:
```sh
wendy cloud tunnel 8080:80
```
Target a device by name:
```sh
wendy cloud tunnel 8080:80 --device playful-reed
```
Target an unnamed device by its numeric asset ID (from `wendy cloud discover --json`):
```sh
wendy cloud tunnel 8080:80 --device 43
```
Forward UDP port 9000:
```sh
wendy cloud tunnel 9000:9000/udp --device playful-reed
```
Check connectivity to the device's agent:
```sh
wendy cloud ping --device playful-reed --count 3
```
Cloud ping measures echo round trips through the broker to the agent. It requires no privileged ICMP sockets and does not test whether an application port is open.
## See also
- [`wendy cloud discover`](./discover.md): list available devices (supports `--all` to include offline devices)
- [Reach a UDP service through Wendy Cloud](../../../../../../guides/tutorials/python/cloud-udp-service/): deploy an echo server and test a reply through the tunnel
---
# wendy completion
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/completion/
---
title: "wendy completion"
description: "Generates or installs shell completion scripts for the wendy CLI."
---
# `wendy completion`
Generates or installs shell completion scripts for the `wendy` CLI.
> **Note:** `wendy completion` is not listed in `wendy --help` to keep the
> top-level surface focused, but it is fully functional. Run
> `wendy completion --help` directly to see its subcommands.
## Usage
```sh
wendy completion
wendy completion install [flags]
```
## Subcommands
| Subcommand | Description |
|---|---|
| `bash` | Print the bash completion script to stdout |
| `zsh` | Print the zsh completion script to stdout |
| `fish` | Print the fish completion script to stdout |
| `powershell` | Print the PowerShell completion script to stdout |
| `install` | Auto-detect the current shell and install the script to the conventional location |
## Printing a script
Each shell subcommand writes the completion script to stdout. You can source it directly or redirect it to a file:
```sh
wendy completion bash
wendy completion zsh
wendy completion fish
wendy completion powershell
```
## `wendy completion install`
Detects the running shell from `$SHELL` (Unix) or defaults to `powershell` (Windows) and writes the completion script to the conventional location for that shell, appending an idempotent sourcing block to the shell rc file when required.
Running `install` more than once is safe — the rc file is only modified on the first run. Subsequent runs detect the `# wendy-completion` sentinel and make no changes.
A successful `install` also records `completionInstalled: true` in `~/.wendy/config.json`, which permanently suppresses the [ambient install prompt](#automatic-prompt-to-install-completions). The dry-run flags `--print-path` and `--stdout` do **not** set this flag, since they don't actually install anything.
### Shell detection and install paths
| Shell | Script path | rc file modified |
|---|---|---|
| bash (bash-completion v2 detected) | `${XDG_DATA_HOME:-~/.local/share}/bash-completion/completions/wendy` | No |
| bash (fallback) | `~/.wendy/completions/wendy.bash` | `~/.bashrc` |
| zsh | `~/.zfunc/_wendy` | `${ZDOTDIR:-~}/.zshrc` |
| fish | `${XDG_CONFIG_HOME:-~/.config}/fish/completions/wendy.fish` | No (fish auto-loads) |
| powershell (Unix) | `~/.config/powershell/Completions/wendy.ps1` | `~/.config/powershell/Microsoft.PowerShell_profile.ps1` |
| powershell (Windows) | `~/Documents/PowerShell/Completions/wendy.ps1` | `~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1` |
bash-completion v2 is detected by probing standard locations (`${XDG_DATA_HOME}/bash-completion`, `/etc/bash_completion`, `/usr/local/etc/bash_completion`, `/opt/homebrew/etc/bash_completion`). When found the script is written to the XDG path and no rc edit is needed. When not found, a stand-alone script is written to `~/.wendy/completions/wendy.bash` and a sourcing line is appended to `~/.bashrc`.
### Flags
| Flag | Description |
|---|---|
| `--shell bash\|zsh\|fish\|powershell` | Override shell auto-detection |
| `--print-path` | Dry run — print the computed script and rc paths, then exit without writing anything |
| `--stdout` | Print the completion script to stdout without writing any files or modifying rc files |
`--stdout` and `--print-path` are mutually exclusive; combining them returns an error.
### Examples
Install completions for the detected shell:
```sh
wendy completion install
```
Install completions for a specific shell:
```sh
wendy completion install --shell zsh
```
Check where files would be written without writing them:
```sh
wendy completion install --shell fish --print-path
```
Print the completion script to stdout (useful for Homebrew formula staging):
```sh
wendy completion install --shell bash --stdout
```
The `--stdout` flag is intended for package managers such as Homebrew that call the binary directly and expect the completion script on stdout (e.g. via `generate_completions_from_executable`). It prints the script for the selected shell and exits without touching the filesystem or shell rc files.
After installation, restart your shell (or source the relevant rc file) for completions to take effect. Fish and bash-completion v2 load the script automatically on the next shell start without any rc change.
## Automatic prompt to install completions
When shell completions aren't installed yet, the CLI may offer to install them with an ambient prompt after a command finishes:
```
Shell completions for `wendy` aren't installed yet.
Install them now? [y/n]
```
The prompt has no default — you must answer `y` or `n`.
- **`y`** installs completions for the detected shell (same as `wendy completion install`).
- **`n`** permanently dismisses the prompt (sets `completionPromptDismissed`); it won't be shown again.
- **Ctrl+C / EOF** leaves the prompt unanswered. It is throttled and won't reappear until the throttle window (24 hours) elapses.
The prompt is intentionally unobtrusive and is **never** shown when:
- the session is non-interactive (no TTY) or output is machine-readable (`--json`),
- completions are already installed (`completionInstalled`) or the prompt was dismissed (`completionPromptDismissed`),
- the first-run analytics notice or a CLI-update prompt is shown in the same invocation (the prompt never stacks on top of those),
- the command runs its own completion flow — `wendy completion …` and [`wendy tour`](tour.md) — or is an internal helper.
> **See also:** [`wendy tour`](tour.md) includes a completions step that installs shell completions as part of first-time setup. Completing the tour, or installing completions there, suppresses this ambient prompt.
### Config fields
The prompt's state is persisted in `~/.wendy/config.json`:
| Field | Type | Meaning |
|---|---|---|
| `completionInstalled` | bool | Set once `wendy completion install` (or the prompt/tour install path) succeeds. Permanently suppresses the prompt. |
| `completionPromptDismissed` | bool | Set when you answer `n` to the prompt. Permanently suppresses the prompt. |
| `lastCompletionPromptCheck` | RFC3339 timestamp | When the prompt was last shown. Used to throttle it to at most once per 24-hour window. |
---
# wendy data
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/data/
---
title: "wendy data"
description: "For a first recording and campaign, start with the Wendy Data guide."
---
# `wendy data`
For a first recording and campaign, start with the [Wendy Data guide](../../../../../data/).
Wendy Data records synchronized, device-local Episodes and manages
flight-recorder campaigns. An Episode uses Linux `CLOCK_BOOTTIME` as its
canonical timeline, so wall-clock changes cannot reorder records. Its Wendy
manifest retains UTC correlation intervals, native clock mappings, source and
device identity, drop accounting, calibration revisions, lifecycle state, file
sizes, formats, and SHA-256 checksums.
### Clock status
The manifest's `system_clock_status` says what the Episode knows about the
device's own system clock, judged against the Roughtime consensus. It is one of
four values and never anything else.
| Value | Meaning |
|---|---|
| `system_reported` | No Roughtime consensus was available, so the manifest carries only the system's own reported uncertainty. |
| `agreement` | The system observation's UTC offset interval overlaps the Roughtime consensus interval. |
| `conflict` | The two intervals are disjoint: the system clock and the Roughtime consensus cannot both be right. |
| `roughtime_only` | The device has no synchronized system clock, so its observation is unbounded and there is no interval to compare. Roughtime is the Episode's only UTC evidence. This is not a disagreement; the system clock simply said nothing. |
Each Roughtime round's full evidence, including the nonce and the raw signed
responses, is appended to `roughtime.jsonl` in the Episode, which is sealed and
checksummed like every other file. The manifest keeps the bounds of the first
and most recent rounds in `roughtime_observations` and counts the rounds in
`roughtime_rounds`, so it stays a fixed size however long the Episode records.
## Episode commands
| Command | Description |
|---|---|
| `wendy data sources` | List camera, audio, ROS 2, application, and telemetry sources. |
| `wendy data record` | Start one detached Episode. With no source flags, every healthy discovered source is selected. |
| `wendy data stop` | Stop and seal the active Episode. |
| `wendy data episodes` | List finalized Episodes. |
| `wendy data inspect ` | Print the manifest and verify every sealed payload checksum. |
| `wendy data download ` | Resume a per-file download, verify it, and publish the local directory atomically. |
Use repeatable `--source` flags to narrow recording or `--exclude-source` to
remove a default source. `record` also accepts `--name`, repeatable
`--calibration =`, and
`--require-utc-uncertainty `. The uncertainty option refuses to start
when fresh UTC evidence does not meet the bound; it never affects local
monotonic ordering.
```sh
wendy data sources
wendy data record --name commissioning --source v4l2:/dev/video2
wendy data stop
wendy data episodes
wendy data inspect
wendy data download --output ./commissioning-episode
```
`wendy data stop` seals an Episode that captures application records only after
its post-seal drain has elapsed, so the command takes up to two seconds longer
than the capture itself. That wait is what lets an app file a prediction about
the samples the Episode just produced into that Episode. Campaigns set it with
`capture.drain`; an ad-hoc Episode started by `wendy data record` uses the two
second default whenever it includes the applications source, which it does
unless `--source` or `--exclude-source` leaves it out.
`sources` prints an aligned table whose `DETAIL` column carries the device's own
description of each source, truncated to keep the table readable. A single kind
can dominate a board: an NVIDIA Jetson exposes 20 internal audio-DMA routing
channels alongside its real microphone, so when more than six sources share a
kind the first six are listed and the remainder are counted in a note. Nothing
is hidden from `--json`.
```text
SOURCE KIND CLOCK STATUS DETAIL
applications application CLOCK_BOOTTIME healthy
audio:16777217 audio ALSA_CAPTURE/AGENT_RECEIPT healthy C920 [HD Pro Webcam C920], device 0: USB Audi...
audio:16777729 audio ALSA_CAPTURE/AGENT_RECEIPT healthy APE [NVIDIA Jetson Orin Nano APE], device 0: ...
audio:16777730 audio ALSA_CAPTURE/AGENT_RECEIPT healthy APE [NVIDIA Jetson Orin Nano APE], device 1: ...
audio:16777731 audio ALSA_CAPTURE/AGENT_RECEIPT healthy APE [NVIDIA Jetson Orin Nano APE], device 2: ...
audio:16777732 audio ALSA_CAPTURE/AGENT_RECEIPT healthy APE [NVIDIA Jetson Orin Nano APE], device 3: ...
audio:16777733 audio ALSA_CAPTURE/AGENT_RECEIPT healthy APE [NVIDIA Jetson Orin Nano APE], device 4: ...
telemetry telemetry CLOCK_BOOTTIME healthy
v4l2:/dev/video0 camera V4L2_BUFFER_TIMESTAMP healthy HD Pro Webcam C920: HD Pro Webc VIDEO_TRANSPO...
... 15 more audio sources not listed (--kind audio to list all, --json for everything)
```
Narrow the table with `--kind`, which is repeatable or comma-separated and
matches case-insensitively. A kind named there is never summarised, because
asking for a kind is asking to see all of it. Without `--kind`, `--json` stays
the full unfiltered response the device sent, so existing scripts keep working;
with `--kind` it carries the filtered set.
```sh
wendy data sources --kind camera
wendy data sources --kind camera,telemetry
wendy data sources --kind audio # every audio source, nothing summarised
```
### The `--json` dialect
Every `data` subcommand whose payload is a protocol buffer message prints it as
canonical protobuf JSON on one compact line, with protocol buffer field names
exactly as the `.proto` declares them, and unpopulated fields omitted. That
covers `sources`, `episodes`, `record`, `stop`, `campaign list` and
`campaign trigger`.
Some payloads are already a JSON document the device produced, and those bytes
are written through untouched so they stay checkable against the device:
`inspect` emits the episode manifest, and `campaign deploy` and
`campaign inspect` emit the canonical campaign plan. `campaign inspect` emits
the plan whether or not `--json` is given, because the plan is the whole output
of the command.
`download` reports what it wrote to the local filesystem, which no wire message
describes, so under `--json` it prints an object of its own: `episode`,
`destination`, `files` (paths relative to the destination, including the
`manifest.json` the command writes itself) and `bytes`.
Episode IDs are stable opaque identifiers. Their readable UTC prefix is only a
convenience; canonical ordering comes from the Episode's `CLOCK_BOOTTIME`
timestamps and boot ID.
## Campaign commands
| Command | Description |
|---|---|
| `wendy data campaign deploy ` | Strictly validate, persist, and arm a campaign on the connected device. |
| `wendy data campaign list` | List deployed campaigns and revisions. |
| `wendy data campaign inspect ` | Print the canonical deployed plan and state. |
| `wendy data campaign trigger ` | Trigger a campaign manually; `--reason` is stored in the Episode. |
Event and model-uncertainty triggers are armed immediately after deployment.
A complete plan is in `Examples/WendyDataCampaign` in the WendyOS repository.
### Deployment and Cloud registration
`wendy data campaign deploy [file]` validates the plan before contacting the
device. With no filename in an interactive terminal, it offers `.yaml` and
`.yml` files in the current directory. Enter selects a file; Escape or Ctrl-C
cancels. Non-interactive and `--json` use requires an explicit filename.
An enrolled device's campaign is registered in Cloud Apps as `campaign:`
using a matching operator session. Registration failures stop deployment.
`--skip-cloud-registration` permits an offline deployment; unenrolled devices
skip registration automatically. Existing app metadata and notification grants
are preserved. An owner or admin must enable notifications separately.
## Campaign YAML reference
A campaign file contains exactly one YAML document. Unknown fields are
rejected, with one exception: unknown keys inside `notify` warn at deployment
and are ignored when `notify.on` is `episode_committed`. The immediate modes
(`event` and `detection`) reject unknown `notify` keys.
| Top-level field | Required | Description |
|---|---|---|
| `version` | yes | Campaign schema version the author wrote the file against. This release supports version `1`; higher versions are a deploy-time error. |
| `name` | yes | Unique device-local name using letters, numbers, `.`, `-`, or `_`; maximum 128 characters. |
| `fleet` | no | Fleet selector retained with the plan. A direct device deployment applies to the connected device. |
| `sources` | yes | One or more source entries. |
| `capture` | yes | Buffer, post-trigger duration, and triggers. |
| `upload` | yes | Upload condition, optional logical destination, and optional rate cap. |
| `retention` | no | Optional on-device storage bounds. |
| `export` | yes | Annotation integration lifecycle intent. |
| `models` | no | Map of model name to deployed version, copied into Episodes. |
| `privacy` | no | List of declared transforms with optional revisions. |
| `inference` | no | Agent-managed object detection from pinned Hugging Face models, using Transformers or YOLO ONNX; see below. |
| `notify` | no | Episode-commit notification intent or immediate event/detection delivery; see below. |
Each `sources` item selects exactly one source:
| Source | Description |
|---|---|
| `camera: ` | A stable source ID, `/dev/videoN` path, or unambiguous name fragment. `front` and `default` select the only healthy camera when exactly one exists. `camera: "*"` selects every healthy camera, including cameras discovered after deployment. Quote the wildcard in YAML. |
| `audio: ` | A stable audio source ID or unambiguous name fragment from `wendy data sources --kind audio`. The selector must resolve to a healthy capture source at deployment. |
| `ros2: ` | A ROS 2 topic name such as `/lidar/points`. Selects that topic on every healthy ROS 2 graph publishing it. A full per-topic source ID such as `ros2:rmw_cyclonedds_cpp:domain-42:/lidar/points` selects that one source. A domain-level source ID such as `ros2:rmw_cyclonedds_cpp:domain-42` selects all topics in that domain. Missing topics warn at deployment; a triggered episode records available sources and reports missing sources in its manifest. Requested topics remain in the manifest. |
| `telemetry: true` | Records uptime, system load, and total/free RAM once per second on Linux. Other sensor measurements need ROS 2 or application streams. |
An optional `calibration_revision` can accompany a source. Application records
are included automatically because they carry campaign trigger evidence and
pre-roll.
### Per-source capture policy
Each source may carry an optional `capture` block declaring how it records.
Fields belonging to a different mode than the declared one are rejected.
| Field | Mode | Description |
|---|---|---|
| `mode` | | One of `continuous` (default), `snapshot`, `fragment`, or `threshold`. |
| `rate` | `continuous` | Optional capture frequency cap in hertz. |
| `interval` | `snapshot` | Required snapshot period, for example `2s`. |
| `pre`, `post` | `fragment` | Durations bounding a fragment around an occurrence; at least one is required. |
| `trigger` | `threshold` | Required expression ``, for example `model.uncertainty > 0.9` or `level_db > -20`. Only `model.uncertainty` is bounded to 0 through 1; other fields carry their own units. |
| `fragment` | `threshold` | Optional captured duration per threshold crossing. |
| `max_resolution` | any | Camera sources only: `WxH` cap such as `1280x720`. |
What each adapter honors today: cameras implement `continuous` and `snapshot`,
audio implements `continuous` and `threshold`, and the ROS 2 and telemetry
paths implement `continuous` only. Every other combination validates and
deploys, and deployment prints a warning naming each source whose requested
mode is not implemented for its kind; those sources record continuously for
now.
Camera snapshots and the continuous capture-rate cap require a stream that
delivers whole encoded frames, currently local V4L2 cameras with native H.264.
IP cameras and GStreamer-encoded streams record continuously at their stream
rate instead; the episode's source detail reports the unapplied policy.
Snapshot cameras do not maintain a video pre-buffer. Audio threshold capture
uses `level_db`, for example `trigger: "level_db > -20"`, to retain WAV
fragments during an active episode. It does not itself start a campaign.
`capture.buffer` must be a Go-style duration from `0s` through `5m`. On a continuous
camera source, a buffer and explicit stream parameters are mutually exclusive in this
release: a buffered camera is armed into a standby subscription that asserts no
stream parameters, so it never takes a running camera away from a viewer and
never changes the stream parameters part way through a clip.
There is exactly one exception to that promise, and it belongs to the other
half of the same rule. A capture that *does* name explicit stream parameters
(`max_resolution` or `rate`, with no buffer) restarts the camera producer at
those parameters when the only consumers holding it asked for nothing in
particular. Those parameter-less consumers, a plain `wendy device camera view`
among them, have their stream ended rather than spliced onto the new stream:
the replacement producer emits a new sequence parameter set, and a decoder
handed both in one timeline produces garbage. The agent marks the ended stream
with the machine-readable reason `CAMERA_PRODUCER_RESTARTED`, and
`wendy device camera view` rejoins the replacement stream automatically,
printing one line when it does. A consumer that named its own stream
parameters is never taken over; a campaign that conflicts with it is refused
and says so in the episode manifest. A source that sets
both a buffer and an explicit `max_resolution` or `rate` therefore records both
its pre-roll and its live tail at whatever parameters the producer is already
running, and the explicit values are reported as requested but not achieved in
the episode manifest. Deployment prints a warning naming each source in that
position. Set one or the other: a buffer for pre-roll, or explicit parameters
for a clip that must be captured at a specific resolution or rate.
`capture.after_trigger` must be greater than `0s` and no more than `24h`.
`capture.drain` is the opposite end of the same idea: `buffer` decides how much
happened *before* the trigger that the Episode keeps, and `drain` decides how
long the Episode stays open for application records *after* capture stops. An
app that scores asynchronously reads samples, spends time computing, and writes
its prediction once the Episode has already finished recording. Without a drain
that write arrives too late, is acknowledged `buffered`, and is then replayed
into whichever Episode records next, where it appears as that Episode's
pre-trigger context. The drain keeps the Episode open long enough for the
verdict to be filed against the recording it was computed from.
It must be a duration from `0s` through `30s`. Omitting it takes the default of
`2s`, so campaigns get the behavior without being rewritten; `drain: 0s` opts
out and seals the moment capture stops. Every campaign Episode pays it: a
campaign plan always selects the applications source, whatever `sources`
declares, because that source is what arms the plan's triggers. A telemetry-only
or camera-only campaign therefore waits exactly as long as any other one. Only
an ad-hoc `wendy data record` Episode can avoid the wait, and only by leaving
the applications source out with `--source` or `--exclude-source`, as described
above. The wait is added
to the Episode's own lifetime, after `after_trigger` has expired, and it is
included in the `stopped_episode_nanos` the manifest records. Nothing is
captured during it, so the manifest also records
`capture_stopped_episode_nanos`, the point at which the capture adapters
actually stopped. Measure how long an Episode recorded from that field;
`stopped_episode_nanos` is the point past which no record was accepted, which
is what an event timeline needs. The difference between them is the drain.
A draining Episode has already stopped capturing, so it no longer holds its
campaign. A trigger that matches during the drain starts the campaign's next
Episode rather than being dropped. It does have to wait for the previous
Episode to finish draining before it can begin, because starting and stopping
capture are serialized on the device, so with the default the next Episode
begins up to two seconds after the trigger that asked for it. Set `drain: 0s`
on a campaign whose events arrive in bursts and whose applications write their
records live rather than asynchronously.
A failed capture adapter start is the one path that does not drain. That
Episode never delivered a sample to any application, so no record about it can
be outstanding; it is sealed as `interrupted` with reason
`capture_adapter_start_failed` and `wendy data record` returns immediately
instead of after the drain.
At least one trigger is required, and each trigger selects exactly one
condition:
| Trigger | Description |
|---|---|
| `event: ` | Matches an entitled application's event name. |
| `model.uncertainty: "> 0.65"` | Compares prediction uncertainty with `<`, `<=`, `==`, `>=`, or `>` and a value in `[0, 1]`. |
Prediction uncertainty is read from the structured `uncertainty` attribute
when present, otherwise from the prediction value.
The `upload` and `retention` blocks:
| Field | Required | Description |
|---|---|---|
| `upload.when` | yes | One of `always`, `wifi`, or `manual`. |
| `upload.destination` | no | Logical dataset name the fleet backend maps to storage. Not a URL; devices never receive bucket layouts or credentials through campaign plans. |
| `upload.max_rate` | no | Upload bandwidth cap in bytes per second; plain integers and rates such as `5MB/s` are accepted. |
| `retention.local_quota` | no | Declared on-device episode storage bound in bytes; plain integers and sizes such as `10GiB` are accepted. Stored with the plan; this release enforces only the device-wide quota and deployment prints a warning. |
### Inference
An `inference` block runs object detection in the agent without a separate model
application. The agent installs its managed Python runtime and downloads the
pinned Hugging Face checkpoint on first use; this requires network access and
disk space outside the episode quota. Remote model code is disabled.
| Field | Required | Description |
|---|---|---|
| `backend` | no | `transformers` by default, or `yolo_onnx` for a compatible YOLO ONNX detection export. |
| `model` | yes | `owner/repository` or `https://huggingface.co/owner/repository`; the repository containing the checkpoint or ONNX export. |
| `model_file` | for `yolo_onnx` | Relative `.onnx` file path inside the pinned repository. Not accepted with `transformers`. |
| `revision` | yes | Lowercase, 40-character Hugging Face commit SHA. Branch names are rejected. |
| `labels` | yes | 1–32 unique, nonempty model labels, each at most 128 bytes. |
| `threshold` | yes | Detection confidence in `(0, 1]`. |
| `rate` | yes | Frames per second per camera in `(0, 30]`. |
| `event` | yes | Event emitted on detection; 1–128 letters, numbers, `.`, `-`, or `_`. |
| `clear_after` | yes | Continuous absence needed to clear presence; positive duration, at most 24h. |
| `cooldown` | yes | Minimum interval between detection events; positive duration, at most 24h. |
| `enabled` | no | Defaults to `true`; `false` keeps the configuration but stops inference. |
Inference requires a camera source and a trigger matching its event in
`capture.triggers`. Predictions and events are attributed to
`sh.wendy.campaign.`. Campaign inspect reports
`inference_status`, including runtime state, per-camera state and notification
errors. Temporary camera loss retains presence/cooldown state so reconnecting
does not produce a duplicate detection.
See [the all-camera people example](https://github.com/wendylabsinc/WendyOS/tree/main/Examples/WendyDataPeople)
for a complete campaign. Campaign revision hashing now uses revision schema 2,
which includes inference and notification settings. Existing plans receive new
revision hashes when redeployed; the YAML `version` remains `1`.
### Notifications
| Field | Required | Description |
|---|---|---|
| `notify.on` | yes | `episode_committed`, `detection`, or `event`. |
| `notify.event` | for `event` | Exact named application or inference event to match. |
| `notify.webhook` | no | For `event`/`detection`, an HTTP(S) endpoint without embedded credentials or a URL fragment. Omit it to deliver through Wendy Cloud. |
`episode_committed` is cloud-side intent carried in the committed manifest;
it does not open a device-side notification connection. `detection` requires
an inference block and sends an immediate notification when presence is detected.
`event` also works for matching application events, independently of episode
capture triggers. Immediate notifications use a bounded queue and retries;
delivery failures appear in `inference_status.notification_error` and agent logs.
For Cloud delivery, enroll the device and enable the campaign's notification
grant in Cloud Apps. Deployment registers `campaign:` but never grants
permission to send notifications. Permanent authentication, authorization,
validation and protocol errors stop retries; transient failures get at most
three attempts, each with a 10-second timeout and the same event UUID.
A `detection` notification carries `detections`: up to five `{label, score}`
pairs that passed the label and threshold filter, highest score first; `count`
stays the total. They appear in the device's notification journal and in
webhook bodies; Wendy Cloud notifications do not carry them yet. A
`notify.on: event` rule naming the inference's own event carries them too. It
never carries boxes or images. Readers that predate the field ignore it.
Unknown keys inside `notify` are rejected for `event` and `detection`.
For `episode_committed`, they warn at deployment and are ignored.
```yaml
version: 1
name: forklift-failures
fleet: warehouse-west
sources:
- camera: front
capture:
mode: snapshot
interval: 2s
max_resolution: 1280x720
- ros2: /lidar/points
- ros2: /vehicle/odometry
capture:
buffer: 10s
drain: 2s
after_trigger: 20s
triggers:
- event: emergency_stop
- model.uncertainty: "> 0.65"
upload:
when: wifi
destination: forklift-episodes
max_rate: 5MB/s
retention:
local_quota: 10GiB
export:
annotation: cvat
notify:
on: episode_committed
```
## Playing back camera capture
Camera capture is kept as a raw H.264 Annex-B elementary stream
(`cameras//segment-NNNNNN.h264`). An elementary stream has no
container, so it carries no timing at all, and a player handed one has nothing
to build a presentation schedule from. Players invent a frame rate instead, so
the clip runs at the wrong speed and reports the wrong duration. Passing a
fixed rate with `ffmpeg -r` does not fix this: the capture rate is genuinely
variable, falling as the device gets busy with inference, so no single number,
including the measured average, describes more than a fraction of the clip.
The timing is already recorded. `cameras//index.jsonl` carries one line
per kept frame with `canonical_episode_nanos`, the frame's position on the
Episode's canonical `CLOCK_BOOTTIME` timeline, alongside the segment file, byte
offset and `byte_size` of its payload. Remuxing those payload bytes into an MP4
gives every frame the presentation time the index recorded for it, so the
variable frame rate is represented honestly rather than averaged away.
### Sealed Episodes already carry the MP4
The agent runs that remux itself while sealing an Episode: each camera source
gains a `cameras//playable.mp4` beside its raw capture, listed in the
manifest with a size and SHA-256 like every other file and marked with
`"role": "derived"`. Because it is listed, the transfer worker uploads it and
commit-time verification covers it, so the bucket object plays directly in a
browser (or after a plain download) with no conversion step. The derived file
is never capture payload: model-input and payload-retention accounting
resolve payloads through `index.jsonl` into the raw segments, and the raw
files are kept byte-for-byte as before. The remux is a copy, not a transcode,
so expect the clip to add roughly the size of the raw stream to the Episode;
that overhead counts against the Episode's size for the local quota.
Sealing never fails or waits on the remux. A source whose stream cannot become
an honestly timed, seekable clip (B slices, slice headers the muxer cannot
parse, parameter sets that change mid-stream after a producer restart, no
random-access frame) seals without its `playable.mp4`, and the
manifest's `playable_notes` names the reason; a clip that had to omit frames
whose bytes were missing gets a note too.
### Converting older Episodes by hand
The `episode-playable` command performs the same remux laptop-side, for
Episodes sealed before the agent did it (or for a source whose seal-time mux
was refused, where it will report the same warnings). When every camera source
already carries a seal-time `playable.mp4` the command says so and converts
nothing:
```sh
# From the repository root.
CGO_ENABLED=0 go build -o bin/episode-playable ./go/cmd/episode-playable
wendy data download -o ./episode --device
./bin/episode-playable -o ./playable ./episode
```
`wendy data download` takes either an absolute or a relative `-o` path. A
trailing separator is fine too: the destination is cleaned before the staging
directory beside it is named.
One `.mp4` is written per camera source into the output directory,
which must be somewhere other than the Episode. The command prints the index
line count, the frames written, the segments read, the index's first-to-last
span, and the minimum, mean and maximum inter-frame interval, which is enough
to check the conversion numerically without watching it.
Details worth knowing:
- The Episode is only ever read. The elementary stream and its index are
checksummed archival truth, and `index.jsonl` addresses frames by byte offset
within the stream, so rewriting it in place would break the join between a
frame and the model input recorded against it.
- It is a remux, not a transcode. The coded pictures are copied verbatim, so
the output is the same video, only re-framed.
- MP4 was chosen over Matroska because the output has to play unmodified in
both VLC and QuickTime, and QuickTime cannot open Matroska. MP4 states an
explicit duration for every sample in its `stts` box, which represents a
variable frame rate exactly as precisely as Matroska timestamps do.
- There is no external dependency. The muxer is pure Go, so no ffmpeg,
mkvmerge or PyAV is needed anywhere on the path from an Episode to a playable
file.
- A frame the index names but whose bytes are missing or truncated is reported
and skipped rather than written short, because a truncated access unit makes
the file undecodable from that point on. A partial trailing index line, the
shape an interrupted Episode leaves behind, is counted as unusable and
ignored. Multiple segments per source and multiple sources per Episode are
both handled.
- `byte_size` from the index is authoritative for how many bytes belong to a
frame. The model-input ledger's `payload_bytes` can exceed it for a frame
that opens a segment, because the segment begins at the parameter-set prefix
inside that payload rather than at its first byte.
- The index records frames in capture order, which is coded order. That is
exact for the B-frame-free streams the device's encoder produces. A stream
containing B slices has a presentation order that differs from its coded
order, which the index does not carry, so such a stream is flagged with a
warning rather than silently mistimed.
`wendy data export` is the natural future home for this, at which point the
command becomes a verb on the CLI rather than a separate binary.
## Pre-trigger behavior
Application pre-roll is exact, device-wide, and bounded by the campaign buffer
(with a five-minute/50 MiB global ceiling). Continuous camera sources also keep
pre-roll: a campaign that requests a buffer arms them into a standby ring of
keyframe-aligned encoded frames, so the episode can include footage before the
trigger. The camera source's manifest entry reports the achieved offset, which
may be shorter than requested when the campaign was recently armed or the
ring's byte cap was reached. Snapshot cameras do not maintain this video ring.
Audio, ROS 2, and telemetry start at the trigger. Every source's manifest entry
preserves both the requested offset and the achieved offset; campaign deployment prints a warning
when audio or ROS 2 pre-roll was requested. Unknown drop counts are displayed
as unknown, never as zero.
A record the ring replays into an Episode is written with
`preroll_flushed: true` in `events.jsonl`. Records the Episode observed live
omit the key entirely, so the two are distinguishable offline without changing
any existing line. Window membership is decided on both the agent's own receipt time and the
record's presentation timestamp: a record reaches an Episode's pre-roll only if
it physically arrived inside that window, so a client cannot place itself into
a later Episode by choosing its own `client_boottime_nanos`.
## Concurrency and retention
Each campaign records at most one active Episode at a time; a second trigger
for the same campaign while its Episode is active is dropped. Different
campaigns capture concurrently, and one ad-hoc `wendy data record` Episode can
run beside them. `wendy data stop` finalizes the ad-hoc Episode when one is
running, otherwise the single active campaign Episode.
When the device storage quota is exceeded, the oldest Episodes that are
already uploaded or were recorded without an upload policy are evicted first.
Episodes still awaiting upload are evicted only as a last resort, with a
warning in the agent log naming the Episode and campaign.
The device-wide quota is the smaller of a fifth of the episode store's
filesystem and `WENDY_DATA_MAX_BYTES` (default 50 GiB), and eviction preserves
`WENDY_DATA_RESERVE_BYTES` of free space on it (default 5 GiB). Both are read
by the agent from its environment file.
## Current layer boundaries
- Upload policy: `upload.when` and `upload.max_rate` are applied by the agent's
episode transfer worker, which uploads sealed episodes to the cloud ingest
service. `wifi` is currently treated as `always` because no device
network-type signal is plumbed to the worker yet. `upload.destination` is a
logical dataset name stored with the plan; the device never receives bucket
layouts or credentials.
- Per-source capture modes other than `continuous` are validated and stored,
with camera `snapshot` and audio `threshold` implemented. Other combinations
record continuously and deployment warns per source. Camera snapshot and
rate policies also depend on the transport, as described above.
- `retention.local_quota` is validated and stored, but eviction currently
applies only the device-wide quota; deployment warns when it is set.
- `export.annotation` is stored as labeling lifecycle state, but this release
does not create CVAT tasks.
- `privacy` declares transform metadata; it does not apply redaction to captured
payloads.
- Fleet catalog search, replay, evaluation, and model redeployment are later
Wendy Data layers and are not performed by these commands yet.
Episodes remain locally inspectable and downloadable regardless of network or
UTC confidence.
## Application recording streams
The `episode-write` entitlement can declare named streams with `mode` set to
`lightweight` or `durable`, plus a `mediaType` and an optional schema. Lightweight
streams accept native bytes over Unix `SOCK_SEQPACKET` sockets without an SDK.
Durable streams accept Protobuf envelopes and acknowledge after local storage sync,
including when no episode is active. The agent injects `WENDY_DATA_STREAM_DIR`;
each stream is available at `/.sock`.
Supported payloads include text, CSV, JSON, CBOR, Protobuf, Arrow IPC, JPEG, PNG
and custom binary layouts. Time-series streams declare their channels and clock;
sample timestamps remain separate from agent receipt time. See the repository's
`Examples/WendyRecording` for configuration, senders and an export reader.
```sh
wendy data export-stream --service -o records.wdr
```
Omit `--service` for a single-service app with no explicit service name. Export
writes a snapshot of retained durable records and refuses to overwrite an existing
file. Plain export does not delete records, change cloud upload state or include
volatile lightweight records. `--reclaim` exports the oldest chunk, syncs its local
file and directory, then acknowledges that checkpoint for device reclamation.
Chunks contain whole segments up to 16 MiB plus one segment, or 64 segments.
For continuous capture, use an output directory under an existing parent:
```sh
wendy data export-stream --follow --reclaim --interval 1s -o capture
```
This writes separate files, retries temporary RPC failures and reclaims only saved
chunks. Local storage failures stop export. A lost acknowledgement or overlapping
exports can produce duplicates; use record IDs and stream identity to deduplicate.
Existing destination files are preserved. The receiver must keep up and manage its
own storage; the CLI never deletes local capture files.
Durable stream configuration accepts `storage.maxBytes` from 1 MiB to 1 TiB and
`storage.retentionSeconds` from 0 to 31536000. Defaults remain 64 MiB and 24 hours.
Zero retention disables age expiry, retaining records until export acknowledgement.
Set a larger spool, such as 1 GiB, for continuous capture. A full journal rejects
new data without overwriting retained records; producers must buffer and retry.
Deduplication applies only while records are retained, with at most one million
retained IDs per stream. Age retention across reboots depends on device clock accuracy.
This export path does not update cloud upload state.
Payloads and metadata are exported in checksummed Protobuf `.wdr` frames. Episodes
also include these frames in `records.wdr`, alongside legacy `events.jsonl`.
---
# Install
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/apps/install/
---
title: "Install"
description: "Installs an app from the Wendy AppStore onto the target device."
---
Installs an app from the [Wendy AppStore](https://appstore.wendy.dev) onto the target device.
This is the canonical spelling. (There is no top-level `wendy app install`; `wendy install` is the OS-image installer.)
## Flags
| Flag | Description |
|------|-------------|
| `--api` | Override the Wendy AppStore resolution API base URL (default: `$WENDY_APPSTORE_API` or the built-in default). |
| `--no-start` | Create the container but do not start it. |
## Examples
```sh
wendy device apps install jellyfin
wendy device apps install jellyfin --no-start
```
---
# List
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/apps/list/
---
title: "List"
description: "Lists all uploaded apps for a given device."
---
Lists all uploaded apps for a given device.
Shows the app's status (running), the app ID and app version.
If available, also shows the RAM usage and disk space.
You can also, through this menu, interactively start, stop and remove apps - or view an app's logs.
---
# Remove
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/apps/remove/
---
title: "Remove"
description: "Removes an app from the device. With an [app-name] argument, removes that app; otherwise lists all apps and allows interactive removal."
---
Removes an app from the device. With an `[app-name]` argument, removes that app;
otherwise lists all apps and allows interactive removal.
```sh
wendy device apps remove [app-name] [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--cleanup` | Also remove the container image, reclaiming disk space. |
| `--delete-volumes` | Also delete the app's persistent volumes. |
| `--force` | Skip confirmation prompts. Required without an interactive terminal. |
When neither `--cleanup` nor `--delete-volumes` is given and `--force` is
absent, the command shows an interactive "Also clean up?" checklist offering to
delete the container image and the persistent volumes, so the cleanup choices
are still reachable without passing flags.
Without an interactive terminal there is nobody to answer the confirmation, so `remove` refuses to run unless `--force` is given, exiting with status 2 (a usage error).
## JSON output
With `--json` (automatic when stdout is not a terminal) the command prints one JSON object to stdout. `deleteImage` and `deleteVolumes` appear when those clean-ups were requested; `status` is `cancelled` if the removal was declined at the prompt.
```json
{"app": "my-app", "action": "remove", "status": "removed", "deleteImage": true}
```
---
# Start
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/apps/start/
---
title: "Start"
description: "Starts an app by name. If the app-name argument is not provided, and the terminal is interactive - a list of all uploaded apps is shown. You can then interactively start an app."
---
Starts an app by name. If the app-name argument is not provided, and the terminal is interactive - a list of all uploaded apps is shown. You can then interactively start an app.
By default, after starting the app the CLI attaches to the container's output stream and prints logs to the terminal. Press **Ctrl-C** to detach: once the agent has confirmed the start, detaching is a success (exit status 0). Stopping the CLI any other way, such as with `SIGTERM`, is reported as a failure.
An attached start (the default) keeps the restart policy the app was deployed with. A detached start (`--detach`) sets the app's restart policy to **`unless-stopped`** and saves it on the device, replacing the deployed policy: the agent restarts the app whenever it exits, and after a reboot, until it is explicitly stopped with `wendy device apps stop`.
If the container keeps exiting and the agent has already performed at least one automatic restart, `wendy device apps list` shows the app as **crash-looping** (a red `↻` icon) rather than stopped, so a restart loop is not mistaken for a clean exit. Use `wendy device logs --app ` to view the crash output.
## Flags
| Flag | Description |
|------|-------------|
| `-d`, `--detach` | Start the app and return once the agent confirms it has started, without streaming output. Also sets the app's restart policy to `unless-stopped` (see above). |
## Examples
Start an app and stream its output:
```sh
wendy device apps start my-app
```
Start an app in the background (detached):
```sh
wendy device apps start --detach my-app
wendy device apps start -d my-app
```
When `--detach` is used, the CLI waits for the agent to confirm the container has started, then returns without streaming any output. If the agent reports an error or closes the stream before confirming the start, the command exits with a non-zero status instead of reporting success.
## Reported outcome
When an attached start returns, the CLI reports the app's state at that point: `started` if it is still running (for example a multi-service app), `stopped` — or a short reason such as `crashed (exit 1)` — if it has exited, or `crash-looping` if it keeps restarting.
## JSON output
With `--json` (automatic when stdout is not a terminal) the command prints one JSON object to stdout. An attached start sends the app's own output to stderr, so stdout holds only the result.
```json
{"app": "my-app", "action": "start", "status": "started", "restartPolicy": "unless-stopped"}
```
`status` is `started` for a detached start. For an attached start it is the app's state when its output stream ended (`running`, `stopped` or `crash_looping`), with `exitCode` and `terminationReason` when the agent recorded why the app last exited. If you detach from an attached start with Ctrl-C, the result has `"detached": true` and `status` is the app's state at that moment.
---
# Stop
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/apps/stop/
---
title: "Stop"
description: "Stops an app by name. If the app-name argument is not provided, and the terminal is interactive - a list of all uploaded apps is shown. You can then interactively stop an app."
---
Stops an app by name. If the app-name argument is not provided, and the terminal is interactive - a list of all uploaded apps is shown. You can then interactively stop an app.
## JSON output
With `--json` (automatic when stdout is not a terminal) the command prints one JSON object to stdout:
```json
{"app": "my-app", "action": "stop", "status": "stopped"}
```
---
# wendy device bluetooth
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/bluetooth/
---
title: "wendy device bluetooth"
description: "Manages Bluetooth peripherals on the connected WendyOS device. Aliased as wendy device bt."
---
# `wendy device bluetooth`
Manages Bluetooth peripherals on the connected WendyOS device. Aliased as `wendy device bt`.
Run without a subcommand to open an interactive table for browsing and managing peripherals; the subcommands below remain available for scripting.
## Interactive TUI
```sh
wendy device bluetooth
```
Results appear quickly — an initial batch of nearby peripherals is delivered after
about 1 s, and a more complete set arrives once the full ~8 s scan window finishes.
Discovered peripherals are deduplicated by address and sorted: connected devices
first, then paired devices, then named devices (alphabetically by name), then
anonymous peripherals (by descending RSSI — strongest signal first). Ties within
each group fall back to address ascending.
Unnamed peripherals are hidden by default. Press `h` to show them or hide them
again. The table shows how many peripherals are hidden. This also applies to the
Bluetooth tab in `wendy device pair`.
| Key | Action |
|-----|--------|
| `↑`/`↓` | Move the selection |
| `←`/`→` | Scroll the table horizontally (when it overflows) |
| `enter` | Connect to the selected peripheral (pairs and trusts it) |
| `d` | Disconnect the selected (connected) peripheral |
| `f` | Forget the selected (paired) peripheral |
| `h` | Show or hide unnamed peripherals |
| `r` | Rescan |
| `q` / `esc` | Quit |
Actions update the table immediately (optimistically) on success. Because a Bluetooth rescan takes several seconds, the table is not rescanned automatically after each action — press `r` to refresh against the device.
## Subcommands
### `wendy device bluetooth list`
Scans for peripherals and prints them as a table, or as JSON with `--json`.
The scan runs in two passes over roughly 8 s total (an early ~1 s partial pass,
then a full pass); `list` waits for the stream to finish before printing, so
expect it to take up to ~8 s to return.
```sh
wendy device bluetooth list [--json]
```
For a connected Bluetooth LE peripheral, the **Link timeout** column shows the link's supervision timeout: how long a silent link lasts before it is dropped. The agent holds BLE HID devices such as gamepads at 0.5 s; `(device)` marks a device whose own, longer value is in force. See [Bluetooth](../../../../../../hardware/bluetooth/#game-controllers-and-other-hid-devices). With `--json` the values are `supervision_timeout_ms` and `requested_supervision_timeout_ms`.
### `wendy device bluetooth connect`
Connects to a peripheral by address.
```sh
wendy device bluetooth connect [--pair] [--trust]
```
### `wendy device bluetooth disconnect`
Disconnects a peripheral by address.
```sh
wendy device bluetooth disconnect
```
### `wendy device bluetooth forget`
Removes the pairing for a peripheral by address.
```sh
wendy device bluetooth forget
```
---
## Flags
| Flag | Description |
|------|-------------|
| `--pair` | Pair with the device when connecting (default `true`, `connect` only). |
| `--trust` | Trust the device when connecting (default `true`, `connect` only). |
| `--json` | Output the scan result as JSON (`list` only). |
---
# Build Host
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/build-host/
---
title: "Build Host"
description: "Controls whether a device will run image builds submitted by other people with wendy run --build-host."
---
Controls whether a device will run image builds submitted by other people with
[`wendy run --build-host`](../run.md#remote-build-host).
For a complete example that sends one build to several devices, follow [Build Once, Deploy to Several Devices](../../../../../../guides/fleet-deployment/).
```sh
wendy device build-host enable --device spark-office
wendy device build-host status --device spark-office
wendy device build-host disable --device spark-office
```
## The role is off by default
A remote build executes the caller's build instructions on the host. Enabling
the builder role allows users in your organisation to run builds there. Use a
host intended for shared builds.
The role requires an explicit `enable` command. Installing the agent,
provisioning the device, or installing BuildKit does not enable it.
Enabling takes effect on the next build. There is no agent restart.
## Who may change it
`enable`, `disable`, and build submission require a **user** certificate.
Device certificates are rejected, including on hosts with the builder role
enabled. The unauthenticated port the agent serves before provisioning does not
accept builds.
The one caller without a certificate is an on-device container holding the
**admin entitlement**, which reaches the agent over a local unix socket. There
the socket's own permissions are the credential, and that entitlement already
carries full agent authority.
Cross-organisation callers are rejected before any of this is reached, by the
agent's mTLS organisation check.
## Source contexts and concurrent builds
The host stores source files under `/var/lib/wendy/buildctx/` between
builds so BuildKit can reuse its local-source cache. `` is the app ID for a
single-image app, and `-` for each service of a
[multi-service app](../../../../apps/wendy-services.md#remote-build-host). The
files are cleared and rewritten at the start of each build with that id, but
are not deleted after the build finishes.
Builds with the same id are serialised on a host. Builds with different ids can
run concurrently, so the services of one app build side by side and each keeps
its own cache between runs. This prevents one build from replacing source files
that another build with the same id is still compiling.
## Delivery
The finished image reaches the target by chunks: the build host asks the device
which layers and chunks it already holds and sends only the missing bytes into
its content store. A link that drops mid-transfer resumes from the chunks the
device already staged rather than restarting. This leg is scoped to one build —
the host dials the target's agent with its own certificate, pinned to that one
device, so a build for one target cannot deliver to another.
A device whose agent predates chunked delivery receives a registry push instead,
as does a build run with `--chunking=off`. The build log says which route it took.
## Delivery credentials
On the registry-push route, the agent exposes a loopback endpoint for BuildKit to
push through while a build runs. The endpoint holds the credentials for reaching
the target device and requires a password generated for that build alone. Other
processes on the host cannot use it to push their own image to the device.
The password is passed to BuildKit in a file with mode `0600`. It is not placed
on the command line, where other local users could read it through `/proc`.
## `status`
`status` reports what a build host can actually do, which is worth checking
before pointing a build at it:
```
Builder role: enabled
BuildKit: available (v0.32.2)
Cache: /data/buildkit/state (823.3 GiB free of 911.7 GiB)
Platform: linux/arm64
Builds: linux/arm64 natively
```
- **Builder role** — whether `enable` has been run.
- **BuildKit** — whether a buildkitd socket is present at
`/run/buildkit/buildkitd.sock`, and the `buildctl` version found on `PATH`.
BuildKit is installed out-of-band (distro package, release tarball, or not at
all), so "which version is on that box" is otherwise invisible from the CLI.
- **Cache** — BuildKit's state directory and the free and total space on its
filesystem. If it reports `inspection failed`, remote builds are refused
until the cache location and available space can be determined.
- **Builds** — the platforms this host builds natively. Only its own platform is
claimed. Emulated platforms are reported empty rather than guessed at, so a
cross-architecture build is refused up front instead of silently running under
QEMU and taking twenty minutes.
`status` needs neither the builder role nor BuildKit to answer — that is the
point of it. A device that is missing both still reports so.
## Cache space safety
Before starting a remote build, `wendy run --build-host` checks the free space
where BuildKit keeps its cache:
- Less than 8 GiB free: the build is refused.
- At least 8 GiB but less than 25 GiB free: the build proceeds with a warning.
- At least 25 GiB free: the build proceeds without a cache-space notice.
To move the cache, set buildkitd's `--root` option or put a top-level `root` in
the TOML file selected by its service, then restart buildkitd. For example:
```toml
root = "/data/buildkit/state"
```
The active file is not necessarily `/etc/buildkit/buildkitd.toml`: a service can
select another one with `buildkitd --config`. After restarting, run
`wendy device build-host status` and confirm that the `Cache` line names the
intended path and filesystem. If it reports `inspection failed`, inspect the
running buildkitd command and its active configuration rather than assuming the
default path.
## A Mac cannot be a build host
The Mac agent runs Linux containers through Apple Container, which has no
BuildKit underneath, so `status` on a Mac reports BuildKit unavailable and
`enable` cannot make it otherwise. A Mac remains a perfectly good build
*target*, and the intended machine to run `wendy run` from.
## Adopted Linux hosts
A non-WendyOS Linux machine that has been adopted into the mesh — a DGX Spark
running Ubuntu, say — is a legitimate build host, but Ubuntu ships no `buildkit`
package. Install the upstream release tarball and make sure `buildctl` is on
`PATH` by name (the agent runs it unqualified, and systemd units get a minimal
`PATH`, so a symlink into `/usr/bin` is the reliable placement). `status` then
reports the version it finds. If the host's root filesystem is small, configure
BuildKit's state directory on a larger filesystem before submitting builds.
---
# Dashboard
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/dashboard/
---
title: "Dashboard"
description: "Renders a TUI dashboard for your device, showing OTel metrics and logs in real time."
---
Renders a TUI dashboard for your device, showing OTel metrics and logs in real time.
## Flags
| Flag | Description |
|------|-------------|
| `--app ` | Filter the dashboard to a single application. |
## Key bindings
The footer lists the available keys:
```
q/Ctrl+C exit | ↑/↓ scroll | ←/→ pan logs | G/g end/start
```
> Looking for the app-count status line (`3 apps ● 2 running ○ 1 stopped`)?
> That belongs to the interactive app list, [`wendy device apps list`](apps/list.md),
> not to this dashboard.
---
# wendy device enroll
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/enroll/
---
title: "wendy device enroll"
description: "Add a device to your Wendy Cloud organization so you can manage it remotely. For a new board, first follow its installation guide. For Wendy Lite, use the USB enrollment guide."
---
# `wendy device enroll`
Add a device to your Wendy Cloud organization so you can manage it remotely.
For a new board, first follow its [installation guide](../../../../../../#2-set-up-a-device).
For Wendy Lite, use the [USB enrollment guide](../../../../../../advanced/cli/lite-enrollment/).
## Enroll a device
Sign in, find your device, then enroll it:
```sh
wendy cloud login
wendy discover
wendy device enroll --device playful-reed.local --name lab-pi-01
```
Replace the address and name. The CLI asks which organization to use when you
have more than one sign-in. Keep the device powered and connected until the
command finishes, then run `wendy cloud discover` to check its Cloud presence.
`wendy cloud enroll-device` uses the same enrollment workflow.
The CLI normally renews a nearly expired sign-in automatically. If it asks you
to sign in again, follow the prompt; there is no need to log out first.
> `wendy device enroll` is an advanced command and is hidden from
> `wendy device --help`. The interactive `wendy device setup` wizard also helps
> configure Wi-Fi and enrollment.
## Choose a name
Use a name that is unique within your organization: up to 63 lowercase letters,
digits, or hyphens, starting with a letter and ending with a letter or digit.
You can change it later with `wendy device rename`.
Without `--name`, the CLI suggests the device's hostname, lowercased and without
`.local`. In a terminal you can accept or replace it. If there is no usable
hostname, enter a name when prompted; scripts must supply `--name`.
The device also receives a permanent identity that Wendy manages automatically.
Renaming it does not change that identity.
## Requirements and troubleshooting
- You need a current agent and permission to enroll in the selected organization.
- A Mac target needs macOS 26 or later for Cloud enrollment. On macOS 15 you can
still run local apps; update macOS before enrolling.
- If the device was intentionally reset or reflashed, the CLI may ask whether
to trust its setup connection again. Accept only after confirming that it is
the device you reset. A failed enrollment preserves the previous saved identity.
- If it is already enrolled, [remove the old enrollment](./unenroll.md) first.
## Flags
| Flag | Description |
| --- | --- |
| `--device` | Address or selector of the device to enroll. Omit it to use normal device selection. |
| `--name` | Device name. Defaults to the hostname when available. |
| `--cloud-grpc` | Select a saved sign-in by its Cloud endpoint. Use this for a self-hosted deployment. |
| `--org` | Organization ID override for legacy enrollments only. Current sign-ins use their selected organization. |
| `--acme-directory-url` | Custom certificate enrollment URL supplied by your administrator. Normally selected automatically. |
For Lite-specific certificate and connection overrides, see the
[advanced Lite enrollment options](../../../../../../advanced/cli/lite-enrollment/#advanced-self-hosted-deployments).
## Remove enrollment later
Use [`wendy device unenroll`](./unenroll.md). For current WendyOS enrollments,
connect over the LAN and use `--check` to verify reset support before removal.
The native Mac agent does not yet support that reset. The
[unenrollment guide](../../../../../../device/unenrollment/) covers offline devices and
interrupted cleanup.
---
# Get Default
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/get-default/
---
title: "Get Default"
description: "Note: wendy device get-default is not listed in wendy device --help, but it remains fully functional."
---
> **Note:** `wendy device get-default` is not listed in `wendy device --help`,
> but it remains fully functional.
Shows the current [default device](../../device-selection.md), if one is set.
Use this to confirm what is persisted in the CLI configuration:
```sh
wendy device get-default
```
Use [`set-default`](./set-default.md) to change the saved target, or [`unset-default`](./unset-default.md) to clear it.
---
# wendy device info
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/info/
---
title: "wendy device info"
description: "Shows organization, agent version, OS, architecture, GPU, NPU, and hardware info for the target device."
---
# `wendy device info`
Shows organization, agent version, OS, architecture, GPU, NPU, and hardware info for the target device.
## Usage
```sh
wendy device info [flags]
```
## Description
`wendy device info` queries the connected device's agent and prints its version, operating system, CPU architecture, CPU core count, total RAM, GPU and NPU presence, and other hardware details. Use this command anywhere device metadata is needed — in scripts, CI pipelines, or interactively.
The output format follows the standard `--json` / human-readable convention shared across all device commands.
### Organization output
`Organization:` shows the device's enrolled organization name and ID, or just the ID when the name cannot be resolved. The name lookup uses credentials for the device's cloud and organization, with a cached name as an offline fallback. The ID comes from the device's enrollment, regardless of your default organization.
With `--json`, `organization` contains a string `id` and an optional `name`:
```json
{"organization": {"id": "42", "name": "Robotics"}}
```
The ID can be a tenant UUID or a legacy numeric ID encoded as a string. An unenrolled device shows `Organization: Not enrolled` and `"organization": null`. If enrollment information is unavailable, including on unsupported agents or Bluetooth connections, the field is omitted.
### CPU and memory output
Two headline hardware specs are included when the agent reports them. Both are omitted from the human-readable output and the JSON map when the agent cannot read them (older agents or non-Linux hosts), so treat them as optional.
| Field (JSON) | Human-readable label | Description |
|---|---|---|
| `cpuCount` | `CPU Cores:` | Number of online logical CPU cores. |
| `memTotalBytes` | `Memory:` | Total physical RAM in bytes; human-readable output shows a formatted size (e.g. `131.9 GB`). |
For **live** CPU and memory utilization, use [`wendy device top`](top.md).
### Power mode output
On NVIDIA Jetson devices the agent reports the current power mode (nvpmodel), shown as `Power Mode: MODE_30W (2)`. With `--json`, `powerMode` holds the numeric `id` and the `name`:
```json
{"powerMode": {"id": 2, "name": "MODE_30W"}}
```
The field is omitted on devices that are not a Jetson, when the mode cannot be read, and on agents that predate it. To see every available mode or switch, use [`wendy device power-mode`](power-mode.md).
### Storage output fields
When the agent can identify the filesystem that holds `/var/lib/containerd`, it is reported alongside the mounted partitions. The field is omitted from the JSON map when inspection fails or the agent predates it.
| Field (JSON) | Human-readable label | Description |
|---|---|---|
| `containerStorage` | `(container storage)` suffix on the matching partition row | The mounted filesystem backing `/var/lib/containerd`: `mountpoint`, `filesystem`, `device`, `usedBytes`, and `totalBytes`. Image pulls and container layers consume this filesystem, so its usage feeds the disk-usage warning. |
### GPU output fields
On GPU-capable devices, the following GPU fields are included. Each is omitted from both the human-readable output and the JSON map when the agent does not report it (e.g. non-GPU devices or older agents), so consumers should treat every field as optional.
| Field (JSON) | Human-readable label | Description |
|---|---|---|
| `gpuVendor` | `GPU:` | GPU vendor (e.g. `nvidia`, `qualcomm`); shown as `unknown` in human-readable output when a GPU is present but the vendor is unreported. |
| `jetpackVersion` | `JetPack:` | JetPack/L4T version string (Jetson only). |
| `cudaVersion` | `CUDA:` | CUDA toolkit version (e.g. `12.6`). |
| `gpuArch` | `GPU Arch:` | GPU architecture identifier. Format is vendor-specific (e.g. `sm_87` for NVIDIA, `a623` for a Qualcomm Adreno). On a Jetson, which ships no `nvidia-smi`, it comes from the SoC in the device tree: `sm_110` on Thor, `sm_87` on Orin, `sm_72` on Xavier. |
| `gpuCapabilities[]` | `GPU Compute:` | One entry per detected GPU: `vendor`, `path` (the device node that identified it, e.g. `/dev/dri/card0`), and `computeBackends` (`cuda`, `rocm`, or `metal`). A single GPU prints its backends; several print each one with its vendor and path. The line is omitted when no GPU reports a backend, and no entries at all means an older agent. An on-SoC neural accelerator is not a GPU backend — see `npuBackends`. |
### NPU output fields
On devices with an on-SoC neural accelerator the agent can reach, the following fields are included.
| Field (JSON) | Human-readable label | Description |
|---|---|---|
| `hasNpu` | — | Whether the device has a reachable NPU. Always present; `false` elsewhere. |
| `npuVendor` | `NPU:` | NPU vendor (e.g. `qualcomm`); shown as `unknown` in human-readable output when an NPU is present but the vendor is unreported. |
| `npuBackends[]` | `NPU:` | Runtimes an app can use on the NPU (e.g. `qnn` on a Qualcomm Hexagon). Rendered in parentheses after the vendor, as `qualcomm (qnn)`. Empty when the vendor's runtime is unrecognized, and on agents predating this field. |
Detection requires a non-secure FastRPC node: the signed-PD nodes are root-only, so a board exposing only those reports no NPU. The vendor comes from the DSP's device-tree `compatible`, the only vendor signal an on-SoC accelerator has.
FastRPC is the DSP's transport, so it is evidence for the NPU alone. A Dragonwing reports `NPU: qualcomm (qnn)` and no GPU compute backend for the Adreno beside it.
`wendy device info` reports static GPU *metadata* (vendor, architecture, toolkit versions). For **live** GPU utilization, memory, temperature, and power draw, use [`wendy device top`](top.md).
### Network output
The output includes the device's network interfaces and their routable IP addresses under a `Network:` block (human-readable) or a `networkInterfaces` array (JSON). This is the address to use to reach an app running on the device — the `.local` hostname does not always resolve on every network.
Loopback, down, and container/virtual bridge interfaces are omitted, as are link-local addresses. The section is absent entirely when the agent cannot enumerate interfaces (e.g. older agents).
| Field (JSON) | Human-readable label | Description |
|---|---|---|
| `networkInterfaces[].name` | (row label) | Interface name (e.g. `eth0`, `wlan0`). |
| `networkInterfaces[].ipAddresses` | (row value) | Routable IPv4/IPv6 addresses assigned to the interface. |
### OS Update output
When an update outcome has been recorded, `wendy device info` prints a compact `OS Update:` block:
```
OS Update:
Last update: committed (0.16.0 → 0.17.0)
```
This reads the saved outcome. For live A/B slot state, pending updates, and engine diagnostics, run [`wendy os update-status`](../os/update.md).
The block is omitted when no update record exists, the agent does not support update status, or the optional status request fails or times out.
When the last update outcome is not `committed`, a hint is printed:
```
Details: wendy os update-status
```
The outcome is colour-coded: `committed` is green, `rolled back` is amber, and failure outcomes are red.
### JSON: `osUpdate` field
When a recorded update outcome is available, `--json` includes an `osUpdate` object. It is omitted when no outcome is available.
```json
{
"osUpdate": {
"lastUpdate": {
"outcome": "committed",
"createdAtUnix": 1750000000,
"oldOsVersion": "0.16.0",
"newOsVersion": "0.17.0"
}
}
}
```
| Field | Description |
|---|---|
| `osUpdate.lastUpdate.outcome` | One of `committed`, `rolled back`, `rollback failed`, `commit failed`, `unknown`. |
| `osUpdate.lastUpdate.createdAtUnix` | Unix timestamp of the recorded update outcome. |
| `osUpdate.lastUpdate.oldOsVersion` / `newOsVersion` | OS versions before and after the update (omitted when not recorded). |
| `osUpdate.lastUpdate.note` | Failure reason from the updater, if any (omitted when empty). |
For the live engine snapshot, use `wendy os update-status --json` and read its `engineStatus` field. Basic device info does not request this snapshot.
## Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--check-updates` | `false` | Check whether a newer agent release is available and include the result in the output. |
| `--prerelease` | `false` | Include pre-release (nightly) versions when checking for updates. |
## Examples
Print device info in human-readable form:
```sh
wendy device info --device my-device.local
```
Print device info as JSON (useful in scripts):
```sh
wendy device info --device my-device.local --json
```
Extract specific fields with `jq`:
```sh
wendy device info --device my-device.local --json | jq '{osVersion, agentVersion: .version, deviceType, cpuCount, memTotalBytes}'
```
## Deprecated alias
`wendy device version` is a deprecated alias for this command. It remains functional for backward compatibility but is hidden from help output. When invoked in non-JSON mode it prints a deprecation warning to stderr:
```
Warning: 'wendy device version' is deprecated; use 'wendy device info' instead.
```
No warning is emitted when `--json` is passed, so existing machine-readable scripts that use `wendy device version --json` continue to work without noise on stderr.
Migrate any usage of `wendy device version` to `wendy device info`.
## Related
- [`wendy os update-status`](../os/update.md) — full OS update record including service-level healthcheck details and the live engine snapshot
- [`wendy os update`](../os/update.md) — apply a WendyOS OTA update
---
# Logs
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/logs/
---
title: "Logs"
description: "Tails the OTel logs from a wendy-agent, rendering them in the terminal. By default it shows all apps and the agent's logs."
---
Tails the OTel logs from a wendy-agent, rendering them in the terminal. By default it shows all apps **and the agent**'s logs.
With `--app`, you can filter on a per-app basis. You can also set a minimum log level using, for example, `--level error`.
If you provide `--json`, the output will be JSONL, one line per log statement.
To inspect the device's kernel ring buffer (`dmesg`) instead of container/agent logs, use [`wendy device os-logs`](./os-logs).
## Flags
| Flag | Description |
|------|-------------|
| `--app ` | Only show logs from the named app. For multi-service apps this also includes each service container's logs, so crash output from a crash-looping service member is reachable with `--app `. |
| `--service ` | Only show logs from the named service. |
| `--level ` | Minimum log level: `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. **Defaults to `info`** when neither `--level` nor `--min-severity` is given, so kernel `dmesg` debug/trace output stays hidden unless you ask for it. |
| `--min-severity ` | Minimum OTel severity number; a numeric alternative to `--level`. |
| `--tail ` | Request the last N stored log batches **matching the active filters** before following new output (default `0`). The window counts only batches that survive `--app`/`--service`/`--level`, so other apps logging at high volume on the same device cannot push the requested app's logs out of the requested window. |
| `--no-follow` | Print the logs the device replays and exit instead of following new output. Combine with `--tail ` to get the last N matching batches, e.g. `wendy device logs --app --tail 20 --no-follow`. The command ends when the device closes the stream after its replay (agents with finite log replay), when it switches from replayed to live output, or after a short pause once the replay stops (1.5 s, stretched on a slow link up to 10 s; the command notes on stderr when it ended this way with fewer batches than `--tail`, or than the 20 the device caches without it). Device agents released before 2026-08-19 replay history only with `--tail`, and those before 2026-05-22 not at all; when nothing is replayed, the command says so on stderr and exits. |
---
# Os Logs
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/os-logs/
---
title: "Os Logs"
description: "wendy device os-logs streams the device's kernel ring buffer (dmesg) for inspecting kernel/boot/hardware messages. By default it replays the buffered records and then keeps followi"
---
`wendy device os-logs` streams the device's kernel ring buffer (`dmesg`) for inspecting kernel/boot/hardware messages. By default it replays the buffered records and then keeps following new ones until you interrupt with ctrl-c (like `dmesg -w`). Pass `--follow=false` (`-f=false`) for a one-shot snapshot that prints the current buffer and exits (like `dmesg`).
The output is raw and unredacted. This is the kernel log, distinct from the container/agent logs shown by [`wendy device logs`](./logs) and not filterable by app or service.
Each record is printed in classic dmesg style, `[ seconds.microseconds] message`. With `--json`, each record is emitted as one JSON object (`timestamp_us`, `level`, `message`). Available on Linux devices only.
---
# wendy device pair
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/pair/
---
title: "wendy device pair"
description: "Connect peripherals and save their settings on the selected Wendy device."
---
# `wendy device pair`
Connect peripherals and save their settings on the selected Wendy device.
```sh
wendy device pair --device
```
Use Tab or Shift+Tab to switch between **Bluetooth**, **SensorLink**, **IP Cameras**,
**USB Arms**, and **Unitree**. Discovery runs on the selected device, so it lists
peripherals reachable from that device even when your CLI is elsewhere.
| Key | Action |
| --- | --- |
| Arrow keys | Select a peripheral. |
| Enter | Pair it, or edit an IP camera's login. |
| `f` | Forget the selected saved pairing. |
| `r` | Refresh discovery. |
| `q` / Esc | Quit. |
## Bluetooth peripherals
Select a peripheral and press Enter to connect and pair it. Unnamed peripherals
are hidden; press `h` to show them. Press `d` to disconnect a selected peripheral.
This is separate from Wendy Net's automatic Bluetooth links between devices.
## SensorLink sources
Select a sensor source and press Enter to pair it. Sources must be reachable
from the selected Wendy device, enrolled in the same organization, and accessible
to your signed-in account. Sources needing enrollment are listed separately;
enroll them and press `r` to refresh.
Saved sources are found again if their address changes, and remain available to
forget while offline. If an older agent falls back to discovery on the CLI's
network, the list says so; update that agent to scan from the device instead.
`--name` gives the pairing a friendly name; `--sensors` limits it to selected
sensor names. Either flag opens the SensorLink tab. Use `--list` to print saved
SensorLink pairings.
## IP cameras
Select a camera and press Enter to save its login. Replace the suggested `admin`
username with your camera's account and enter its password. Passwords are masked.
Inside the form, Tab switches fields, Enter advances or saves, and Esc cancels.
Press `f` to forget a camera and remove its saved login, including when offline.
See [network cameras](../../../../../../hardware/camera/#ip-cameras) for streaming and
camera-specific commands.
## Unitree Go2 robots
Agree with the robot's operator before pairing: the connection uses a robot
client slot and remains active after you close the CLI. Arrange a handoff if a
mobile app or another controller already uses it.
```sh
wendy device pair --device my-wendy --unitree
```
1. Press `l` to sign in to your Unitree account. Use `--unitree-region cn` for an
account registered in China; the default region is `global`.
2. Select your robot by its serial number. Press Enter to connect.
3. If discovery does not find its address, press `i` to enter it. You can also
start with `--ip 192.168.1.42` and select the matching account robot.
The password is masked and is not saved. The Wendy device saves the selected
robot's connection key so it can reconnect after a restart. Keys are stored in a
private file, without encryption at rest. Forget the pairing with `f` to remove
that key. Sign in again and reconnect to refresh it. Pairings belong to the Wendy
device's organization; unenrollment removes them.
Saved offline robots stay listed. Press Enter to reconnect or `i` to change an
address. Use a trusted robot LAN, especially with older firmware that cannot
provide authenticated connections.
Pairing sends connection checks and heartbeats, not movement commands.
`connected` does not confirm camera streaming, motor readiness, or physical safety.
## USB robot arms
Connect the arm to the selected Mac or Linux Wendy device, then open its inventory:
```sh
wendy device pair --device --usb-arms
```
Select the USB serial endpoint, press Enter, and confirm that it is an SO-101.
Press `f` to forget a saved arm or `r` to refresh after reconnecting it. On a Mac,
use the native Wendy Agent app; the Go agent on macOS does not support this inventory.
Pairing saves a configuration; it does not move, calibrate, or verify the arm.
USB adapter information cannot prove which arm is attached. Check the physical
connection yourself before confirming. A saved arm may be present, disconnected,
or ambiguous. Resolve ambiguous matches before pairing; Wendy does not choose
one automatically.
Adapters with a serial number can keep their binding when moved between USB ports.
Without a serial number, the binding follows the USB port, including a replacement
adapter. The interactive prompt explains this; scripts must explicitly accept it
with `--allow-location-binding`.
### USB scripting
List the inventory, then copy the exact `id` into a bind or forget command:
```sh
wendy --json device pair --device --usb-arms --list
wendy --json device pair --device --usb-arms --bind '' --arm-name Follower
wendy --json device pair --device --usb-arms --forget ''
```
Listing returns `{ "devices": [...] }`, binding returns `{ "device": {...} }`,
and forgetting returns `{ "forgotten": "" }`. Field names use snake_case.
`configured` means a binding was saved; it does not mean the arm was verified.
`access_status` remains `unknown` because discovery does not open the serial port.
Bindings survive agent restarts. If the saved configuration is corrupt, Wendy
reports an error and preserves the file; ask your administrator to back it up
before repair. An unsupported agent reports that it needs updating rather than
scanning the CLI computer instead.
## Selection flags
| Flag | Purpose |
| --- | --- |
| `--list` | List SensorLink pairings, or USB inventory with `--usb-arms`. |
| `--name`, `--sensors` | Name or filter a SensorLink pairing. |
| `--unitree` | Open the Unitree tab. |
| `--ip` | Supply a Go2's IPv4 address. Opens the Unitree tab. |
| `--unitree-region` | Unitree account region, `global` or `cn`. |
| `--usb-arms` | Open or script USB arm setup. |
| `--bind`, `--forget` | Save or remove an exact USB inventory ID. |
| `--arm-name` | Friendly USB arm name, up to 128 UTF-8 bytes. |
| `--allow-location-binding` | Accept a USB-port-based identity in scripts. |
Do not combine SensorLink, Unitree, and USB-specific flags in one operation.
---
# wendy device power-mode
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/power-mode/
---
title: "wendy device power-mode"
description: "Shows and sets the NVIDIA Jetson power mode (nvpmodel) of the target device."
---
# `wendy device power-mode`
Shows and sets the NVIDIA Jetson power mode (`nvpmodel`) of the target device.
## Usage
```sh
wendy device power-mode # interactive in a terminal, `list` otherwise
wendy device power-mode list [--json]
wendy device power-mode get [--json]
wendy device power-mode set [--yes]
```
## Interactive screen
Run in a terminal, `wendy device power-mode` opens a list of the device's modes with the current one and the boot default marked. Use the arrow keys to move and Enter to set the highlighted mode, or Esc (or `q`) to leave without changing anything. A mode that needs a reboot asks first, exactly as `set` does (below), and the screen then waits for the device to come back and shows the mode it reports.
Without an interactive terminal, or with `--json`, the bare command prints the same output as `list`, so scripts that call it keep working.
## List the current and available modes
`wendy device power-mode list` prints the mode the device is in and every mode its `nvpmodel` configuration offers:
```text
Power mode: MODE_30W (2)
Available modes:
0 MAXN
1 MODE_15W
* 2 MODE_30W (current, boot default)
3 MODE_50W
```
Modes are the device's own names and IDs, exactly as `/etc/nvpmodel.conf` declares them. They differ between Jetson models, and Wendy does not translate or alias them. `boot default` is the mode `nvpmodel` applies at boot (`PM_CONFIG DEFAULT`) and is omitted when the configuration does not declare one.
With `--json`:
```json
{
"current": {"id": 2, "name": "MODE_30W"},
"available": [{"id": 0, "name": "MAXN"}, {"id": 2, "name": "MODE_30W"}],
"bootDefault": {"id": 2, "name": "MODE_30W"}
}
```
## Get the current mode
`wendy device power-mode get` prints only the current mode, which suits scripts:
```text
MODE_30W (2)
```
With `--json` it prints `{"current": {"id": 2, "name": "MODE_30W"}}`. The current mode also appears in [`wendy device info`](info.md).
## Set a mode
```sh
wendy device power-mode set MAXN
wendy device power-mode set 0
```
The argument is a mode name or numeric ID from the list above. An unknown mode is rejected with the available modes.
Some modes switch immediately. Others only take effect after a reboot. For those, the command says so and asks before doing anything:
```text
Power mode MAXN (0) only takes effect after a reboot.
Set MAXN (0) and reboot the device now? (y/n)
```
- **Yes** sets the mode and reboots the device in one step.
- **No** leaves the device untouched. A mode that needs a reboot is never half-applied: NVIDIA's `nvpmodel` records it only when the reboot is accepted.
Without an interactive terminal (a script, CI, or `--json`), the command never asks. If the mode needs a reboot it exits with an error and changes nothing; pass `--yes` (`-y`) to confirm in advance and set the mode and reboot without asking. `--yes` has no effect on a mode that switches immediately.
With `--json`:
```json
{"mode": {"id": 0, "name": "MAXN"}, "rebootRequired": true, "rebootScheduled": true}
```
A mode change can alter CPU and GPU clocks under running workloads, and a reboot stops every app on the device. Check what else is using the device first.
## Unsupported devices and older agents
Devices that are not an NVIDIA Jetson report that power modes are not supported. A device whose agent predates this command reports that the agent needs updating (`wendy device update`).
## Related
- [`wendy device info`](info.md) — includes the current power mode
---
# Push Agent
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/push-agent/
---
title: "Push Agent"
description: "Developer / diagnostic tool. This command is hidden from wendy device --help. It is not part of the normal update workflow."
---
> **Developer / diagnostic tool.** This command is hidden from `wendy device --help`. It is not part of the normal update workflow.
Uploads a locally-built `wendy-agent` binary to the device using the same `UpdateAgent` RPC that `wendy device update` uses, bypassing the GitHub release fetch. After upload the CLI waits for the restarted agent to become reachable, then verifies what the device is running:
- When the agent reports the SHA-256 of its running executable and it matches the uploaded binary, the command confirms the device is running your binary and prints the version it reports. This is the only proof that matters for a dev-over-dev push, since dev builds share identical version strings.
- When the running hash does **not** match the uploaded binary, the command exits non-zero — the push did not land.
- When the agent reports no hash (the macOS agent, agents from releases predating the field) but the upload stream was confirmed, the command says the push could not be verified and exits 0.
- When the upload stream was **not** confirmed *and* the agent reports no hash, nothing proves the push landed, so the command exits non-zero and asks you to check the device.
An unconfirmed upload is expected rather than exceptional here: the agent restarts the moment the binary lands, which can close the stream before its acknowledgement arrives.
## Usage
```sh
wendy device push-agent
```
## When to use this
Use `push-agent` when you need a patched or development agent running on a device to capture verbose engine diagnostics (e.g. raw EFI variables via `wendy os update-status --json`) without SSH access. For routine agent updates, use [`wendy device update`](update.md).
## Difference from `wendy device update --binary`
`wendy device update --binary` performs an OS-update check after uploading the binary and re-uploads the binary if an OS update is applied. `push-agent` performs no OS update check — it uploads the binary and waits for the agent restart only.
Both commands hash-verify the running binary after the restart (see [`device update` verification](update.md#verification)), but `push-agent` reconnects by address only rather than through the full reconnect path `device update` uses, so it does not follow a device that comes back at a different address.
## Arguments
| Argument | Description |
|---|---|
| `` | Path to the compiled `wendy-agent` binary to upload. |
## Exit codes
| Code | Meaning |
|---|---|
| `0` | Agent restarted and the uploaded binary was verified by hash — or the upload was confirmed and the agent reports no hash. |
| non-zero | Binary could not be read, upload failed, the agent did not come back online, the running binary's hash does not match the upload, or the upload was unconfirmed and the agent reports no hash. |
---
# Rename
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/rename/
---
title: "Rename"
description: "Renames a device in two places: its asset name in Wendy Cloud, and its hostname (and mDNS .local name) on the device itself. A single name is applied to both."
---
Renames a device in two places: its asset name in Wendy Cloud, and its hostname
(and mDNS `.local` name) on the device itself. A single name is applied to both.
```sh
wendy device rename [name]
```
Without a `[name]` argument, an interactive prompt asks for one, prepopulated
with `wendyos-` as a starting point. The hostname is set **literally** — no
`wendyos-` prefix is added for you.
## Name rules
The name must be a valid DNS label:
- starts with a lowercase letter
- contains only lowercase letters, digits, and hyphens
- does not end with a hyphen
- is at most 63 characters
## Related
- `wendy device unpin ` — clear a device identity pin keyed by the old name
---
# Set Default
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/set-default/
---
title: "Set Default"
description: "Selects a device as the default device, so that other commands default to this device if available."
---
Selects a device as the [default device](../../device-selection.md), so that other commands default to this device if available.
The default can be a hostname, IP address, provider key, or explicit `host:port` value:
```sh
wendy device set-default my-mac.local:50051
wendy device info --json
wendy run
```
Use `wendy device get-default` to see the current default, or `wendy device unset-default` to clear it.
A device name made only of digits is rejected. To make a cloud asset the default, pass its cloud selector, `cloud:///org//asset/`; the error for a numeric name prints the exact selector when you are logged in.
## Scripts and other non-interactive shells
With no argument, `set-default` opens the device picker, which needs an interactive terminal. In a script, CI, an AI agent's shell, or with `--json`, it instead exits with an error that shows the command to run and the devices this CLI has seen recently:
```sh
wendy device set-default wendyos-calm-zinnia.local
wendy device set-default vm:dev
```
To target a different device in one shell or AI session without changing the default that every session shares, set `WENDY_DEVICE` instead (see [device selection](../../device-selection.md)).
## Certificate pinning
When you set a default device (and again on the first successful connection if the device was offline at set-default time), the CLI **pins** the device's identity — the organisation and cloud host its TLS certificate belongs to. On every later connection to the default device, the CLI checks that the device still presents that same organisation and cloud host.
A routine certificate **renewal or re-enrollment** keeps the same organisation and cloud, so it is accepted silently. A change of organisation or cloud host — which can indicate a man-in-the-middle or a swapped device — **refuses the connection**. So does a device that was pinned while enrolled but now answers with no identity at all, which means it was reflashed, factory reset, or replaced by something squatting its name.
Both refusals are **unconditional**: they read identically in interactive, `--json`, and non-interactive runs, and there is deliberately **no "trust this anyway?" prompt** — a man-in-the-middle warning that can be dismissed gets dismissed. Re-running `wendy device set-default` does not re-pin either. Clearing the pin is a separate, deliberate act:
```sh
wendy device unpin
```
Loopback addresses share the pin of their bare host, whatever the port: `127.0.0.1:50051` and `127.0.0.1:50061` are both checked against the `127.0.0.1` pin, and `localhost:50051` against the `localhost` pin. The exception is a running local VM: `127.0.0.1` on its forwarded agent port, or on the mTLS port after it, is that VM, pinned as `vm:` like `--device vm:`, so each VM keeps its own pin. `wendy device set-default vm:` clears only that VM's pin, never the shared `127.0.0.1` pin.
---
# wendy device setup
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/setup/
---
title: "wendy device setup"
description: "Configure a device's Wi-Fi and enrollment interactively. This advanced command remains available even though it is hidden from wendy device --help."
---
# `wendy device setup`
Configure a device's Wi-Fi and enrollment interactively. This advanced command
remains available even though it is hidden from `wendy device --help`.
```sh
wendy cloud login
wendy device setup
```
Follow the prompts to select your device, configure Wi-Fi, and optionally update
the agent. For enrollment alone, use
[`wendy device enroll`](./enroll.md). For a self-hosted deployment, ask your
administrator for its Cloud endpoint and select the matching saved sign-in.
---
# Telemetry Stream
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/telemetry-stream/
---
title: "Telemetry Stream"
description: "Note: wendy device telemetry-stream is not listed in wendy device --help, but it remains fully functional."
---
> **Note:** `wendy device telemetry-stream` is not listed in
> `wendy device --help`, but it remains fully functional.
Streams all OTel data as a custom JSONL format. This is used by the [VSCode extension](../../../../vscode/) to render the OTel traces, metrics and logs using only a single API call.
## Planned: runtime telemetry for embedded apps (not yet shipped)
> The flow described below is **planned, not implemented**. Today the CLI only streams the OTel data generated by `wendy-agent` itself; runtime failures inside an embedded WASM app are not yet surfaced back to the CLI automatically.
A pending change will pipe runtime logs and errors from on-device apps — including heap-allocation failures, WAMR fault conditions, and other runtime issues that pre-deploy validation can't catch — back to the user through this same CLI surface. Memory limits and the exact taxonomy of reported errors are still being defined.
---
# wendy device top
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/top/
---
title: "wendy device top"
description: "Live CPU, memory, disk, GPU, and temperature telemetry for the device and its containers — an htop-style monitor for a WendyOS device."
---
# `wendy device top`
Live CPU, memory, disk, GPU, and temperature telemetry for the device and its containers — an `htop`-style monitor for a WendyOS device.
## Usage
```sh
wendy device top [flags]
```
## Description
`wendy device top` opens a full-screen, auto-refreshing dashboard showing whole-machine CPU and memory utilization, the hottest current device temperature, per-GPU utilization/memory (and temperature/power where reported), and a per-app/per-container table of CPU% and memory. CPU percentages are computed from deltas between refreshes, so the first frame may read low until a second sample is taken.
The temperature header normally shows the maximum reading and its source. A yellow `●` appears when any classified sensor is within 5°C of its operational warning threshold; the circle turns red at or above that threshold. On a Unitree Go2 discovered through LowState telemetry, the agent adds the IMU and 12 physical motor temperatures to the Linux thermal zones and expires them after 15 seconds without a fresh sample. The current operational thresholds are 70°C for Go2 motors and 85°C for the Go2 IMU and host thermal zones. These values were observed and chosen for Woof operations; they are not Unitree or NVIDIA vendor ratings.
When the device stops answering polls — because it lost power, dropped off the network, or ran its battery flat — the dashboard raises a banner in place of the usual status flash:
```
⚠ DEVICE OFFLINE — no response for 41s (last battery reading 3%, discharging)
Readings below are the last values received, not live.
```
The meters keep showing the last successful sample, since there is nothing newer to draw, and the second line marks them as stale. The battery percentage is named only when that last sample showed the pack discharging — a charging or full pack is not evidence of why the device went away. Before the first sample arrives the dashboard shows a `Connecting…` placeholder instead; once the device is offline the banner replaces that placeholder rather than appearing alongside it.
Only transport-level failures raise the banner. An error the agent itself returns means the device answered and is therefore still reachable, so it appears as the normal status flash at the bottom of the screen — and it clears the banner if one was up. The banner also clears on the next successful poll.
Apps are grouped the same way as [`wendy device dashboard`](dashboard.md): multi-service apps show a group header with one subrow per service. Running apps (`●`), stopped apps (`○`), and crash-looping apps (`↻`) have distinct row styling and are counted separately. Resource columns show unavailable values for stopped and crash-looping rows instead of presenting them as active zero-usage workloads. A side panel shows the listening ports of the currently selected running app.
Press `Enter` to open the selected app's recent and live logs. Use the arrow keys or Page Up/Page Down to scroll, `End` to resume following, and `Esc` to return to the monitor. Press `s` to start the selected app.
The disk meter shows the filesystem holding container storage, which is `/data` on WendyOS. It refreshes every ten seconds. Press `p` to clear unused container layer and snapshot caches. Current images and active apps remain intact; containerd reclaims unreachable data in the background.
Press `x` to stop the selected app. For a multi-service app, this stops the whole app even when the cursor is on one of its service rows. Stop uses Wendy's normal graceful shutdown behavior and may escalate to a force kill when the app does not exit within its grace period.
> **Note:** This command requires a recent device agent. Against an agent that's too old to report resource stats, the command reports that the agent doesn't support resource stats and suggests updating it with [`wendy device update`](update.md).
### Keyboard shortcuts
| Key | Action |
|---|---|
| `↑` / `k`, `↓` / `j` | Move the selection up / down |
| `c` | Sort apps by CPU usage (descending) |
| `m` | Sort apps by memory usage (descending) |
| `Enter` | Open recent and live logs for the selected app |
| `Esc` | Return from logs to the monitor |
| `s` | Start the selected app and all of its services |
| `x` | Stop the selected app and all of its services |
| `p` | Clear unused container caches |
| `q` / `Ctrl+C` | Quit |
## Flags
| Flag | Default | Description |
|---|---|---|
| `--interval` | `2s` | Refresh interval for the live view. A device that loses power mid-request cannot freeze the dashboard — a stalled poll times out and raises the offline banner. |
The [global `--json` flag](../../global-flags.md) is also honored — see below.
## JSON snapshot mode
`wendy device top` is a live TUI, so it cannot stream into a pipe. When `--json` is passed (or stdout is not a TTY), the command switches to a **one-shot snapshot**: it samples the device, prints a single JSON object, and exits instead of rendering the dashboard.
```sh
wendy device top --json
```
Plain snapshots include a `STATE` column. JSON snapshots have this shape:
```json
{
"host": {
"cpuPercent": 12.5,
"cpuCount": 8,
"memUsedBytes": 2147483648,
"memTotalBytes": 8589934592,
"thermalZones": [
{ "name": "go2/imu", "tempC": 79.0 },
{ "name": "go2/motor/fr-thigh", "tempC": 66.0 },
{ "name": "tj-thermal", "tempC": 55.0 }
],
"maximumTemperature": { "name": "go2/imu", "tempC": 79.0 },
"battery": {
"percent": 82.0,
"state": "discharging",
"secondsRemaining": 5400
},
"gpus": [
{
"index": 0,
"name": "Orin",
"utilPercent": 30.0,
"memUsedBytes": 1073741824,
"memTotalBytes": 8589934592,
"tempC": 45.0,
"powerW": 7.5
}
]
},
"containers": [
{ "name": "my-app", "state": "running", "cpuPercent": 4.2, "memBytes": 134217728 }
]
}
```
- `host.containerStorage` reports `mountpoint`, `usedBytes`, and `totalBytes` for the container filesystem, and is omitted when unavailable. Plain snapshots also show this disk usage.
`host.battery` is omitted entirely on a device with no battery, and
`secondsRemaining` is omitted when the device reports no usable
charge/discharge rate.
- `host.gpus` is omitted on devices that report no GPU.
- `host.thermalZones` is omitted when the agent has no readable temperature source.
- `host.maximumTemperature` is the hottest valid thermal-zone or GPU reading and is omitted when no temperature is available.
- Go2 IMU and motor temperatures require a device agent with the LowState thermal extension; older agents continue to report host thermal zones only.
- Each GPU's `tempC` and `powerW` are omitted when the agent doesn't report them.
- `containers[].state` is `running`, `stopped`, or `crash-loop`.
- `containers[].cpuPercent` is each container's share of the whole machine (0–100 across all cores).
## Related
- [`wendy device dashboard`](dashboard.md) — full-screen app/service status dashboard
- [`wendy device info`](info.md) — one-shot device hardware and GPU metadata
- [`wendy device apps`](apps/) — list and manage deployed apps
---
# wendy device unenroll
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/unenroll/
---
title: "wendy device unenroll"
description: "Remove a device from Wendy Cloud and, when connected directly, clear its local enrollment so it can enroll again. Sign in to the organization that owns it."
---
# `wendy device unenroll`
Remove a device from Wendy Cloud and, when connected directly, clear its local
enrollment so it can enroll again. Sign in to the organization that owns it.
## Remove a reachable device
For WendyOS, use a direct LAN address. With a current enrollment, check support
first, then confirm the removal:
```sh
wendy device unenroll --device --check
wendy device unenroll --device
```
Update the agent with `wendy device update` if the command asks you to.
The native Mac agent does not yet support this reset for current enrollments.
For Lite, connect USB and use its selector from `wendy discover`:
```sh
wendy device unenroll --device wendy-lite:/dev/cu.usbmodemXXXX
```
Lite reboots after removal and keeps its Wi-Fi and app settings. Firmware must
support enrollment reset. LAN and Bluetooth cannot clear a Lite enrollment.
## Remove an offline device's Cloud access
```sh
wendy cloud discover --all --json
wendy cloud device unenroll --device
```
You can also use:
```sh
wendy device unenroll --cloud-only --device
```
Cloud-only removal revokes access and deletes the Cloud record without
contacting the device. Its local credentials remain intact. The selector can
also be the device ID shown in an enrollment-conflict error.
A `cloud://` selector, including a saved Cloud default, chooses Cloud-only removal.
## Retry after a failure
For current WendyOS enrollments, rerun the same command over the direct LAN
connection. For Lite, reconnect USB and rerun it. This also handles Cloud removal
that succeeded before the local reset failed.
Older numeric Cloud enrollments reset locally before Cloud cleanup. They require
an owner or administrator account. If cleanup fails, run the exact Cloud removal
command printed in the error; it retains the old enrollment details needed to
finish. See [device unenrollment](../../../../../../device/unenrollment/) for recovery guidance.
## Clear a device that Cloud no longer has
If Cloud has no record of a reachable WendyOS device, `--force` clears only the
device's local enrollment over the LAN so it can enroll again:
```sh
wendy device unenroll --device --force
```
It asks for confirmation, or accepts `--yes` (required with `--json`). Cloud
reports "not found" both for a missing device and for one your account cannot
access, so make sure the device is no longer in use. `--force` refuses a device
Cloud still lists, and stops on any other Cloud error. It does not delete a
Cloud record or revoke the device certificate. It needs an updated agent, a
sign-in to the device's organization and a direct LAN connection. To resume an
interrupted Cloud removal, use `--asset-id` instead.
If the connection drops before the device confirms, the command reports the
reset as unconfirmed and keeps the saved device pins. After checking the device
reset, clear them with `wendy device unpin `.
## Flags
| Flag | Description |
| --- | --- |
| `--device` | Device selector. Cloud-only removal accepts an asset ID, name, or device ID. |
| `--cloud-only` | Remove Cloud access without connecting to the device. |
| `--cloud-grpc` | Cloud endpoint of the saved sign-in to use. |
| `--asset-id` | Check the exact Cloud asset UUID for a current WendyOS enrollment. Not available for Lite or Cloud-only removal. |
| `--check` | Check the Cloud binding and agent reset support without making changes; with `--force`, check the forced local reset instead. Not available for Lite or Cloud-only removal. |
| `--force` | Clear only the local enrollment of a device Cloud has no record of. Does not delete a Cloud record or revoke the certificate. Not available with `--asset-id`, for Lite, or for Cloud-only removal. |
| `--yes` | Confirm removal without a prompt. Required when running non-interactively. |
## Related
- [Enroll a device](./enroll.md)
- [Enroll Wendy Lite](../../../../../../advanced/cli/lite-enrollment/)
---
# Unset Default
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/unset-default/
---
title: "Unset Default"
description: "Unsets the default device, so that other commands will prompt selection again."
---
Unsets the [default device](../../device-selection.md), so that other commands will prompt selection again.
Use [`get-default`](./get-default.md) to see the current default, or [`set-default`](./set-default.md) to set a new one.
---
# Update
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/update/
---
title: "Update"
description: "Updates the wendy-agent installation on the remote device, then checks for a newer WendyOS image. By default downloads the latest release binary from GitHub matching the device's C"
---
Updates the wendy-agent installation on the remote device, then checks for a newer WendyOS image. By default downloads the latest release binary from GitHub matching the device's CPU architecture. Pass `--binary ` to upload a locally built binary instead (e.g. a cross-compiled development build). When an upload is performed, the command waits for the restarted agent to become reachable, [confirms what the device is actually running](#verification), then checks the OS update outcome. The command exits non-zero if the update was rolled back.
If the device responds with "an update is already in progress" (a `FailedPrecondition` error), a previous upload likely committed without the agent restarting — a bug fixed in this release. Reboot the device to clear the stale lock, then retry.
On the auto-download path, if the device already runs the resolved release version, the upload and agent restart are skipped and the command reports that it is already up to date. The OS update step below still runs afterward — except in `--json` mode, where the command returns immediately and the OS update step is not run. Because `--json` skips the OS step, combining it with `--pr` is refused outright rather than silently ignoring the requested PR image.
GitHub release lookups use the `GITHUB_TOKEN` environment variable for authentication when it is present, and fall back to unauthenticated requests otherwise.
## Verification
A successful reconnect only proves the device is reachable — a silent no-op, a rollback, or an arch-mismatched binary that never starts would answer just as well. So after **every** agent upload, confirmed or not, `wendy device update` queries the restarted agent and checks what the device actually runs before reporting success.
If the connection drops while the agent binary is being uploaded (the agent restarts the moment the binary lands, which can close the stream before the confirmation arrives), the command treats the outcome as **unconfirmed** rather than an error: it prints an informational message, reconnects, and lets the check below decide the outcome.
The check has two levels:
1. **Binary SHA-256 — definitive.** When the agent reports the SHA-256 of the executable it was started from, the CLI compares it against the hash of the binary it uploaded. A match is proof the upload landed; a mismatch exits non-zero and asks you to re-run `wendy device update`. This is the only way to prove a dev-over-dev `--binary` push landed, since dev builds share identical version strings. The comparison is skipped for macOS targets — the upload payload there is an app-bundle ZIP, whose hash can never equal the running executable's — so those fall through to the version check.
2. **Version comparison — fallback.** When no hash is available (the macOS agent, agents from releases predating the field), the CLI compares version strings. On the auto-download path the device must report the resolved release version or newer. A device that still reports a **dev**-build version after a **release** update fails verification: dev builds sort as newer than any release, so without this guard a silent no-op would pass vacuously. When the expected version is itself dev-stamped (a `workflow_dispatch` publish can stamp a `-dev` suffix into the manifest), a dev report is the correct outcome and is accepted. The `--binary` path has no release version to compare against, so with no hash available any reachable agent is accepted.
The first reconnect can land on the still-alive *old* agent — it exits shortly after committing the update, and takes longer to do so on macOS — whose answer would mis-fail a good update. A failing verdict is therefore re-polled within the same restart budget used to wait for the agent to come back, and only becomes final when that window expires.
On success the reported version is included in the message:
```
Agent updated successfully (agent reports 2026.07.01-223311).
```
## JSON output (`--json`)
With `--json`, a successful agent update emits a single JSON object and the command returns without running the OS update step:
```json
{
"status": "success",
"message": "Agent updated successfully.",
"version": "2026.07.01-223311"
}
```
`version` is the agent version the device reported during verification. It is empty only if the agent reports no version.
When the device already runs the resolved release version, no upload happens and the object is instead:
```json
{
"status": "up-to-date",
"message": "Agent is already up to date."
}
```
## OS update step
After the agent is updated, the command checks for an OS update on WendyOS devices that advertise the in-house **wendyos-update** OTA engine. When a newer image is available it prompts before applying (default no); use `--yes` to apply without prompting, and `--nightly` to track the nightly channel for both the agent and the OS. Non-interactive runs report the available update without applying it. Devices without an OTA backend, and non-WendyOS hosts, skip this step silently — `device update` still succeeds as an agent-only update.
If the available artifact uses the wendyos-update stack (`.wendy` format) but the device image predates that stack, the auto-detected OS update is not offered — the command prints an explanation instead of prompting, and the agent-only update still counts as a success. This check runs only when an update is actually available; an already-current device is not warned.
An explicit `--artifact-url` pointing to a `.wendy` artifact on an incompatible device is **not** silently skipped: `device update` exits non-zero with the same reflash explanation.
## Update the OS to a pull-request build
```sh
wendy device update --pr 123 --yes
```
`--pr N` makes the OS update step install the WendyOS image built by
wendyos-builder PR #N instead of the manifest's latest. The agent-binary step is
unaffected: `--nightly` still selects the agent channel, and `--binary` still
uploads your build and re-applies it after the OS reboot.
PR images are **debug builds**: SSH is enabled, root login is passwordless, and
the serial console is active. They are for testing the PR on hardware — **never
install a PR image on a production device.** Artifacts are deleted when the PR
is closed.
`--pr` is supported for Linux disk-image devices (Raspberry Pi, Jetson Orin
Nano, Jetson AGX Orin) with OTA support; it is not supported for Jetson AGX
Thor or ESP32 targets. It is mutually exclusive with `--artifact-url` and
`--json`.
A `--pr` run never skips as "already current": a PR's version tag (`pr-N`) is
constant across rebuilds, so re-running after pushing a new commit to the same
PR always re-flashes. And unlike the default OS step — which degrades to an
agent-only success when the OS check fails — a `--pr` run must install that
PR's build or exit non-zero.
`--pr` works over the cloud tunnel (`wendy cloud device update --pr N`): the
resolved artifact is a public URL the device downloads directly, with only the
control stream tunneled. As with any cloud-tunneled OS update, the command does
not wait for the reboot (see [Post-update outcome](#post-update-outcome)).
## Pre-0.17.0 devices require a reflash
WendyOS 0.17.0 introduces a new OTA update system that is not backward-compatible with older images. When the device reports a WendyOS version older than 0.17.0, the OS step is refused and the command exits non-zero with:
```
this device runs WendyOS ‹version›. WendyOS 0.17.0 introduces a new update system
with no backward compatibility, so it cannot be updated over the air.
Reflash it with `wendy os install` to continue receiving updates.
```
The agent-binary update (including `--binary`) still runs and lands successfully; only the OS OTA step is blocked. To resume receiving OTA updates, reflash the device to WendyOS 0.17.0 or later with [`wendy os install`](../os/install.md). Dev builds and devices whose OS version cannot be parsed are allowed through.
## Post-update outcome
After the device is back online, `wendy device update` queries the post-reboot commit/rollback verdict from the device (the same status record that `wendy os update` and `wendy os update-status` report). If the update was rolled back, the command prints the rollback reason and exits non-zero:
```
Update failed post-reboot healthchecks and was rolled back to WendyOS-0.10.4.
Reason: wendyos-update commit failed: exit status 4 (health hook "50-containerd.sh" failed: exit status 1)
```
The first line distinguishes a healthcheck failure from an update that never
booted (the firmware fell back to the old slot, so `health.d` never ran) and from
a rollback whose reason the CLI cannot classify. See
[`wendy os update`](../os/update.md) for all three forms.
On cloud-tunneled (asset) devices the command does not wait for the reboot and does not query the outcome; it prints instructions to reconnect and re-run if needed.
## `--binary` survives the OS update
An OS update reboots into a new image that ships its own bundled agent, which would otherwise replace a `--binary` build. When `--binary` was provided and an OS update is applied, the command re-uploads the same binary after the device comes back online, so the development agent you asked for is what ends up running. (The auto-download path is not re-applied, to avoid downgrading the new image's bundled agent.) On cloud-tunneled devices the command does not wait for the reboot, so it prints instructions to re-run `device update --binary` once the device is back online.
The re-apply is verified the same way as the initial upload — by hash, which is what catches the failure this step exists for (the image's bundled agent still running instead of your binary):
- If the restarted agent reports a binary SHA-256 matching the re-uploaded binary, the command confirms the dev agent survived the OS update, including the version it reports.
- If the agent reports no hash, nothing was proven: the command prints an informational message saying the re-apply could not be verified and that the running agent may still be the OS image's bundled one. This is not treated as a failure.
## Artifact signature
`wendy device update` passes a detached **ML-DSA65** signature alongside the binary. The agent verifies the signature over the SHA256 digest of the binary before installing it.
By default no signature is sent (the verification key is not yet embedded in production builds, so the check is a fail-safe no-op and the install proceeds as before). When a signing pipeline is deployed, set `WENDY_AGENT_SIGNATURE_PATH` to the path of the detached signature file and `wendy device update` will include it automatically.
Once a real key is embedded in the agent, sending an unsigned binary — or a binary whose signature does not match — causes the agent to reject the install and return an error.
> **TODO**: On ubuntu machines, this should use `apt upgrade wendy-agent`
---
# Version
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/version/
---
title: "Version"
description: "Deprecated. wendy device version is a hidden alias for wendy device info and prints a deprecation warning. Use wendy device info instead. (wendy cloud device version is deprecated "
---
> **Deprecated.** `wendy device version` is a hidden alias for
> [`wendy device info`](info.md) and prints a deprecation warning. Use
> `wendy device info` instead. (`wendy cloud device version` is deprecated the
> same way, in favour of `wendy cloud device info`.)
Lists the OS (+version), CPU architecture and Agent version of a Wendy-enabled
device. Can be used to check for updates, but also used by
[`wendy run`](../run.md) to understand how to cross-compile correctly.
---
# List
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/volumes/list/
---
title: "List"
description: "Lists all volumes on the remote wendy device, including the combined volume size. Also lists which apps use this volume."
---
Lists all volumes on the remote wendy device, including the combined volume size. Also lists which apps use this volume.
---
# Remove
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/volumes/remove/
---
title: "Remove"
description: "Removes a volume from the remote Wendy device to clean up disk space."
---
Removes a volume from the remote Wendy device to clean up disk space.
```sh
wendy device volumes remove [name] [--force]
```
With a `[name]` argument, removes that volume. Without one, lists all volumes
(including their combined size) and lets you select one interactively.
## Flags
| Flag | Description |
|------|-------------|
| `--force` | Skip the confirmation prompt. |
---
# wendy device wifi
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/device/wifi/
---
title: "wendy device wifi"
description: "Manages WiFi on the connected WendyOS or Wendy Lite device. Run with no subcommand to open the WiFi management table on WendyOS, or start the interactive connection flow on Wendy L"
---
# `wendy device wifi`
Manages WiFi on the connected WendyOS or Wendy Lite device.
Run with no subcommand to open the WiFi management table on WendyOS, or start
the interactive connection flow on Wendy Lite.
Wendy Lite devices reached over BLE, and devices behind providers that manage
WiFi themselves, use the networks visible from the host machine because they
cannot scan from the device. The [Platform behavior](#platform-behavior)
section below describes that host-side scan.
## Subcommands
### `wendy device wifi list`
Lists WiFi networks visible to the device.
```sh
wendy device wifi list
```
### `wendy device wifi connect`
Interactively selects and connects the device to a WiFi network.
```sh
wendy device wifi connect [--ssid ]
```
Pass `--ssid` to skip the interactive network picker.
### `wendy device wifi status`
Shows the device's current WiFi connection.
### `wendy device wifi disconnect`
Disconnects the device from its current network.
### `wendy device wifi rank`
Shows or adjusts the device's network priority order.
### `wendy device wifi forget`
Removes a saved network from the device.
### `wendy device wifi regulatory`
Shows the country and available channels on a Linux WendyOS device. See
[check Wi-Fi country and available channels](#check-wi-fi-country-and-available-channels).
### `wendy device wifi regulatory set-country`
Requests a temporary country hint for the device's physical location. Query the
regulatory state again to verify the result.
---
## Platform behavior
> These notes describe the **host-side scan fallback** described above — how
> the CLI enumerates networks on the machine you are running it from. They do
> not describe scanning on a WendyOS device, which the agent performs.
### macOS
Uses CoreWLAN (`scanForNetworks`) to perform a synchronous, on-demand scan. Results are always current.
Keychain lookup is supported: previously saved "AirPort network password" entries are read automatically, so the password prompt is often skipped.
### Linux
Uses `nmcli device wifi rescan` followed by `nmcli device wifi list`. The rescan is triggered before listing, so results are always current.
Keychain lookup is not supported. The password prompt is always shown when connecting to a network that requires a password.
### Windows
Uses `netsh wlan show networks mode=bssid` to enumerate nearby networks. SSIDs are deduplicated; when a network is visible through multiple BSSIDs (roaming), the strongest signal observed across all BSSIDs is reported. Hidden networks (empty SSID) are skipped.
**Important:** `netsh` reads the Windows WLAN service's cached scan list. The service asks the driver to rescan periodically and may not scan at all while the host is already associated to a network. Results may therefore be stale. If the expected network is missing, wait a few seconds and run the command again, or pass `--ssid` to specify the network directly.
Keychain lookup is not supported. Windows has no equivalent of macOS's auto-stored "AirPort network password" Keychain entry. The password prompt is always shown when connecting to a network that requires a password.
---
## Flags
| Flag | Description |
|------|-------------|
| `--ssid ` | Skip the interactive network picker and connect to the named network directly (`connect` only). |
## Wendy Lite devices
Running `wendy device wifi` against a Wendy Lite device starts the same
interactive connection flow as `wendy device wifi connect`: select a network
visible from the host machine, enter its password, and connect the device.
You can also use the explicit subcommands:
```sh
wendy device wifi connect --ssid
wendy device wifi disconnect
```
> **Note:** `wendy device wifi connect` and `wendy device wifi disconnect` stop the running app on the device before applying the new Wi-Fi configuration. If no app is currently running, this step is skipped silently — no warning is printed.
## Check Wi-Fi country and available channels
On a Linux WendyOS device, inspect the current country and channel restrictions.
This requires a device running the agent; local and Docker providers do not
support the command.
Inspect the current state:
```sh
wendy device wifi regulatory --device
```
To request the country where the device is physically located, use its two-letter
country code. For example, for a device in the United Kingdom:
```sh
wendy device wifi regulatory set-country GB --device
wendy device wifi regulatory --device
```
This submits a temporary country hint; it does not override the radio's firmware
or guarantee more channels become available. Changes can take time, so query
again to check the result. The hint does not survive a reboot and does not restart
Wi-Fi, Wi-Fi Aware, or the agent. Devices need `iw` installed; older or non-Linux
agents may report that this operation is unsupported.
### Detailed output and automation
Add `--json` for the full channel report. It includes the kernel's country and
per-radio restrictions, maximum transmit power, permitted widths, and radar-related
restrictions where available. Use `globalCountry` for the kernel hint and
`phys[].country` and `phys[].selfManaged` to distinguish each radio's own state.
A self-managed radio may keep a different country from the kernel hint. `no IR` means the radio cannot initiate transmission
on that channel. Raw output is retained as `rawRegulatory`, `rawPhy` and
`rawChannels` for diagnosis.
---
# wendy discover
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/discover/
---
title: "wendy discover"
description: "Discovers local and Wendy Cloud devices in a tabbed terminal interface."
---
# `wendy discover`
Discovers local and Wendy Cloud devices in a tabbed terminal interface.
## Usage
```sh
wendy discover [flags]
```
## Description
Without output or timeout flags, `wendy discover` opens a live TUI with three
tabs:
- **Nearby** discovers devices over LAN, USB, Bluetooth, and external providers.
- **Simulator** lists local simulator devices.
- **Cloud** lists online devices enrolled in the active Wendy Cloud
organization.
Use `tab` or `shift+tab` to switch tabs. Cloud discovery starts lazily on the
first visit to the Cloud tab, so the command makes no cloud connection if you
only use Nearby discovery.
Local discovery combines these mechanisms and merges their results:
- **Ethernet (USB NCM) discovery** — enumerates host network adapters and
returns those whose name or interface description contains "wendy"
(case-insensitive).
- **LAN discovery** — uses mDNS/Bonjour to find WendyOS devices and Headless Mac targets advertising themselves on the local network.
## Platform support
### LAN (mDNS) discovery
mDNS discovery works on all platforms. On Linux, the CLI performs an mDNS browse
that requires UDP port 5353 open on the host firewall (e.g., `sudo ufw allow 5353/udp`).
On macOS, the CLI browses through mDNSResponder in-process and requires Local Network TCC permission.
For a USB-C-connected device on Linux, `wendy discover` notices when the host's
link to it isn't configured yet and offers to set it up before scanning (it needs
sudo). The offer only appears in an interactive terminal; with `--json` or
without a terminal, `wendy discover` prints a notice on stderr instead.
Headless Mac advertises the same `_wendyos._udp` service. When discovery
succeeds, Mac agents appear under `lanDevices` in JSON output with
`"os": "darwin"`. For automation, prefer an explicit target such as
`--device {hostname}:50051`, because discovery can be blocked by network policy
or macOS permissions.
## Local run targets
By default `wendy discover` hides **local run targets** — this machine,
Docker/OrbStack, and Apple Container — so the table shows only separate WendyOS
devices. Set `WENDY_SHOW_LOCAL_DEVICES=1` to include them:
```sh
WENDY_SHOW_LOCAL_DEVICES=1 wendy discover
```
> **Note:** JSON output (`wendy discover --json`) always includes local run
> targets regardless of `WENDY_SHOW_LOCAL_DEVICES`, so scripts and MCP callers
> continue to receive the full set.
## Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--type` | `all` | Local discovery type: `usb`, `lan`, `bluetooth`, `external`, or `all`. Does not affect the Cloud tab. |
| `--timeout` | `5s` | Scan local devices once for this duration, print the results, and exit. When omitted, the live tabbed TUI runs until quit. |
| `--json` | `false` | Output discovery results as JSON instead of a table |
## Environment variables
| Variable | Description |
|----------|-------------|
| `WENDY_SHOW_LOCAL_DEVICES` | When truthy (`1`/`true`/`yes`/`on`), include local run targets (this machine, Docker/OrbStack, Apple Container) in the table. JSON output always includes them regardless. |
## Interactive TUI
Without `--json` or an explicitly set `--timeout`, discover renders Nearby, Simulator, and
Cloud tables that refresh as devices come and go. A leading `✦` marks the
current default device or organization.
### Keyboard shortcuts
| Key | Action |
|-----|--------|
| `tab` / `shift+tab` | Switch between Nearby, Simulator, and Cloud tabs |
| `↑` / `↓` | Navigate the active device list |
| `enter` | Copy the selected device as JSON; when logged out in Cloud, start login |
| `a` | Copy all devices in the active tab as JSON |
| `u` | Update the selected device's agent |
| `d` | Make the selected device the default; press it again on the default to clear it |
| `s` / `x` | Start or stop the selected VM in Simulator |
| `c` / `r` | Create a VM, or remove the selected one and its disk (asks first), in Simulator |
| `o` | Switch the active organization in Cloud |
| `q` / `Ctrl+C` | Quit |
### Nearby tab
The Nearby tab shows devices found through the enabled local discovery
transports. The `--type` flag filters this tab only.
### Cloud tab
The Cloud tab shows online, cloud-enrolled devices for the active organization.
Its header includes the organization name and ID and marks the persisted
default organization with `✦ default`.
If no cloud credentials are stored, the Cloud tab displays `Wendy Cloud login —
Not logged in`. Press `enter` to start browser-based login; after login succeeds,
discovery restarts with the Cloud tab available. Press `tab` to return to Nearby
or `q` to quit without logging in.
### Switching organizations (`o`)
Press `o` in the Cloud tab to choose from all organizations available through
the stored sessions, including organizations for which this machine does not
yet have credentials.
- An organization with stored credentials becomes active immediately.
- An organization without local credentials starts browser-based login against
its cloud environment. After login, the CLI verifies that credentials for the
selected organization were stored; if not, it reports an error and you can
repeat the flow.
### Nearby table columns
LAN devices appear after their mDNS service resolves or their agent answers a
probe. Cached devices are checked in the background and stay hidden until
confirmed during the current scan.
| Column | Description |
|--------|-------------|
| Name | Device display name |
| Type | Transport(s) the device was discovered on (LAN, USB, BLE, …) |
| Address | IP address (or hostname) and port |
| Agent | Running agent version; `⚠` marks an agent older than the CLI; blank when the metadata probe hasn't succeeded |
| OS | OS version reported by the agent |
| Provisioned | `Provisioned` or `Unprovisioned` for LAN devices, from the mDNS-advertised mTLS state; blank for transports that don't report it (BLE-only, USB, external providers) |
### Connection hints
If the CLI cannot verify a device's agent, select its row to see what went
wrong and what to try next.
### Clipboard JSON
Pressing `enter` copies the highlighted device as a JSON object; `a` copies all
devices as a JSON array. Each object contains:
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Device display name |
| `type` | string | Transport(s), e.g. `LAN` or `USB, LAN` |
| `usb` | string | USB interface summary (omitted when not connected over USB) |
| `address` | string | IP address (or hostname) and port |
| `version` | string | Agent version (omitted when unknown) |
| `provisioned` | string | `Provisioned` or `Unprovisioned` for LAN devices (omitted for other transports) |
| `probeFailure` | object (optional) | Details of a failed connection and suggested recovery steps |
When provided, `probeFailure` contains a `code` identifying the failure, a
`message` explaining it, and optional `next_steps` listing suggested actions.
The same object is included in `--json` output.
---
# wendy doctor
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/doctor/
---
title: "wendy doctor"
description: "Check your setup and get a local diagnostic report with suggested fixes."
---
# `wendy doctor`
Check your setup and get a local diagnostic report with suggested fixes.
```sh
wendy doctor
```
To print a sanitized JSON report that you can review and share:
```sh
wendy doctor --report
```
The report goes to standard output. Wendy does not upload it or create a report
file. To save it yourself:
```sh
wendy doctor --report > wendy-report.json
```
## Choose what to check
Doctor recognizes a project in the current directory, or you can select one:
```sh
wendy doctor --project ./my-app --report
```
It checks the target chosen by `--device`, then `WENDY_DEVICE`, then the saved
default device. To check a particular target:
```sh
wendy doctor --device my-device.local
wendy doctor --device vm:my-running-vm --report
wendy doctor --device docker
```
Doctor does not scan for devices or start stopped VMs. Named VM checks read the
existing state and probe its recorded local endpoint. Use the actual VM name
printed by the tour. An unnamed `sim` or `simulator` target is reported as
unknown because selecting or creating a VM would change state.
## Checks and results
Doctor checks project configuration and build inputs, the Docker executable,
daemon and Buildx plugin, QEMU and ARM64 firmware, and the selected target's
connection. Project validation does not execute project commands or hooks.
Docker checks only run `docker info` and `docker buildx version`.
Docker is required for a recognized container project, the local Docker target,
or a VM/simulator target. QEMU and firmware are required for VM/simulator targets.
Without that context, these tools are optional. A recognized project or explicit
`--project` is required to pass static validation.
| Status | Meaning |
| --- | --- |
| `pass` | This check succeeded. |
| `fail` | This check found a problem. Read its suggested fix. |
| `unknown` | The check lacks enough context or evidence. |
| `not_required` | This check does not apply to the selected route. |
Each JSON check has an `id`, `status`, `required`, stable `code`, and a sanitized
`message`. Failed checks include a `remediation` when available. The report also
includes `schema_version`, CLI version, host OS and architecture, and `ok`.
The command exits with code 1 when a required check fails or the diagnostic
deadline expires. Optional missing tools and unknown context do not fail the
command. `ok: true` means no required check failed. It does not mean a device has
been installed, an app has run, or authentication has been verified.
The default overall deadline is 20 seconds. Individual subprocess and device
checks have a three-second limit. You can change the overall deadline up to one
minute:
```sh
wendy doctor --timeout 30s --report
```
## Authentication and privacy
Doctor can make a TCP connection and read-only agent version and provisioning
requests to the selected local endpoint. A reachable endpoint alone does not
verify device identity. An enrolled device may require credentials that doctor
does not use, so its agent or authentication result may remain unknown.
Cloud targets are not probed. Doctor does not use or refresh saved credentials.
Authentication is `not_required` for the local Docker target or when a responding
local agent explicitly reports that it is not enrolled. Doctor never infers that
saved authentication is valid.
Reports omit target names and addresses, local paths, project and app names,
project source, credentials, and subprocess output. Error messages and suggested
fixes come from fixed product text, not arbitrary logs or validation errors.
Doctor does not install or update software, modify configuration, deploy apps,
start VMs, or move robots. To install a missing QEMU or ARM64 firmware, run
`wendy run` or `wendy tour` in a terminal; they offer to install it. CLI
telemetry and routine update or configuration housekeeping are skipped for this
command.
---
# wendy init
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/init/
---
title: "wendy init"
description: "Creates a new Wendy project: a wendy.json, scaffolded source files, and optionally an AI assistant session."
---
# `wendy init`
Creates a new Wendy project: a [`wendy.json`](../../../apps/wendy.json.md), scaffolded source files, and optionally an AI assistant session.
Run it with no arguments for an interactive wizard, or pass flags to script it end to end. Every prompt has a matching flag, so any wizard answer can be supplied up front.
## Usage
```sh
# Interactive wizard
wendy init
# Create ./my-app and scaffold into it
wendy init my-app
# Already inside an empty my-app/ — scaffold here instead of nesting ./my-app/my-app
wendy init --here my-app
# Fully non-interactive
wendy init \
--app-id my-app \
--target wendyos \
--language swift \
--no-extra-entitlements \
--assistant skip
```
An `[app-id]` argument (or `--app-id`) always creates a new subdirectory of that name. With neither, the wizard offers to scaffold into the current directory instead. Pass `--here` to scaffold into the current directory directly instead of nesting a subdirectory — with an app ID it still writes that as `wendy.json`'s app ID; without one, it infers the app ID from the current directory's name.
## What it produces
1. Picks a **deployment group** within the Linux, Mac, or Microcontrollers tab. A concrete `--template ` resolves first instead: when the template supports exactly one target, the question is skipped and the target is inferred with a notice; when it supports several, the picker is narrowed to those.
2. Shows the **projects** for that deployment group. Select a project template (`--template`), or choose **No template** to continue with manual setup.
3. Picks the **language**. Skipped with a notice when the chosen template offers exactly one language.
4. Collects **entitlements** — the capabilities the app needs. See [Entitlements](../../../apps/wendy.json.md#entitlements-1).
5. Collects **framework** configuration — currently ROS 2. See [Frameworks](#frameworks).
6. Writes `wendy.json` and scaffolds the project files.
7. Optionally launches an AI assistant (`--assistant`).
## Targets
The interactive picker has Linux, Mac, and Microcontrollers tabs, matching `wendy install`. Press Tab or Shift+Tab to switch tabs. Under Linux, choose Edge AI, Unitree G1, Unitree Go2, or ROSMaster to see that group's projects. Edge AI contains general Linux edge applications. Robot groups appear when the catalog has projects for them. Mac and Microcontrollers offer macOS and ESP32 projects.
Press Enter to open a group, then select a project. **Back to deployment groups** returns to the group picker. **No template** starts manual setup. A bare `--template` also uses the groups, with projects limited to `--target` when supplied. A concrete `--template ` skips group browsing.
Catalog authors can set a Linux template's optional `deployment` field in `meta.json` to `edge-ai`, `unitree-g1`, `unitree-go2`, or `rosmaster`. Existing catalogs work without this field: `g1-*` and `go2-*` projects go into their robot groups, `rc-car` and `rosmaster-*` projects go into ROSMaster, and other Linux projects go into Edge AI.
| `--target` | `platform` written to `wendy.json` | Languages |
|---|---|---|
| `wendyos` | `linux` | `swift`, `python` |
| `darwin` (aliases: `mac`, `macos`) | `darwin` | `swift` (plain wizard), `mojo` (via templates) |
| `wendy-lite` | `wendy-lite` | `c` (native), `swift` (WASM) |
Templates may offer additional languages (for example `rust`, `node`, or `cpp`) on a `wendyos` target; the plain wizard writes `swift` or `python`. On a `darwin` target the plain wizard writes `swift`; `mojo` is accepted when a catalogued Mojo/MAX template such as `mac-llm` provides it.
**Language resolution.** `wendy-lite` accepts `c` for native apps and `swift` for WASM apps. `darwin` accepts `swift` or `mojo`; with a template, the language must be one the template offers, and an unsupported choice is rejected with the languages it does offer.
For ESP32, regular native ESP-IDF projects are recommended. Create them with the standard ESP-IDF tooling and add a `wendy.json` whose platform is `wendy-lite`; you do not need `wendy init`. The `wendy-lite` wizard target remains available for scaffolding an optional Swift/WASM guest.
## Flags
### Project flags
| Flag | Description |
|---|---|
| `--app-id ` | Application ID written to `wendy.json`. Creates a subdirectory of that name. |
| `--here` | Scaffold into the current directory instead of creating a subdirectory. With no `[app-id]`/`--app-id`, infers the app ID from the current directory's name. |
| `--target ` | Target platform: `wendyos`, `darwin`, or `wendy-lite`. Required when running non-interactively. |
| `--language ` | Project language. `swift` or `python` for the plain wizard (`swift` on `darwin`); templates may offer more, including `mojo` on `darwin`. |
| `--git-init ` | Initialize a git repo in the project directory. |
### Template flags
| Flag | Description |
|---|---|
| `--template []` | Scaffold from a template. Passing it **without** a value opens a picker filtered by target, auto-selecting when only one template exists for the target. With a value, `--target` becomes optional: it is inferred when the template's metadata maps to exactly one target. |
| `--branch ` | Branch of the templates repo to read templates from. |
| `--var KEY=VALUE` | Override a template variable. Repeatable. |
### Entitlement flags
| Flag | Description |
|---|---|
| `--entitlement ` | Entitlement to enable. Repeatable or comma-separated. |
| `--all-entitlements` | Enable every entitlement. Requires the field flags below for `gpio`, `i2c`, and `persist`. |
| `--no-extra-entitlements` | Skip the entitlement prompts and write only the default `network` entitlement. |
| `--gpio-pins 17,27,22` | Pins for the `gpio` entitlement. |
| `--i2c-device /dev/i2c-1` | Device path for the `i2c` entitlement. |
| `--persist-name ` | Storage namespace for the `persist` entitlement. |
| `--persist-path /data` | Mount path for the `persist` entitlement. |
Passing any entitlement flag suppresses the interactive entitlement prompts entirely — the flags become the complete answer.
### Framework flags
| Flag | Description |
|---|---|
| `--framework ` | Framework to enable under `wendy.json`'s top-level `frameworks` key. Repeatable or comma-separated. Currently accepts `ros2`. |
| `--ros2-domain-id ` | ROS 2 domain ID, `0`–`232`. Default: derived as a stable hash of the app ID. |
| `--ros2-rmw ` | ROS 2 middleware: `cyclonedds` (default), `fastrtps`, `connextdds`, or `gurumdds`. |
| `--ros2-distro ` | ROS 2 distribution, e.g. `humble` (default) or `jazzy`. Lowercase letters and digits, starting with a letter. |
| `--ros2-scope ` | Who can see the app's topics: `app` (only this app), `device` (default, every app on the device), `peripherals` (the device and hardware attached to it, such as a robot), or `network` (the device's networks). |
| `--ros2-discovery-scope ` | Deprecated, use `--ros2-scope`. `app` writes scope `device`, `host` writes scope `network`. |
See [Frameworks](#frameworks) below.
### AI assistant flags
| Flag | Description |
|---|---|
| `--assistant ` | Launch an assistant in the new project after scaffolding. `claude` and `codex` must be on `PATH`. |
| `--install-claude-skills` | Install the Wendy Claude skills before launching Claude. Requires `--assistant=claude`. |
## Frameworks
`wendy.json` has a top-level [`frameworks`](../../../apps/wendy.json.md#frameworks) key, separate from `entitlements`, for framework-level configuration. ROS 2 is the only framework today.
`wendy init` fills it in three ways:
**Flags.** Pass `--framework ros2`, or any `--ros2-*` flag on its own (which implies `--framework ros2`):
```sh
wendy init \
--app-id go2-network-bridge \
--target wendyos \
--language swift \
--framework ros2 \
--ros2-rmw cyclonedds \
--ros2-scope peripherals \
--assistant skip
```
This writes:
```json
{
"frameworks": {
"ros2": {
"rmw": "cyclonedds",
"scope": "peripherals"
}
}
}
```
Values are validated against the same rules `wendy.json` itself enforces, so a typo fails immediately at `init` rather than surfacing later from `wendy run` or `wendy device ros2`.
**Interactively.** On a `wendyos` target, when no entitlement flags and no framework flags were passed, the wizard asks whether the app uses ROS 2 and then walks through middleware, scope, and domain ID (not asked for scope `app`, which uses the app's own domain). The distro is left at its default; edit `frameworks.ros2.distro` in `wendy.json` to change it.
**By hand.** Add the `frameworks` key to `wendy.json` yourself at any point; `wendy run` picks it up on the next deploy.
Frameworks are WendyOS-only. `--framework ros2` with `--target darwin` or `--target wendy-lite` is rejected — neither target runs the ROS 2 container image.
### With `--template`
Entitlement and framework flags are merged into the template's own `wendy.json` after it is scaffolded, and in both cases **the template's configuration wins where it is more specific**:
- `--entitlement` values already covered by the template are skipped; the rest are appended.
- `--framework`/`--ros2-*` are written only if the template does not already configure `frameworks`. A template that ships its own `frameworks` block keeps it, and the flags are dropped. A `frameworks` key that configures nothing (`null`, `{}`, or an object whose members are all `null`) does not count as configured, so the flags still apply.
`wendy init` reports what it merged in its summary (`Frameworks added from flags: ros2`).
## Non-interactive and headless use
`wendy init` detects whether a real terminal is attached. In CI, in scripts, and under AI agents there is none, so the interactive pickers are skipped rather than failing on a TTY that cannot be opened. Instead the CLI prints the choices as plain text and exits with an error naming the flag to pass:
- **No `--target`** — prints the available targets, then errors with `--target is required when running non-interactively`. With `--template `, the target is inferred when the template's metadata maps to exactly one target; otherwise the error lists only that template's targets.
- **Bare `--template` with no value** — prints the templates available for the selected target, then errors with `--template requires a value when running non-interactively`. If the target has exactly one template it is auto-selected instead; if it has none, the error is `no templates available for `.
- **Template with several languages and no `--language`** — prints the languages available for the template, then errors with `--language is required when running non-interactively`. A single-language template is auto-selected with a notice.
- **No app ID** — errors with `an app ID is required when running non-interactively`; pass `--app-id` or the `[app-id]` argument.
So discovering the valid values is a matter of running the command and reading the list:
```sh
$ wendy init
Available targets:
WendyOS - Full Linux-based edge device (Jetson, Raspberry Pi, ...)
macOS - Native macOS app deployed to Wendy Agent for Mac
ESP32 - Microcontroller running WASM (ESP32)
Error: --target is required when running non-interactively (valid: wendyos, darwin, wendy-lite)
$ wendy init --app-id my-app --target wendyos --template
Available templates for wendyos:
simple-api - Minimal HTTP API
fullstack - Web UI plus API
Error: --template requires a value when running non-interactively; pass --template= using one of the templates listed above
```
For a fully scripted run, supply every choice as a flag. `--no-extra-entitlements` and `--assistant skip` are what suppress the remaining prompts:
```sh
wendy init \
--app-id my-app \
--target wendyos \
--language swift \
--no-extra-entitlements \
--assistant skip
```
## Examples
```sh
# Scaffold from a template, picking the language interactively
wendy init --template simple-api
# Target and language inferred from the template's metadata
# (go2-rc supports only WendyOS + Python, so neither is asked)
wendy init go2-app --template go2-rc
# Non-interactive template scaffold with a variable override
wendy init --app-id my-api --template simple-api --language rust --var PORT=8080
# A template from a branch of the templates repo
wendy init --template simple-api --branch feature/new-template
# WendyOS Python app with persistent storage
wendy init \
--app-id demo-app \
--target wendyos \
--language python \
--entitlement gpu,usb,persist \
--persist-name demo-data \
--persist-path /data \
--assistant skip
# WendyOS app with GPIO and I2C
wendy init \
--app-id edge-sensors \
--target wendyos \
--language swift \
--entitlement gpio,i2c \
--gpio-pins 17,27,22 \
--i2c-device /dev/i2c-1 \
--assistant skip
# Optional Wendy Lite Swift/WASM scaffold
wendy init \
--app-id lite-app \
--target wendy-lite \
--no-extra-entitlements \
--assistant skip
# Native macOS app for Wendy Agent for Mac
wendy init \
--app-id mac-llm \
--target darwin \
--language swift \
--template mac-llm \
--assistant skip
# The same template as a native Mojo/MAX app (run.command on the Mac agent)
wendy init \
--app-id mac-llm \
--target darwin \
--language mojo \
--template mac-llm \
--assistant skip
# ROS 2 app that talks to a robot attached to the device
wendy init \
--app-id go2-robot-app \
--target wendyos \
--language swift \
--framework ros2 \
--ros2-rmw cyclonedds \
--ros2-scope peripherals \
--assistant skip
# Start Claude in the new project with the Wendy skills installed
wendy init \
--app-id ai-app \
--target wendyos \
--language python \
--entitlement gpu,audio \
--assistant claude \
--install-claude-skills
```
## Related
- [`wendy.json`](../../../apps/wendy.json.md) — every field `wendy init` can write.
- [`wendy project entitlements add`](project/entitlements/add.md) — add entitlements after init.
- [Wendy for ROS 2](../../../../../integrations/ros2/) — inspect and debug the ROS 2 app you just scaffolded.
- [`wendy run`](run.md) — build and deploy the new project to a device.
---
# wendy install
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/install/
---
title: "wendy install"
description: "Installs WendyOS onto an NVMe or SD card, fully recovers supported Jetsons over USB, flashes Wendy Lite firmware onto an ESP32, or prints agent.sh install instructions for an exist"
---
# `wendy install`
Installs WendyOS onto an NVMe or SD card, fully recovers supported Jetsons over USB, flashes Wendy Lite firmware onto an ESP32, or prints `agent.sh` install instructions for an existing Linux machine or Mac.
`wendy install` is the surfaced, top-level alias for [`wendy os install`](os/install.md). It is the recommended entry point: the underlying `wendy os` group is hidden from the main help, but the install flow is the most common first step when bringing up a device, so it is promoted to a first-class command.
The two commands are the **same command** — they accept identical flags and arguments and behave identically. `wendy os install` remains available for backward compatibility and for discoverability under the `wendy os` group.
The interactive picker has Linux, Mac, and Microcontrollers tabs. Press Tab or Shift+Tab to switch categories, use the arrow keys to choose a device, and press Enter to select it. Linux includes WendyOS devices and Linux Desktop; Mac offers Headless Mac; Microcontrollers lists ESP32 targets with published Wendy Lite firmware. Empty categories are hidden.
```sh
# Interactive (recommended)
wendy install
# Install nightly firmware
wendy install --nightly
# Linux: non-interactive with all flags
wendy install --device-type raspberry-pi-5 --version 0.10.4 --drive /dev/disk4 --force
# Direct install from a local image (Linux only)
wendy install path/to/image.img /dev/disk4 --force
# Install wendy-agent on an existing Linux machine or Mac (prints a one-liner)
wendy install --device-type linux-desktop
wendy install --device-type headless-mac
# Same, with pre-enrollment into your org (requires `wendy auth login`)
wendy install --device-type linux-desktop --pre-enroll
```
## Reference
See [`wendy os install`](os/install.md) for the complete reference: the ESP32 (Wendy Lite) flash path, the Linux (WendyOS) write path, WiFi pre-configuration, pre-enrollment, exit-code semantics, and the full flags table.
## Linux Desktop / Headless Mac
Selecting **Linux Desktop** or **Headless Mac** in the interactive picker (or passing `--device-type linux-desktop` / `--device-type headless-mac`) does not write any drive. Instead it prints a `curl … | bash` one-liner for `agent.sh`, which auto-detects the platform. The Linux command adds `--network-access network` so the machine can be discovered and managed from your computer; for a Mac, choose **Allow Other Devices** when Wendy Agent first opens. When you are logged in and confirm (or pass `--pre-enroll`), it mints a short-lived (1 h) enrollment token and embeds it in the command so the freshly installed `wendy-agent` self-enrolls on first startup. Without a token the device is discovered over mDNS as described in the [Linux installation guide](../../../../../installation/linux/).
## Related
- [`wendy os install`](os/install.md) — full installation reference (same command)
- [`wendy os download`](os/download.md) — pre-download a WendyOS image into the cache
- [`wendy device setup`](device/setup.md) — provision a device after first boot
---
# Schema
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/json/schema/
---
title: "Schema"
description: "wendy json schema outputs the JSON Schema definition of wendy.json files into stdout."
---
`wendy json schema` outputs the JSON Schema definition of [wendy.json](../../../../apps/wendy.json.md) files into `stdout`.
[VSCode](../../../../vscode/) uses this schema for validating the JSON file, and providing editor support with autocompletion.
---
# Validate
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/json/validate/
---
title: "Validate"
description: "Validates wendy.json files as found in the current working directory."
---
Validates `wendy.json` files as found in the current working directory.
---
# Cache
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/os/cache/
---
title: "Cache"
description: "Alias to wendy cache."
---
Alias to [`wendy cache`](../../cache/).
---
# wendy os cache list
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/os/cache/list/
---
title: "wendy os cache list"
description: "Lists cached WendyOS OS images stored locally."
---
# `wendy os cache list`
Lists cached WendyOS OS images stored locally.
## Usage
```sh
wendy os cache list [--json]
```
## Description
`wendy os cache list` shows OS image files that have been downloaded to the local cache by `wendy install` or `wendy os download`. Cached images are reused on subsequent installs to avoid re-downloading.
Pass `--json` to receive a JSON array on stdout instead of the human-readable output described below.
## Human-readable output
When `--json` is **not** passed, each cached image is printed one per line with its size in MiB:
```text
wendyos-raspberry-pi-5-0.10.4.img (4608.0 MB)
...
Cache directory: /Users/you/Library/Caches/wendy/os-images
```
When no cached images are found (or the cache directory does not exist), the command prints:
```text
No cached OS images.
```
## JSON output (`--json`)
Each element of the returned array represents one cached OS image file:
```json
[
{
"name": "wendyos-raspberry-pi-5-0.10.4.img",
"sizeBytes": 4831838208,
"size": "4.5 GB"
}
]
```
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Filename of the cached OS image |
| `sizeBytes` | integer | Size in bytes |
| `size` | string | Human-readable size string |
Returns `[]` when no cached OS images are found.
> **Note:** Unlike `wendy cache list --json`, this output does not include a `path` field.
## Error handling
If reading the metadata for a cache entry fails, the command exits with an error rather than silently skipping the entry.
## Related
- [`wendy cache list`](../../cache/list.md) — lists all cached items, including OS images
- [`wendy install`](../install.md) — installs WendyOS and populates the cache
- [`wendy os download`](../download.md) — downloads WendyOS images into the cache
---
# Download
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/os/download/
---
title: "Download"
description: "Downloads an image for WendyOS or Wendy Lite to your developer machine."
---
Downloads an image for [WendyOS](../../../../wendyos/) or [Wendy Lite](../../../../wendy-lite/) to your developer machine.
---
# wendy install
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/os/install/
---
title: "wendy install"
description: "Installs WendyOS onto an NVMe or SD card, fully recovers supported Jetsons over USB, or flashes Wendy Lite firmware onto an ESP32."
---
# `wendy install`
Installs WendyOS onto an NVMe or SD card, fully recovers supported Jetsons over USB, or flashes Wendy Lite firmware onto an ESP32.
> **Tip:** [`wendy install`](../install.md) is the recommended, surfaced entry point for this command. `wendy os install` remains available and behaves identically — it is kept for backward compatibility and for discoverability under the `wendy os` group.
The command presents a device picker with Linux, Mac, and Microcontrollers tabs. Press Tab or Shift+Tab to switch categories, then use the arrow keys and Enter to select a device. Linux includes WendyOS targets and Linux Desktop, Mac offers Headless Mac, and Microcontrollers lists ESP32 targets with published firmware. Empty categories are hidden. Select the device type to take the appropriate path:
- **Jetson Orin Nano / AGX Orin** -> download a recovery flashpack -> verify the module/carrier -> update QSPI and NVMe/eMMC together
- **Raspberry Pi targets** -> download OS image -> write to SD/NVMe -> write config partition
- **Jetson AGX Thor** -> download flashpack -> boot over USB recovery -> flash QSPI and internal NVMe
- **ESP32 targets** → detect USB serial port → download firmware `.bin` → flash over serial
```sh
# Interactive (recommended)
wendy install
# Install nightly firmware
wendy install --nightly
# Linux: non-interactive with all flags
wendy install --device-type raspberry-pi-5 --version 0.10.4 --drive /dev/disk4 --force
# Jetson AGX Thor: flash over USB recovery (macOS, Linux, and Windows)
wendy install --device-type jetson-agx-thor
# Orin Nano: full QSPI + NVMe recovery (macOS or Linux)
wendy install --device-type jetson-orin-nano
# AGX Orin: storage is mandatory non-interactively
wendy install --device-type jetson-agx-orin --storage nvme
wendy install --device-type jetson-agx-orin --storage emmc
# Explicit legacy raw-media write; QSPI is not updated
wendy install --device-type jetson-orin-nano --rootfs-only --drive /dev/disk4
# Direct install from a local image (Linux only)
wendy install path/to/image.img /dev/disk4 --force
```
> **Note:** `--device-type` is not supported for ESP32 targets. Use the interactive picker to flash an ESP32.
---
## Install a pull-request build
```sh
wendy install --pr 123
```
Downloads and flashes the WendyOS image built by wendyos-builder PR #123.
PR images are **debug builds**: SSH is enabled, root login is passwordless,
and the serial console is active. They are for testing the PR on hardware —
**never flash a PR image to a production device.** Artifacts are deleted when
the PR is closed.
`--pr` is supported for Linux disk-image devices, for Jetson recovery (Orin
Nano/AGX and AGX Thor) and for the Dragonwing EDL flash. PR builds publish
recovery flashpacks into the `pr//` sandbox, so `--pr` can drive a full
recovery install (QSPI+storage for Orin, QSPI+NVMe for Thor) as well as
`--pr --rootfs-only` raw imaging on Orin. It is not supported for ESP32
targets (Wendy Lite firmware is not built by the per-PR pipeline).
`--pr` is mutually exclusive with `--nightly`, `--version`, and a positional
image path.
---
## ESP32 (Wendy Lite) path
### 1. Device detection
The CLI scans for either the ESP32's native USB Serial/JTAG interface or the Silicon Labs CP210x USB-to-UART bridge used on many Espressif development boards:
| Platform | Expected path |
|----------|---------------|
| macOS | `/dev/cu.usbmodem*` or `/dev/cu.usbserial*` |
| Linux | `/dev/ttyACM0` or `/dev/ttyUSB0` (typical) |
| Windows | `COMN` (e.g. `COM7`) |
When both transports are connected, the installer always prefers native USB. A CP210x ID identifies a generic UART adapter rather than a confirmed ESP32, so the CLI treats it as an install candidate and verifies the chip model through the ROM bootloader before writing firmware.
UART is a flashing transport only: Wendy Lite's runtime USB connection uses the ESP32's native USB Serial/JTAG interface. The installer warns when it selects UART and, when no Wi-Fi network is configured in an interactive install, requires explicit confirmation before continuing.
If no device is found, the CLI prints instructions for entering bootloader mode:
```
No ESP32 device detected.
Make sure your ESP32 is connected via USB and in bootloader mode.
To enter bootloader mode: hold the BOOT button, press RESET, then release BOOT.
```
### 2. Firmware resolution
Firmware versions are served from the same manifest used for WendyOS images. With `--nightly`, the latest nightly build is used instead of the latest stable.
Boards without published firmware are omitted from the picker. Immediately before any chip-specific initialization or flash erase, the ROM bootloader's detected chip model is checked against the selected board; a mismatch is rejected before firmware is written. Wendy Lite currently supports ESP32-C5, C6, C61, P4, and S3 targets.
### 3. Serial flash protocol
The CLI implements the ESP32 ROM bootloader protocol directly over the USB serial port — no `esptool` dependency required. It supports both connection styles (a chip's native USB port and a CP210x USB-to-UART bridge), selecting the correct reset sequence for each. Progress is reported as blocks written / total blocks in the terminal.
### 4. Post-flash
The device reboots automatically using the reset sequence appropriate to its native USB or UART transport. Before flashing, the CLI embeds the selected WiFi credentials, device name, and pre-enrollment state into the firmware image's `wendy_conf` partition.
To provision WiFi after first boot, use `wendy device wifi connect`.
---
## Linux (WendyOS) path
For Raspberry Pi devices—and Orin with `--rootfs-only`, the interactive flash-mode choice "OS image only", or a version that predates recovery flashpacks—the install path writes a disk image to a selected SD card, NVMe drive, or USB-attached enclosure:
1. **Resolve version** — `--version` if provided, otherwise latest (or nightly with `--nightly`).
2. **Resolve drive** — `--drive` if provided, otherwise an interactive picker of external drives. Internal drives require `--yes-overwrite-internal` in non-interactive mode; in interactive mode the user must type the device path to confirm.
3. **Download image** — fetched from GCS with a progress bar. Downloaded to `~/Library/Caches/wendy/os-images/` (macOS) or `~/.cache/wendy/os-images/` (Linux). Zip archives are streamed through to the first `.img`, `.raw`, `.wic`, or `.sdimg` entry; gzip-compressed images (`.img.gz`, detected by magic bytes regardless of extension) are decompressed and streamed on the fly. Seekable-zstd images (`.img.zst`) are downloaded and cached directly; when a block map is present, only mapped ranges are decoded during the write step, skipping hole frames entirely. Parallel download is used when the server supports HTTP range requests.
4. **Write image** — `dd`-equivalent write with elevated privileges (`sudo` on Unix, UAC on Windows), progress bar. A drive too small for the image is refused before anything is written. An image with a GPT partition table (such as the Orin NVMe image) only needs a drive that holds its partitions, and its partition table is rewritten for the drive's real size after the write; see [Drive-size check](../../../../../../wendyos/flashing/#drive-size-check). When a block map is used and the bmap write fails (e.g. checksum mismatch or a stale/incorrect published bmap), the CLI automatically falls back to a full sequential write using the already-cached `.img.zst` or `.zip` — no re-download is required. A failure *during* the fallback write is fatal.
5. **Write config partition** — downloads the latest stable `wendy-agent-linux-arm64` binary from GitHub, writes it along with any pre-seeded WiFi credentials and device name to the config partition on the newly written drive. Skipped silently on platforms that don't support config-partition writes. The OS image is already on the drive by this point, and the device boots regardless of this step — it runs the agent baked into the image and fetches updates and configuration after first boot. When provisioning was **not** explicitly requested, a failure here is advisory: a warning is printed and the install is still reported as successful. When `--wifi`, `--device-name`, or `--pre-enroll` **was** requested and could not be applied, the install exits non-zero (see the exit-code note below). On an interactive terminal the CLI offers to retry the write after each failed attempt (useful after, e.g., re-seating an SD card whose config partition couldn't be located); non-interactively it prints guidance and does not retry.
6. **Eject** — the drive is ejected automatically after writing.
> **Exit code:** `wendy install` exits `0` when the OS image was written to the drive and any explicitly requested provisioning (`--wifi`, `--device-name`, `--pre-enroll`) was applied. It exits non-zero when the image itself could not be written, or when provisioning was explicitly requested but could not be applied — in the latter case the drive is still ejected first and the OS image is already safely on it, and the error names the image, version, and drive to make that clear, so the values can be re-applied with another `wendy install` or configured after the device boots. Unrequested provisioning (e.g. a failed agent-binary download when none of those flags were given) remains advisory: a warning is printed and the command exits `0`.
> **Provisioning retry:** When the config-partition write fails on an interactive terminal, the CLI asks `Retry writing provisioning data to the config partition?` after each attempt. Answering yes re-attempts the write (download + config-partition write); answering no, or running non-interactively, prints guidance and stops retrying — the command then exits non-zero if provisioning was explicitly requested (`--wifi`, `--device-name`, `--pre-enroll`), and successfully otherwise. The OS image is already on the drive either way.
## Jetson Orin full recovery path
New Orin releases default to full USB recovery on macOS, Linux, and Windows. Supported hardware is intentionally exact:
- Orin Nano P3767-0005 on P3768-0000, NVMe.
- AGX Orin P3701-0005 on P3737-0000, NVMe or eMMC.
The CLI RCM-boots a signed recovery initrd, correlates its mass-storage LUNs to the selected physical USB port and session, and reads `device.json` before any persistent write. A module/carrier mismatch aborts before the flash-package handoff. It then writes/ejects the flash package, writes the target storage (NVMe or eMMC, exported as a second disk of the same USB device) according to the signed partition layout, collects device logs, and reports success only when the final status is `SUCCESS`. With a recovery initrd that advertises single-enumeration support, the Jetson keeps one USB connection and switches the disks’ media in place. Older recovery packages continue using their existing disconnect/reconnect handoffs.
Full recovery erases QSPI and every partition on the chosen storage, including `/data`. After the handoff, the first Ctrl+C warns that the device may be partially written; a second Ctrl+C confirms the abort. If the Jetson reboots into USB recovery mode mid-flash, the install stops and has to be started again; with single enumeration, the CLI also prints a reconnect hint when the flashing gadget stays off USB. On Windows, the first flash installs a WinUSB driver for the Jetson recovery device and raw disk writes require elevation — expect a single administrator (UAC) prompt as soon as the flash mode is settled for a Jetson Orin target (answered at the interactive flash-mode question, or pinned by `--rootfs-only`/`--storage emmc`/a non-interactive run); accepting it continues the command, including the remaining setup questions, in a new elevated console window. If Windows offers to format one of the Jetson's flashing disks mid-flash, always choose Cancel. On macOS, `wendy` claims the flashing disks so macOS neither mounts them nor reports them as unreadable; if such a prompt still appears, choose Ignore, as Eject interrupts the flash. On Linux, the `eject` tool must be installed (Debian/Ubuntu: `apt install eject`); with an initrd that supports single enumeration, the CLI checks for it before the command handoff and stops the install if it is missing.
Orin recovery retains the schema-v2 flashpack format. Published command packages default to the legacy USB handoff, so existing CLIs can still use them. A newer CLI validates the initrd’s identity and optional capability, then selects single enumeration by adding `conf/usb-mode` through an ext4 library to a private copy; the downloaded cache is preserved. Packages already selecting single enumeration need no editing and require a capable initrd. Unsupported modes are refused before the command handoff.
`--drive`, `--no-bmap`, and `--yes-overwrite-internal` apply only with rootfs-only imaging. eMMC has no rootfs-only mode. Rootfs-only emits a warning because it does not update QSPI. Versions that predate recovery flashpacks fall back to their legacy SD/NVMe image automatically (with a warning; `--rootfs-only=false` turns the fallback into an error); a recovery-capable flash never falls back to raw imaging on failure.
## Jetson AGX Thor recovery flash path
Jetson AGX Thor does not use the drive-writing flow. Selecting `jetson-agx-thor` downloads the Thor flashpack, asks you to put the board into USB recovery mode, scans for the recovery-mode Jetson, then performs:
1. **Stage 1 RCM boot** — sends the Thor recovery payload over USB.
2. **Stage 2 partition flash** — flashes QSPI and the internal NVMe through the Thor flashing gadget. Expect around 25 minutes: USB transfers and device-side writes are deliberately serialized (concurrent USB access could crash the flash tooling, most notably on macOS), so this stage does not parallelize.
3. **Power-cycle** — after a successful flash, power-cycle the Thor out of recovery mode to boot WendyOS.
The CLI prompts for confirmation before erasing the Thor. No external USB drive is selected, and `--drive` does not apply to this path. On Windows, installing or updating the USB driver requires administrator approval (UAC).
### Stage 2 flash errors and recovery
A Stage 2 failure can leave the Thor booting only into the UEFI shell; the CLI prints a recovery guide when that is possible. In all of the cases below, the fix ends the same way: power-cycle the Thor back into USB recovery mode and re-run `wendy install`.
| Error | Meaning |
|---|---|
| `No flash progress for 15m0s — assuming bootburn is stuck and aborting it.` | The stall watchdog killed a flash that moved no data and logged nothing for 15 minutes (a wedged flash would otherwise hang forever). |
| `the wendy flash tooling crashed mid-write` | A flash helper process crashed; the full crash report is in the flash log. |
| `a device-side write command failed mid-flash` | A write on the Thor itself failed (bad image, full or failing NVMe) — check the flash log's `Command failed` line for the specific cause before retrying. |
| `USB access denied opening the flashing gadget` | Linux: install the wendy udev rule (USB vendor 0955) or run with sudo. macOS: quit whatever holds the gadget (e.g. `adb kill-server`). |
Every failure prints the path of the full flash log (`thor-flash-.log`), which contains the complete tooling output.
## Dragonwing path
```sh
wendy install --device-type dragonwing-iq-8275
wendy install --device-type dragonwing-iq-9075
```
Connect the USB0 (USB-C) port, power off, set DIP switch 3 ON, and power on. Wendy downloads and verifies the bundle, then reads the board's chip id before writing anything and refuses the flash if it belongs to a different Dragonwing. A board that does not answer, or reports an id Wendy does not know, is flashed anyway after a caution — verification only happens when the board answers. Set DIP switch 3 OFF and power-cycle after success.
**An EDL flash is a factory reset.** Both OS slots, the config partition and `/data` are rewritten, so device identity, cloud enrollment, saved Wi-Fi and application data are discarded and the board comes back as a new device.
`/data` is blanked rather than overwritten: the flash clears the head of the filesystem and the device recreates it on first boot. That makes the old contents unreachable, but it is not a secure erase — blocks behind the superblock are only overwritten as they are reused. Do not rely on it before handing a board to someone else.
Provisioning works as it does on Thor: the bundle ships no config image, so wendy builds one on the host and programs it into the config partition. `--wifi`, `--device-name` and `--pre-enroll` all apply, and a freshly downloaded `wendy-agent` is seeded on every flash.
## Arduino UNO Q path
```sh
wendy install --device-type arduino-uno-q
```
The UNO Q is flashed over EDL, short for Emergency Download Mode, like the Dragonwing boards, with the same chip-id check, factory reset and provisioning.
Find the small **10-pin header labelled `JCTL`** near the USB-C socket. The **EDL pins are the pair at the end of JCTL farthest from USB-C**, circled in orange in [Arduino's pin-location photo](https://raw.githubusercontent.com/arduino/docs-content/main/content/software/app-lab/assets/flashing/flashing-pins.png).
1. Unplug the board.
2. Connect those two pins together using a jumper cap or a female-to-female jumper wire. This is what "shorting the pins" means.
3. Keep them connected while plugging the USB-C cable into your computer. Wendy detects the board in EDL mode and continues.
4. After flashing succeeds, unplug the board, remove the jumper and reconnect it to boot WendyOS.
You can remove the jumper once the board is detected in EDL mode. It stays in EDL until restarted without the pins connected. See [Arduino's flashing guide](https://docs.arduino.cc/software/app-lab/configure/flash/) for the illustrated instructions.
The UNO Q keeps its boot firmware on the same eMMC, so the flash also rewrites it (with Arduino's own ABL and U-Boot) and replaces the Debian image, which Arduino's flasher restores.
## Linux Desktop / Headless Mac path
Choosing **Linux Desktop** or **Headless Mac** (or `--device-type linux-desktop` / `--device-type headless-mac`) does not write a drive. The command prints the `agent.sh` one-liner from the [Linux installation guide](../../../../../../installation/linux/) (the script auto-detects the target platform). For Linux it passes `--network-access network`; on a Mac, choose **Allow Other Devices** when Wendy Agent first opens. When `--pre-enroll` is supplied and a valid auth session exists, a 1-hour enrollment token is embedded in the command; the agent self-enrolls on first startup without a separate `wendy device enroll` step. Run `wendy discover` afterwards to find the device.
### Privileges
Thor flashing talks to the board's USB recovery device directly (an in-process libusb handle), so — unlike the SD/NVMe disk-image path, which shells out to `sudo` only for the disk write — the **whole command must run as root**.
`wendy install` handles this for you: when it is not already running as root it re-executes itself under `sudo` **before** the recovery briefing, so you are prompted for your password up front rather than hitting a permission error partway through the flash. The elevated run reuses the already-downloaded flashpack (no re-download) and skips straight to the Thor flow.
- **macOS** — always elevates when not run as root; the OS binds its own driver to the recovery device, so there is no non-root path.
- **Linux** — if the wendy udev rule (`70-wendy-jetson.rules`, installed by the deb/rpm package; `wendy install` prints the commands to add it by hand) is present, the flash runs as your user with **no prompt**. Otherwise it re-execs under `sudo`.
- **Non-interactive** (CI, piped input) — the CLI cannot prompt for a password, so it exits with instructions to re-run under `sudo` (Linux: or install the udev rule) instead of hanging.
### WiFi pre-configuration
```sh
# Single network
wendy install --wifi-ssid MyNetwork --wifi-password hunter2
# Multiple networks, highest-priority first
wendy install \
--wifi "ssid=Home,password=hunter2,priority=100" \
--wifi "ssid=Office,password=corp,priority=50" \
--wifi "ssid=Cafe,hidden=true"
# Skip WiFi setup entirely
wendy install --no-wifi
```
`--wifi-ssid` without `--wifi-password` checks the system keychain (macOS) first, then prompts. In interactive mode without any `--wifi` flags, the CLI asks whether to configure WiFi and offers to scan nearby networks. If the scan fails or finds no networks, you can choose to enter credentials manually or skip WiFi setup entirely (configure later with `wendy device wifi connect`).
> **Note:** When a password is entered at an interactive prompt, the input is masked — characters appear as `•` so the password is never displayed in plaintext on screen.
The `--wifi` flag accepts `key=value` pairs separated by commas. Keys: `ssid` (required), `password`/`pass`/`psk`, `priority` (integer), `hidden` (true/false), `security` (e.g. `wpa2`). Commas inside values can be escaped with `\,`.
### Pre-enrollment
```sh
wendy install --pre-enroll
```
Requires an active `wendy auth login` session. The CLI creates an enrollment token via Wendy Cloud and writes provisioning JSON to the config partition so the device enrolls and receives mTLS certificates on first boot. Without `--pre-enroll`, the device boots unenrolled and can be enrolled later with `wendy device enroll`.
### Flags reference
| Flag | Default | Description |
|------|---------|-------------|
| `--nightly` | false | Use nightly/pre-release builds |
| `--pr` | — | Install from wendyos-builder PR #N (mutually exclusive with `--nightly`, `--version`, positional path; not supported for ESP32 targets) |
| `--device-type` | — | Device type from manifest (Linux targets only, e.g. `raspberry-pi-5`; not supported for ESP32 targets: `esp32-c5`, `esp32-c6`, `esp32-c61`, `esp32-p4`, `esp32-s3`) |
| `--version` | latest | WendyOS version to install (Linux only) |
| `--drive` | interactive | Target drive path (e.g. `/dev/disk4`) |
| `--rootfs-only` | false | Explicitly write only an Orin SD/NVMe image; QSPI is not updated |
| `--force` | false | Skip confirmation prompts |
| `--yes-overwrite-internal` | false | Required to wipe a non-removable drive non-interactively |
| `--wifi-ssid` | — | Pre-configure a single WiFi network |
| `--wifi-password` | — | Password for `--wifi-ssid` |
| `--wifi` | — | Pre-configure one WiFi network; repeatable |
| `--no-wifi` | false | Skip WiFi setup entirely |
| `--device-name` | interactive | Set device name on first boot (lowercase letters, digits, hyphens; must start with a letter, 3–55 chars) |
| `--pre-enroll` | auto | Pre-enroll with Wendy Cloud. For WendyOS/Jetson targets, embeds a certificate during imaging. For Linux Desktop / Headless Mac, mints a short-lived token printed with the `agent.sh` one-liner. |
| `--cloud-grpc` | auto | Cloud gRPC endpoint of the auth session to use for pre-enrollment (applies to both drive imaging and Linux Desktop / Headless Mac); defaults to the session set via `wendy auth use`. |
| `--storage` | auto | Storage variant: `nvme`/`sd` for raw imaging; AGX Orin recovery requires `nvme` or `emmc` non-interactively |
| `--no-bmap` | false | Disable bmap-accelerated flashing even when a block map is available |
To manage the device through Wendy Cloud later, [enroll it](../device/enroll.md)
if you did not choose `--pre-enroll` during installation. Local app development
does not require Cloud enrollment.
## Agent-driven installation
`wendy install plan` and `wendy install verify` also work under the `wendy os install`
alias. Both emit JSON. Planning is read-only; it resolves a published release and
returns the installation method, erase scope, requirements and CLI argument array.
```sh
wendy install plan --device-type raspberry-pi-5 --drive
wendy install plan --device-type jetson-orin-nano --carrier developer-kit --storage nvme
wendy install plan --device-type unitree-g1
wendy install verify --address --expected-os-version --expected-device-type
```
Planning supports Pi 3/4/5, Orin/Thor developer kits, G1 PC2 and Linux agent installs.
Jetson planning requires `--carrier developer-kit`; custom carriers need their
vendor guide. Raw-media plans require a drive from `wendy os list-drives --all --json`
before returning a write command. `--rootfs-only` leaves Orin QSPI unchanged.
The write still runs through the regular installer in a terminal.
Verification never selects a default device. Use `--expected-public-key` to pin
a previously observed identity, `--require-enrollment` when Cloud enrollment was
requested, and `--timeout` for a deadline of at most one minute (default 15s).
It exits nonzero for an unreachable target or unmet expectation and reports
`application: "not_checked"`; application health needs a separate check.
For agent-only targets, omit WendyOS version/type expectations.
`--address` takes a hostname, an IP address or `host:port`. For a device on USB,
pass the link-local address `wendy discover` prints, zone included
(`fe80::…%en31`). `--expected-os-version` matches both the bare release the
installer prints (`0.19.5`) and the `WendyOS-0.19.5` form the device reports.
See [agent-assisted setup](../../../../../../installation/agent-assisted-setup/) for the MCP
tools and the complete sequence from hardware identification to first application.
---
# List Drives
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/os/list-drives/
---
title: "List Drives"
description: "Lists all flashable drives that are available for wendy install"
---
Lists all flashable drives that are available for [`wendy install`](./install.md)
---
# wendy os update
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/os/update/
---
title: "wendy os update"
description: "Updates the OS on a WendyOS device using an A/B OTA mechanism, driven by the device's in-house wendyos-update engine."
---
# `wendy os update`
Updates the OS on a WendyOS device using an A/B OTA mechanism, driven by the
device's in-house **wendyos-update** engine.
```sh
# Auto-detect the latest stable release for the connected device
wendy os update
# Use the latest nightly build
wendy os update --nightly
# OTA update to a pull-request build
wendy os update --pr 123
# Provide a specific artifact URL
wendy os update --artifact-url https://example.com/update.wendy
# Provide a local artifact file
wendy os update ./update.wendy
```
---
## Supported targets
`wendy os update` only works with WendyOS devices that have OTA support. The command validates the target before doing anything else, including before updating the agent binary.
A target is treated as a WendyOS OTA target when the agent reports an `os_version` beginning with `WendyOS-`, or a non-empty `device_type`. This check does not depend on the reported `os` field, which is the `/etc/os-release` ID (e.g. `"ubuntu"`, `"wendyos"`) rather than a fixed value.
Hosts that are not WendyOS OTA targets — including macOS, Windows, unknown platforms, Wendy Lite / BLE-only targets, external/local-provider targets, and generic Linux hosts with `wendy-agent` installed but no WendyOS identity — are rejected immediately with an actionable error message. A WendyOS target must also advertise the `wendyos-update` featureset flag; without it, the update is rejected with a dedicated error.
| Circumstance | Error |
|---|---|
| macOS, Windows, unknown non-WendyOS platform, Wendy Lite, external/local provider | `This setup cannot be updated with wendy os update. Use this machine’s normal OS update tools instead. To use WendyOS OTA updates, install WendyOS on supported hardware with wendy os install.` |
| Generic Linux host with `wendy-agent` installed but no WendyOS identity | `This Linux host has wendy-agent installed, but it cannot be updated with WendyOS OTA artifacts. Use the Linux distribution’s package manager, such as apt, dnf, or pacman, to update this machine.` |
| WendyOS identity present but the device does not advertise the `wendyos-update` featureset | `This WendyOS image does not support OTA updates because the wendyos-update engine was not found on the device. Reinstall or upgrade to a WendyOS image with OTA support.` |
| WendyOS device running a version older than 0.17.0 | `this device runs WendyOS ‹version›. WendyOS 0.17.0 introduces a new update system with no backward compatibility, so it cannot be updated over the air.\nReflash it with \`wendy os install\` to continue receiving updates.` |
| No explicit artifact and the device type is missing or unrecognized in the update catalog | Shows a warning and prompts the user to select the correct device type from a picker. The latest version (stable or nightly) is then chosen automatically. |
> **Note:** macOS agents report a host OS version, but this does not qualify the host as a WendyOS OTA target. Only a `WendyOS-`-prefixed `os_version` or a non-empty `device_type` qualifies.
---
## Update sequence
1. **Validate target identity** — query the agent and confirm the target is a WendyOS OTA target. Exits immediately with an error if not.
2. **Check minimum OS version** — if the device reports a WendyOS version older than 0.17.0, exit non-zero with guidance to reflash using `wendy os install`. Dev builds, empty, and unparseable versions are allowed through. This check runs before the agent is updated, so no agent update is wasted on a device that must be reflashed.
3. **Update the agent** — ensure the agent binary is at the latest release before proceeding with the OS image update. GitHub release lookups use the `GITHUB_TOKEN` environment variable when present, and fall back to unauthenticated requests otherwise.
4. **Re-query version** — query the agent's version again after the agent update.
5. **Validate OTA support** — confirm the device advertises the `wendyos-update` featureset.
6. **Resolve artifact** — if no artifact or URL was provided, look up the latest OTA artifact for the device's reported `device_type`. If the device type is missing or not recognized, shows a warning and prompts the user to select the correct device type.
7. **Check current version** — if the device is already at the latest version, exits without updating.
8. **Stack-mismatch check** — if the resolved artifact is a `.wendy` file and the device does not advertise the `wendyos-update` featureset, exit non-zero with an explanation that a reflash is required. Skipped only when the device was already reported current in step 7.
9. **Stream update** — instruct the agent to apply the update, which runs `wendyos-update install` and streams progress to the terminal. The agent then reboots into the updated OS.
10. **Wait for reboot** — poll the device until it is reachable again (up to 10 minutes, enough for a rollback's second reboot).
11. **Report the outcome** — query the device for the post-update commit/rollback verdict and print it. The command exits non-zero when the update was rolled back.
---
## Jetson A/B preparation
Before starting `wendyos-update install`, the agent checks Jetson rootfs A/B
configuration and the alternate `APP_b` partition. It repairs missing or disabled
persistent EFI settings, preserves valid retry counts, and verifies each write
by reading the variable back. This preparation applies to both agent API versions.
If firmware rejects a write, the agent stops before downloading or staging the
OS update. It also refuses to change a disabled volatile setting: that change
would be lost at reboot. Such devices need an A/B-enabled firmware configuration
or image. Preparation does not repair an incompatible bootloader capsule.
## How the agent reboots
The agent always flushes filesystems before restarting. Without that, an
immediate kernel restart can discard recently written data — and on Jetson the
data at risk is the UEFI capsule that `wendyos-update` staged onto the ESP.
On WendyOS images older than **0.18.1** the agent additionally hands the reboot
to systemd (`systemctl --no-block reboot`) so filesystems are unmounted, not just
flushed. Those images ship a `wendyos-update` that fsyncs only the capsule file
and not the ESP, and because the capsule path deliberately leaves the rootfs slot
switch to the firmware, a lost capsule means the update silently reboots back into
the old OS and rolls back (WDY-2200). The orderly shutdown is the combination
validated on hardware for those releases.
An orderly shutdown can hang, so it is bounded: if the device is still running a
minute after the request, the agent forces an immediate restart. Expect an update
on a pre-0.18.1 image to take slightly longer to go down than on a current one.
Devices on 0.18.1 or newer, dev builds, and images whose version cannot be parsed
all take the plain flush-and-restart path.
## Post-update commit and automatic rollback
wendyos-update uses A/B rootfs slots, so an update boots into the new slot while keeping the previous OS intact. On the first boot after an update, the agent's gate runs `wendyos-update commit`. The health verdict is entirely delegated to that command: `commit` runs its own health checks internally (`/etc/wendyos-update/health.d`) before deciding whether the update is accepted, and the agent's gate just acts on the result — it does not run any healthchecks of its own.
- If `commit` accepts the update, it becomes permanent.
- If `commit` rejects the update (its `health.d` checks failed, or the deployment is otherwise marked failed), the agent runs `wendyos-update rollback` and reboots back into the previous OS slot.
- If the `wendyos-update` binary is missing at commit time, or the commit call times out, no health verdict was rendered — the agent keeps the pending-update marker and retries on the next agent start rather than rolling back a possibly-healthy slot. This retry window is bounded to one hour, after which the marker is discarded and treated as a plain commit.
The verdict — including any failure reason reported by `wendyos-update commit` — is persisted on the device's data partition, so it survives a rollback. `wendy os update` reports it once the device is back online:
```
Update failed post-reboot healthchecks and was rolled back to WendyOS-0.10.4.
Reason: wendyos-update commit failed: exit status 4 (health hook "50-containerd.sh" failed: exit status 1)
```
*(the text in parentheses is whatever `wendyos-update commit` itself reported as the failure reason)*
A rollback is not always a healthcheck failure, and the first line says which it
was. When the new OS never booted — the firmware fell back to the old slot, so
`health.d` never ran at all — the report names that instead:
```
The new OS did not boot; the device fell back to WendyOS-0.10.4 and the update was rolled back.
Reason: wendyos-update commit failed: exit status 1 (pending update wendyos-image-... is marked failed; run rollback)
```
If the reason is one the CLI cannot classify, it reports the rollback and the
captured reason without attributing a cause:
```
Update was rolled back to WendyOS-0.10.4.
Reason: wendyos-update commit failed: exit status 4 (platform verify: ESRT status 6163)
```
`wendy os update-status` reports the same record (including the `Reason:` line) after the fact, without re-running the update — useful for diagnosing a commit failure without shell access to the device. In addition to the persisted record, `wendy os update-status` queries the live wendyos-update engine snapshot, which distinguishes a committed nightly from a rolled-back one even when the OS version string is identical across builds.
`wendy os update-status` always requests the live `wendyos-update` engine snapshot in addition to the persisted commit/rollback record. When the agent returns it, the command prints a second block:
```
Engine status (wendyos-update):
Connector: tegrauefi
Booted slot: A
* slot A health=good distro=WendyOS-x.y.z
slot B health=trial distro=WendyOS-x.y.z retries=0
Diagnostics (raw):
RootfsStatusSlotA = 0x01
RootfsStatusSlotB = 0x00
```
The booted slot is marked with `*`. Diagnostic keys and values are connector-specific (e.g. tegra `RootfsStatusSlot{A,B}` bytes, EFI boot-chain/capsule variables, or uboot env entries) and may vary between firmware versions.
Pass `--json` to emit the complete status as indented JSON (useful in scripts or when capturing diagnostics off a device without shell access):
```sh
wendy os update-status --json
```
> **Note:** for older target images whose agent does not report an update result, the CLI falls back to comparing the OS version before and after the reboot, and warns when the device appears to have rolled back.
---
## Artifact auto-selection
When no artifact path or `--artifact-url` is given, the CLI uses the device's `device_type` field to look up the latest OTA artifact from the WendyOS release manifest. If the device type is missing or not recognized in the update catalog, the CLI shows a warning and falls back to an interactive device-type picker. The latest version (stable or nightly) is then chosen automatically.
Use `--nightly` to select nightly (pre-release) artifacts instead of stable ones.
Cloud-connected devices download the artifact directly from its URL. Ethernet
devices with a routable address and NAT-connected simulators also use the URL;
disconnected WiFi alone does not trigger a download on your computer. For an
isolated local connection with no reported routable address or active WiFi,
the CLI downloads and serves the artifact locally after checking the local route.
If a bare device name such as `--device ci-mesh-2` does not resolve locally,
the CLI looks for that exact device in your Cloud account. Explicit IP addresses,
hostnames with a domain, and host:port endpoints retain their direct connection.
Local artifact files require a direct LAN or USB connection. With a cloud tunnel,
use an artifact URL the device can reach. Reboot and update-result checks retain
the selected cloud device's identity instead of resolving its display name as a
LAN hostname.
After the artifact is resolved, the CLI checks its format against the device's advertised backend featureset. A `.wendy` artifact requires the `wendyos-update` engine; if the device does not advertise it, the update errors out with a reflash explanation rather than being applied.
---
## Update to a pull-request build
```sh
wendy os update --pr 123
```
OTA-updates the connected device to the WendyOS image built by wendyos-builder
PR #123. PR images are **debug builds**: SSH is enabled, root login is
passwordless, and the serial console is active. They are for testing the PR on
hardware — **never install a PR image on a production device.** Artifacts are
deleted when the PR is closed.
`--pr` is supported for Linux disk-image devices (Raspberry Pi, Jetson Orin
Nano, Jetson AGX Orin) with OTA support. It is not supported for Jetson AGX
Thor or ESP32 targets. `--pr` is mutually exclusive with `--nightly`,
`--artifact-url`, and a positional artifact file path.
---
## Flags reference
| Flag | Default | Description |
|------|---------|-------------|
| `--pr` | — | Update to wendyos-builder PR #N (mutually exclusive with `--nightly`, `--artifact-url`, positional path; Linux disk-image devices only) |
| `--artifact-url` | — | URL of an artifact (`.wendy`) to install directly. The CLI checks the artifact's format against the device's advertised backend before applying it; a `.wendy` URL on a device that predates the wendyos-update stack is refused with a reflash explanation. |
| `--nightly` | false | Use nightly/pre-release builds for auto-selection |
| `--drivers-dir` | — | Stage driver add-ons for the target kernel from local `.raw` files in this directory instead of the registry, for networks with no internet access. |
| `--no-drivers` | false | Update even though driver add-ons will not be staged for the target kernel; skips both the staging and its confirmation. |
A positional argument (a local `.wendy` file path, or a directory containing one) can be used instead of `--artifact-url`.
> **Tip:** After an update, `wendy device info` shows the last recorded update outcome. For live A/B slot state, pending updates, healthcheck results, and engine diagnostics, run `wendy os update-status`.
---
# Project manifest editor
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/project/
---
title: "Project manifest editor"
description: "Run wendy project inside an app directory to inspect and edit wendy.json. The terminal menu lets you add capabilities and integrations, edit existing settings, switch between app a"
---
# Project manifest editor
Run `wendy project` inside an app directory to inspect and edit `wendy.json`.
The terminal menu lets you add capabilities and integrations, edit existing
settings, switch between app and service scope, and review changes before saving.
You can make several changes before choosing **Review and save**. Exiting or
cancelling the menu discards unsaved changes.
If the directory has no manifest, the menu offers to create one for the existing
project. It does not scaffold source files. Compose projects can run without a
manifest; creating one adds optional Wendy-specific settings.
## Direct commands
```sh
# Inspect the manifest and its services.
wendy project show
wendy project show --json
# Add capabilities or integrations without choosing a manifest section.
wendy project add camera
wendy project add http --port 8080
wendy project add persist --name recordings --path /data
wendy project add i2c --bus i2c-1
wendy project add serial --serial-device ttyACM0
wendy project add ros2 --domain-id 42
# Change settings without removing and re-adding an entry.
wendy project edit ros2 --domain-id 43
wendy project edit persist --path /recordings
wendy project edit app --version 0.2.0
wendy project remove camera
# Validate without building or connecting to a device.
wendy project validate
```
`add` and `edit` show forms when run in a terminal without field flags. Forms
prefill existing values. When flags are provided, only missing required values
prompt. Commands that collect interactive input show a preview and ask before
saving. Fully specified commands apply immediately.
Omit the name from `add`, `edit`, or `remove` to use a searchable picker. Names
include `camera`, `audio`, `gpu`, `npu`, `network`, `persist`, `i2c`, `gpio`, `spi`,
`serial`, `usb`, `input`, `display`, `http`, `mcp`, and `ros2`. `storage` is an alias
for `persist`. The picker explains the remaining capabilities. Deprecated
`video` entries remain editable, but new projects should use `camera`.
New network entries default to explicit `bridge` mode, which supplies outbound
internet access. Use `--mode host` when the app needs the device's host network.
Mesh networking also requires `--service-cidr`. Changing away from mesh removes
the mesh-only service CIDR.
ROS 2 `--scope` sets who can see the app's topics: `app`, `device` (the default),
`peripherals`, or `network`. `device` and `peripherals` need the host network, so
validation rejects them next to a `bridge` network entry. Manifests that still set
the deprecated `discoveryScope` keep working; `wendy project show` reports the scope
it behaves as.
## Service scope
```sh
wendy project add camera --service vision
wendy project edit ros2 --service navigation --domain-id 42
```
These commands edit only the named service. The overview separates app defaults
from local service entries. To change or remove an inherited app-level entry,
edit the app scope without `--service`. For a Compose service, Wendy creates its
companion manifest section when needed. The Compose file itself is unchanged.
Multiple storage, I2C, and serial entries are supported. If a type appears more
than once, the menu lets you choose an entry. In scripts, use the zero-based
index printed by `show`, for example `wendy project edit persist --entry 1 --path
/cache`. Indices refer to the entitlement array within the selected scope and
can change after removing an entry.
## Preview, scripting, and other files
```sh
wendy project add http --port 8080 --dry-run
wendy project edit ros2 --domain-id 42 --dry-run --json
wendy project --file ./apps/robot/wendy.json
wendy project validate ./apps/robot
```
`--file` accepts a manifest file or project directory. `--dry-run` validates and
prints changes without writing. Without a terminal, or with `--json`, commands
never prompt. Missing names and required fields produce errors with the relevant
flags. Plain `wendy project` prints the overview in a noninteractive session.
`show --json` returns `path`, `exists`, `compose`, `manifest`, and `services`.
Mutation output includes `saved`, `dryRun`, `changes`, and `warnings`.
`validate --json` reports `valid`, warnings, and an error on failure, with a
nonzero exit status for invalid manifests. Preview paths use JSON Pointer syntax.
Previews redact password, token, secret, and environment values; `show --json`
returns the complete manifest for tooling.
## Edit the full manifest
```sh
EDITOR='code --wait' wendy project edit --raw
```
This opens a temporary copy in `$VISUAL` or `$EDITOR`. Use an editor command that
waits until editing is complete. After the editor exits, Wendy validates the
result and previews changes before replacing the manifest. This also works for
repairing malformed JSON. Use it for environment variables, readiness checks,
hooks, resource limits, and other fields without dedicated forms.
All saves validate the manifest, preserve unrelated and unknown JSON fields,
retain file permissions, and replace the file atomically. Saves stop if the
manifest changed after editing began. Saving formats JSON with two-space
indentation and sorted object keys; it does not preserve the original layout.
The older `wendy project entitlements` and `wendy project frameworks` commands
remain available and use the same editor and save behavior. `wendy json validate`
and `wendy json schema` also remain available.
---
# Add
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/project/entitlements/add/
---
title: "Add"
description: "wendy project entitlements add [type] adds a capability to wendy.json. The shorter wendy project add [type] also accepts integrations such as ros2."
---
`wendy project entitlements add [type]` adds a capability to `wendy.json`.
The shorter `wendy project add [type]` also accepts integrations such as `ros2`.
Omit the type to open a searchable picker. Interactive forms collect missing
values and show a change preview before saving. For scripts, supply required
values explicitly:
```sh
wendy project add http --port 8080
wendy project add persist --name recordings --path /data
wendy project add i2c --bus i2c-1
```
Use `--dry-run` to preview without saving, or `--service ` for a service's
settings. See the [project editor](../index.md) and
[entitlement reference](../../../../../apps/wendy.json.md#entitlements-1).
---
# List
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/project/entitlements/list/
---
title: "List"
description: "Lists all active entitlements, as --json or in human a readable way."
---
Lists all active entitlements, as `--json` or in human a readable way.
---
# Remove
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/project/entitlements/remove/
---
title: "Remove"
description: "wendy project entitlements remove can be run with an entitlement as input, or without."
---
`wendy project entitlements remove` can be run with an entitlement as input, or without.
`wendy project entitlements remove ` will immediately update `wendy.json` to remove the entitlement.
Omitting the entitlement from the invocation shows a list of all active entitlements and an interactive menu to remove them.
---
# Optimize
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/project/optimize/
---
title: "Optimize"
description: "wendy project optimize statically analyzes your project's build configuration — its Dockerfile(s), requirements.txt, and wendy.json — and reports missed build-speed and runtime opt"
---
`wendy project optimize` statically analyzes your project's build configuration — its Dockerfile(s), `requirements.txt`, and `wendy.json` — and reports missed build-speed and runtime optimizations.
It runs locally and is read-only by default. It works on a single Dockerfile, a multi-service / Compose project (findings are grouped by service), and native Swift (`Package.swift`) or `Brewfile` projects.
## What it checks
- **Build caches** — compiled-language build/install steps (`cargo`, `go`, `swift`, `npm`/`yarn`/`pnpm`, `pip`) that run without a BuildKit `--mount=type=cache`, and so re-download or re-compile dependencies on every build.
- **Release vs. debug** — debug builds shipped to production (`swift build` without `-c release`, `cargo build` without `--release`), and whether `WENDY_DEBUG` is wired to toggle the optimization level. This check targets hand-written Dockerfiles. For Swift apps cross-compiled via `wendy run`/`wendy build` (the swift-container-plugin path), the CLI passes `-c release` automatically; no manual `wendy.json` or Dockerfile change is needed for that path. Stagefile-generated Dockerfiles always spell the configuration out (`swift build -c release` or `-c debug`, `cargo build` with or without `--release`), so this finding never fires for them — pass `--debug` to `wendy build`/`wendy run` to switch a Stagefile build to debug.
- **CUDA / ML** — a CPU-only ML wheel (e.g. `torch==…+cpu`) paired with the `gpu` entitlement (or a CUDA wheel without it), and x86 `nvidia/cuda` base images on an arm64 (Jetson) target.
- **Architecture & image** — an `amd64` base image on an arm64 device (which runs under slow QEMU emulation or fails), a missing `.dockerignore`, and single-stage builds that ship their full build toolchain.
- **Dockerfile hygiene** — `apt-get install` split into a separate `RUN` from `apt-get update` (a stale-package-index trap) or missing `--no-install-recommends`; `pip install` without `--no-cache-dir`; `npm install` used despite a `package-lock.json` (consider `npm ci`); `ADD` used where a plain `COPY` would do; an unpinned or `:latest` `FROM` tag; shell-form `CMD`/`ENTRYPOINT` (breaks signal forwarding on `docker stop`); and `COPY --from` that drags in an entire build stage instead of just the artifact it needs.
## Usage
```bash
wendy project optimize # report findings (colorized in a terminal, JSON in CI)
wendy project optimize --json # machine-readable findings
wendy project optimize --fix # apply the safe, deterministic fixes
wendy project optimize --agentic # emit a context bundle for an AI agent
```
## Flags
- `--fix` — apply the safe fixes only: add a build-cache mount, add the release flag (`swift`/`cargo`), create a default `.dockerignore`, add `--no-install-recommends`/`--no-cache-dir`, and swap a plain-file `ADD` for `COPY`. Fixes are idempotent; contextual changes (multi-stage refactors, choosing the right CUDA wheel, retagging a `:latest` base image, converting shell-form `CMD` to exec form) are left to you or the `--agentic` flow. `npm install` → `npm ci` is reported but never auto-fixed, even here — a drifted lockfile makes `npm ci` fail outright where `npm install` would have quietly updated it, so it's a suggestion, not a fix.
- `--agentic` — instead of a report, emit a JSON bundle (static findings plus the verbatim project files and a prompt) designed to be piped into Claude Code or the Wendy MCP server.
- `--severity ` — the minimum severity that causes a non-zero exit. Defaults to `warning`.
- `--arch ` — override the target architecture (defaults to `arm64`).
- `--json` — emit findings as JSON (also the default when output is not a terminal, e.g. in CI).
## Exit codes
- `0` — no findings at or above the severity threshold.
- `1` — findings at or above the threshold (use this to gate CI).
- `2` — execution error (no project found, parse failure).
## At build time
Every time `wendy run` / `wendy build` builds a plain Dockerfile or Containerfile (not a Stagefile), it first runs a subset of this scan and, if any *purely additive* fix applies — a build-cache mount, `--no-install-recommends`, `--no-cache-dir`, or a plain-file `ADD` → `COPY` swap — applies it **in memory** and builds that instead. Your real Dockerfile on disk is never touched; nothing is written unless a fix actually applies, and even then only a sibling `Dockerfile.generated` is created. This runs unconditionally (interactive or not, every build) since it's pure static analysis over an already-in-memory file — sub-millisecond overhead, nothing you'd notice next to the build itself.
Stagefile projects skip this scan entirely — a Stagefile is compiled, not patched, so the optimizations are the compiler's job. They write into the same `Dockerfile.generated*` namespace: `build.stagefile.yaml` compiles to `Dockerfile.generated` (the same name the auto-fix path uses, which is why only one of the two can apply to a given build), and each variant compiles to its own `Dockerfile.generated.` — `prod.stagefile.yaml` to `Dockerfile.generated.prod`. See [Stagefile naming](../build.md#stagefile-naming).
Fixes that change build *outcome* or *behavior* — `npm install` → `npm ci` (can turn a passing build into a failing one on a drifted lockfile) and the `swift`/`cargo` release flag (changes the shipped binary's runtime behavior) — are deliberately excluded from this silent path. They still show up as findings, and the release flag is still available via an explicit `--fix`.
Separately: after a slow *incremental* build (one that reused cached layers and still took more than ~50s), `wendy run` / `wendy build` also runs the full scan interactively, shows every finding, and offers to apply the remaining safe fixes to disk for your next build. This part never runs in CI or non-interactive shells.
## Sample projects
The repository used to ship deliberately un-optimized sample projects under [`Examples/project-optimize-samples/`](https://github.com/wendylabsinc/wendyos/tree/main/Examples/project-optimize-samples), each triggering a specific finding. Those four have since been converted to `build.stagefile.yaml`, and this command does not analyse Stagefiles — so they no longer reproduce anything. Each one's `EXPECTED.txt` now records what its findings were and which of them a Stagefile makes structurally impossible, which is a useful read if you are choosing between the two formats.
To see the analyzer and `--fix` in action, run it against a project that still has a Dockerfile — [`Examples/ClaudeOnDevice`](https://github.com/wendylabsinc/wendyos/tree/main/Examples/ClaudeOnDevice) and [`Examples/HelloAudio`](https://github.com/wendylabsinc/wendyos/tree/main/Examples/HelloAudio) are the two left in this repository — or against your own.
## A note on `--agentic` and secrets
The `--agentic` bundle includes the **verbatim contents** of your Dockerfile(s), `requirements.txt`, and `wendy.json` so the agent has full context. These files can contain secrets (`ARG`/`ENV` tokens, private registry URLs). The command prints a reminder to stderr; review the bundle before sending it to an external agent.
---
# wendy registry key
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/registry/key/
---
title: "wendy registry key"
description: "Shows or rotates this machine's registry encryption key. Encrypted releases are wrapped to it, so they open on this machine only."
---
# `wendy registry key`
Shows or rotates this machine's registry encryption key. Encrypted releases
are wrapped to it, so they open on this machine only.
```sh
wendy registry key show
wendy registry key rotate
```
Each machine (and each browser) you sign in on holds its own key. `wendy auth
login` creates it and registers it with Wendy Cloud, signed with your operator
key; a certificate refresh retries a registration that did not go through.
If registration fails, `wendy auth login` and a certificate refresh (such as
`wendy auth refresh-certs`, or the automatic renewal before a command) print a
warning and leave the key unbound. It is registered at the next sign-in or
certificate refresh, or when you run `wendy registry key show`.
`show` asks Wendy Cloud whether it still holds the key and registers it again
if not. It prints `registered`, `re-registered`, or `unregistered` with the
reason Cloud refused it.
| Command | Description |
|---------|-------------|
| `show` | Prints the key id and registers the key if Cloud does not hold it |
| `rotate` | Replaces the key with a new one and registers it |
| Flag | Description |
|------|-------------|
| `--cloud-grpc` | Cloud gRPC endpoint of the session to use |
After a rotation, releases wrapped to the old key must be wrapped again to the
new one by another of your signed-in devices.
---
# Run
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/run/
---
title: "Run"
description: "Runs your app on a Wendy-enabled device:"
---
Runs your app on a Wendy-enabled device:
1. [Selects a device](../device-selection.md)
2. [Queries the platform and architecture](./device/version.md) of this device
3. Invokes a [build](./build.md) using the target triple, and injects a [debugger](../../../debugging/) if needed
4. Uploads the artifact(s) for Linux (via the container registry) or macOS
5. [Starts the app](./device/apps/start.md). Attached runs, and any run with `--wait-ready`, then wait for readiness and print the reachable URL; other detached agent deployments report configured HTTP endpoints as described in [Detached output](#detached-output).
6. [Attaches the logs](./device/logs.md) if needed (when `--detach` is not provided)
[Command syntax, examples, and generated flag reference](../../../../../reference/cli/run/)
## Flags
| Flag | Description |
|------|-------------|
| `--deploy` | Build and create the container but do not start it. |
| `--detach` | Start the container and return without streaming logs or opening the app URL. Waits for readiness only with `--wait-ready`. Agent deployments report configured HTTP endpoints; see [Detached output](#detached-output). |
| `--wait-ready` | Succeed only once the app is ready: its readiness probe passes or, without a probe this machine can reach, it stays running for 10 seconds (or a shorter `--readiness-timeout`). Fails if the app crashes or the probe times out. See [Waiting for readiness](#waiting-for-readiness---wait-ready). |
| `--readiness-timeout ` | Override the readiness deadline, from `1s` to `1h` in whole seconds. With `--detach` it requires `--wait-ready`. |
| `--restart-unless-stopped` | Restart the container unless manually stopped. |
| `--restart-on-failure` | Restart the container on failure. |
| `--no-restart` | Do not restart the container on exit. |
| `--debug` | Enable debug logging and inject debug tooling via `WENDY_DEBUG=true`. For SwiftPM projects (both native macOS and cross-compiled Linux container targets), builds with `-c debug` instead of `-c release`. |
| `--yes` / `-y` | Accept all device-selection prompts automatically. |
| `--builder ` | Image builder for Dockerfile/Containerfile builds: `docker`, `apple-container`, or `buildkit`. Cannot be combined with `--build-host`. |
| `--stagefile-backend ` | Stagefile compiler backend: `dockerfile` (default) or experimental direct `llb`. Direct LLB requires Docker/BuildKit and cannot be combined with Apple Container or `--build-host`. |
| `--build-host ` | Build the image on another WendyOS device instead of this machine. See [Remote build host](#remote-build-host). |
| `--build-type ` | Override build type detection: `docker`, `swift`, or `python`. |
| `--prefix ` | Run from a project directory other than the current working directory. |
| `--product ` | Swift Package Manager product to build and run (Swift projects only). |
| `--service ` | Build and run only the named service and its transitive dependencies (multi-service `wendy.json` projects only). Returns an error if the name does not match any key in the `services` map. |
| `--keep-going` | Deploy services that build successfully instead of aborting the whole group on the first build/push failure (multi-service projects only). |
| `--max-concurrency ` | Max service images to build+push at once in multi-service projects. 0 = default limit of 4. |
| `--user-args ` | Extra arguments to pass to the container at runtime. |
| `--env ` | Set an environment variable in the container. Repeatable. Overrides a `wendy.json` `env` entry of the same key. See [Environment variables](#environment-variables). |
| `--chunking ` | Controls the content-based chunking (CBC) chunk-diff deploy path: `auto` (default), `force`, or `off`. See [Deploy path: `--chunking`](#deploy-path---chunking). |
| `--watch` | Watch the project directory and redeploy on every change, streaming the app's logs between deploys. Runs non-interactive. See [Watch mode](#watch-mode). |
| `--debounce ` | Watch mode only: quiet period in milliseconds after the last change before redeploying (default `400`). |
| `--verbose` | Watch mode only: always show build output. By default build output is hidden unless a build fails. |
## Exit status
An attached `wendy run` streams the app's logs until the app exits or you stop the run:
| How the run ends | Exit status | The app afterwards |
|------------------|-------------|--------------------|
| The app exits with code 0 | 0 | As its restart policy leaves it |
| The app crashes: non-zero exit, OOM kill, or crash loop | Non-zero. The error names the exit code and termination reason and points to `wendy device logs --app `. | As its restart policy leaves it |
| The container fails to start | Non-zero, with the start error the device reported | Not started |
| Ctrl-C (SIGINT) | 0 | Stopped |
| SIGTERM (a CI timeout, `kill`, a process supervisor) | Non-zero: `wendy run was terminated; app was stopped` | Stopped |
The table describes single-container runs on WendyOS devices. Multi-service, Compose and native Mac runs, and runs on local provider targets such as `--device docker` or `--device apple-container`, exit 0 on Ctrl-C and try to stop their apps, but can leave them running: their stop races the cancellation of the run and is skipped when the cancellation wins. On SIGTERM they exit non-zero and may leave their apps running.
When nothing changed since the last deploy and the app is already running, `wendy run` only follows its logs. An interrupted run then leaves the app running, and the SIGTERM error says so. When the app is stopped instead, `wendy run` starts the existing container, unless the device reports it from another app `version` than `wendy.json` sets (`latest` when it sets none): that container is another deployment's, so `wendy run` deploys the project instead.
An interrupt that comes after the app's output has ended, while `wendy run` checks how the app exited (see below) or waits for the verdict of `--wait-ready`, stops nothing: the app has already exited, and the device may be running another deployment's app by then. Ctrl-C still exits 0, and SIGTERM exits non-zero.
The crash check reads the exit that the device agent records when the app stops, and re-checks the app for up to 3 seconds before it reports a crash (see below; it prints `Checking how exited...` on stderr as the re-check starts): the device keeps an app's last recorded exit even after its restart policy restarted it, so a record alone does not prove the app just crashed. A clean exit (exit code 0) is reported at once, also when the device lists the app as crash-looping: once its restart policy has restarted an app, the device keeps that restart count until the app is next deployed or started, and lists a clean exit that the policy will restart that way. If the restart policy has already restarted the app by the time of the check, and the restart it counted still shows after that re-check, the exit status is gone: `wendy run` prints a notice and exits 0. Agents that predate exit reporting record no exit: every exit that stays for that re-check reads as a clean stop, and one that the restart policy recovered from reads as described in the known limitations below.
When another deployment replaces the app (for example `wendy run --detach` from another terminal), the run usually exits 0 and prints `Application was replaced by another deployment.` (a run that only follows the app prints a neutral notice instead; see below). To replace an app, the device kills it, removes its container, prepares the new image while no app is listed, and creates the new container, which it lists as stopped (or crash-looping) with no exit recorded until it starts it. The whole replace can take a fifth of a second. The device counts the kill as a restart, so it can briefly list the new app running with a restart counted before the start resets the count. It records the kill like a SIGKILL crash (exit code 137), but that record may come late, on the new container, or never, and the killed app can be listed with the exit it recorded before, such as a crash its restart policy recovered from:
- Before it reports a crash or a restart by the restart policy — and when it finds no app, or an app stopped or crash-looping with no exit recorded — the run re-checks the app for up to 3 seconds. It reports the replacement as soon as the device lists a new container (after listing no app, or with no exit recorded after an app that was running or had an exit recorded), a restart count lower than the last one listed, the app running with no restart by its restart policy counted (once the device has shown that it reports exits or restarts; see the known limitations below), or an app from another version. It reports the crash (or the restart) only if the device still shows the app restarted by its restart policy, or the app stopped (or cannot be read), after all 3 seconds. The device can list an app as stopped before it records the exit, so an exit recorded during the re-check is the one reported.
- When the 3 seconds end with no app listed, a run that started the app, whose output has ended, reports the replacement: the new image is probably still being prepared. An app still crash-looping with no exit recorded counts as a replacement, again once the device has shown that it reports exits or restarts. An app still stopped with no exit recorded does not, since that is also how the device lists an app stopped with `wendy device apps stop`: the run prints `Application stopped.`.
- A record from another app `version` (as set in `wendy.json`) counts as a replacement at once.
- If a run that started the app finds, when its output ends, the new app already running from the same version, it prints `Application stopped.` instead.
- A run that only follows an app it did not start (see above) checks the app every second; while a check finds the app running, the run keeps following it. When a check finds the app stopped, the run re-checks it as above, but reports a crash (a non-zero exit) only if, after the 3 seconds, the app is still stopped with its exit recorded and nothing indicated a replacement. Otherwise (the app running again at any restart count, no app listed, or any sign of a replacement) it prints `Application stopped; it may have been replaced by another deployment or restarted by its restart policy.` and exits 0, leaving the app as it is. A clean exit (exit code 0) prints `Application stopped.` as before.
- With `--wait-ready` and no `--detach`, the readiness check also ends as soon as the run's output does, and a replacement it finds is reported the same way: the run exits 0, leaves the new app running, and runs no postStart actions for it. It also counts as a replacement, once the output of a run that started the app has ended, the app running with no restart counted, on a device that has shown that it reports exits or restarts. A run that only follows the app ends its check the way it ends following above: a crash only when the app stays stopped with its exit recorded, otherwise the notice that it may have been replaced or restarted, and exit 0. An exit or a restart during the check, a clean exit included, is re-checked the same way before the check fails, and a failed check never stops an app the device does not list, or lists as stopped with no exit recorded.
> **Known limitations:** a run that only follows an app it did not start cannot tell a crash that the restart policy recovered from apart from a replacement by another deployment: the device keeps the exit status of earlier exits, and the restart count the run started from may already include restarts. Such a run therefore reports that end neutrally and exits 0.
>
> Devices whose agent reports neither how apps exit nor restart counts (WendyOS agents that predate exit reporting, and the Mac agent) list an app that crashed and was restarted by its restart policy the way they list a replacement: stopped with no exit recorded, then running with no restart counted, or crash-looping with no restart counted. Until the run has seen a record with an exit recorded or a restart counted, it cannot tell the two apart: a run that started the app prints `Application stopped; the device did not report how it exited, so it may have crashed and been restarted by its restart policy, or another deployment may have replaced it.` and exits 0, and with `--wait-ready` the check fails as `not_ready` and does not stop the app. A current device reads the same way when the run sees no exit or restart recorded during a fast replacement.
When the image build fails, `wendy run` prints the build failure details, including the builder's own error and the path of the full build log, and the error line below them names the step that failed and the cause when the log shows them (the last informative line of the step's output, skipping generic closing lines such as make's `make: *** [...] Error 1` or pip's closing notes, or the error the builder reported), for example `build failed at [build 4/4] RUN go build -o /out/app .: ./main.go:6:14: undefined: foo`.
## Detached output
For an ordinary single-device deployment to a Wendy agent, `--json --detach`
emits one result on stdout after start is acknowledged (or the unchanged app is
already running). Build progress and pre-start application output go to stderr:
```sh
wendy --json --device vm:dev run --yes --detach
```
```json
{
"status": "started",
"app": "com.example.web",
"device": "vm:dev",
"readiness": "not_checked",
"url": "http://127.0.0.1:18880",
"endpoints": [
{ "app": "com.example.web", "url": "http://127.0.0.1:18880" }
]
}
```
`readiness: "not_checked"` means no health probe or host `postStart` action ran.
Verify the HTTP response separately. In text mode, URLs appear as
`App URL (): ` notices. With `--wait-ready`, the run checks the app
and prints its outcome instead of this result; see
[Waiting for readiness](#waiting-for-readiness---wait-ready).
URLs come from [`http` entitlements](../../../apps/wendy.json.md#http) and
HTTP(S) `hooks.postStart.openURL` values that contain `WENDY_HOSTNAME`.
`endpoints` contains distinct reported URLs, each with its app or service
identifier; a service may have several URLs. `url` is the first entry
and is omitted when `endpoints` is empty, including when routing cannot be
determined. A TCP readiness probe alone does not declare an HTTP endpoint.
For user-networked VMs, URLs use the connected VM's live forwarding on host
loopback. See [VM HTTP verification](../../../installation/wendyos-virtual-machine.mdx#reaching-an-app-you-deployed).
Multi-service and Compose runs emit one group result after the selected services
start. A partial deployment returns non-zero without a whole-group success
result. Fleet runs, `--deploy`, watch mode, and local container providers do not
produce this result.
## Waiting for readiness: `--wait-ready`
`--wait-ready` makes `wendy run` report whether the app actually came up:
```sh
wendy run --detach --wait-ready
wendy --json run --detach --wait-ready --readiness-timeout 90s
```
After the device confirms the container started, `wendy run` does one of two checks:
- It probes the app's readiness port from your machine (`readiness.tcpSocket.port`, or the `http` entitlement's port) until the port accepts a connection. The deadline is `--readiness-timeout`, else `readiness.timeoutSeconds`, else 30 seconds.
- When the app declares no probe, the device is reached through Wendy Cloud without an active Wendy Mesh VPN and your machine cannot connect to the device's agent on its LAN address (`wendy run` checks once, for up to 1.5 seconds), or `wendy run` runs on the device itself over the agent socket (`WENDY_AGENT_SOCKET`), it checks that the app stays running for 10 seconds, or for `--readiness-timeout` when that is shorter.
The run fails with a non-zero exit when, during the check, the app exits (even with exit code 0), is restarted by its restart policy, or is no longer reported by the device; when the probe deadline passes; or when, without a probe, the app's state cannot be read at the end of the window. A passed probe counts only once `wendy run` reads the app's state right after it and finds the app still running; until then it keeps probing, and if the state cannot be read before the deadline the check fails as `not_ready`.
Restarts count from when `wendy run` starts the app, since the start resets the device's restart count: a restart before the check first looks at the app fails it too. When nothing changed since the last deploy and the app is already running, `wendy run` did not start it, so only restarts after it found the app count.
With `--detach` in JSON mode (`--json`; on by default when stdin or stdout is not a terminal), stdout carries exactly one JSON object, failures included, unless the run is interrupted or its command line does not parse (see below). It takes the place of the `started` result that `--detach` alone prints (see [Detached output](#detached-output)): `started` means only that the device acknowledged the start, while this object reports the check. Its `status` is one of five values:
```json
{"status":"ready","app":"my-app","device":"192.168.1.207","readiness":"passed","url":"http://192.168.1.207:8080"}
{"status":"running","app":"my-app","device":"192.168.1.207","readiness":"not_checked"}
{"status":"crashed","app":"my-app","device":"192.168.1.207","readiness":"not_checked","exit_code":3,"termination_reason":"crashed","message":"app my-app stopped unexpectedly (exit code 3, termination reason \"crashed\"); see its logs with `wendy device logs --app my-app`"}
{"status":"not_ready","app":"my-app","device":"192.168.1.207","readiness":"failed","message":"app my-app did not pass its readiness probe within 30s"}
{"status":"failed","app":"my-app","device":"192.168.1.207","readiness":"not_checked","message":"build failed at [build 4/4] RUN go build -o /out/app .: ./main.go:6:14: undefined: foo"}
```
| `status` | When | `readiness` | Fields besides `status`, `readiness`, `app` and `device` | Exit status |
|----------|------|-------------|------------------|-------------|
| `ready` | The readiness probe passed: the app's port accepted a connection from this machine. | `passed` | `url`, when the device reports an address for the app | 0 |
| `running` | No probe could run from this machine (see the second check above), and the app stayed running without a restart for 10 seconds, or for a shorter `--readiness-timeout`. | `not_checked` | None | 0 |
| `crashed` | During the check the app exited (even with exit code 0), stopped, crash-looped, was restarted by its restart policy, or was no longer reported by the device. | `failed` when a probe was running, else `not_checked` | `message`; `exit_code` and `termination_reason` when the device recorded the exit | Non-zero |
| `not_ready` | The probe did not pass before its deadline, the app's state could not be read to confirm a passed probe before it, or, without a probe, the app's state could not be read at the end of the window. | `failed` with a probe, `not_checked` without | `message` | Non-zero |
| `failed` | The run failed before the check started (see below). | `not_checked` | `message`: the run's error | Non-zero |
`app` is the app ID from `wendy.json`. `device` is the address `wendy run` connected to: for a direct connection that is often an IP address rather than the `.local` name, and through Wendy Cloud it is the device's cloud name. On the device itself (`WENDY_AGENT_SOCKET`), `device` is omitted and readiness is never probed, so the outcome is `running`, `crashed`, `not_ready` or `failed`. A `failed` object also omits `app` and `device` until the run knows them, and it never carries `exit_code`, `termination_reason` or `url`.
`failed` covers every error `wendy run` itself reports before the check: a flag value or combination it rejects (for example `--readiness-timeout 1500ms`, `--env FOO`, or `--wait-ready` with `--watch`, `--hil` or `--deploy`), a project or target `--wait-ready` does not support (listed at the end of this section), an invalid `wendy.json`, device selection, the build, the push, and the container start. The run also prints the error on stderr. Errors that stop the command before `wendy run` starts, such as an unknown flag, a malformed value like `--readiness-timeout abc`, or a CLI configuration that cannot be loaded, print only the error on stderr and no object. A run without `--wait-ready` prints no object when it fails, even when it is rejected (for example `--detach --readiness-timeout 30s`); a successful one prints the `started` result described in [Detached output](#detached-output). Ctrl-C (exit 0) or SIGTERM (non-zero exit) prints no object at any point; during the wait, both leave the app running.
A detached check does not look for another deployment. If one replaces the app during the check, the outcome is whatever the polls see: `crashed` when a poll finds the old app stopped (the device records it like a SIGKILL crash, exit code 137) or the app missing or not yet started, or `ready` or `running` when the polls see only the new app running.
Without `--detach`, `--wait-ready` runs the same check while streaming logs. A failed check fails the run and stops the app, except when nothing changed since the last deploy and `wendy run` only follows the app that was already running: that run did not start the app, so it fails without stopping it, and it ends neutrally instead of failing when the app runs again or may have been replaced (see [Exit status](#exit-status)). If another deployment replaces the app during the check, the run reports the replacement and leaves the new app running; when the device does not report how the app exited, so the run cannot tell a replacement from a crash, the check fails as `not_ready` and does not stop the app (see [Exit status](#exit-status)). Host-side postStart actions run only after the check passes.
`--wait-ready` currently supports single-container image projects (Dockerfile, Containerfile, Stagefile, or Python) on WendyOS devices. It is rejected for multi-service, Compose and Xcode projects, native Mac apps, Swift packages built without a Dockerfile, local provider targets such as `--device docker`, `--build-host`, `--watch`, `--hil`, and `--deploy`.
## Reachable app URLs
In attached mode after the app starts, and in a detached run with `--wait-ready` once its check passes, `wendy run` prints an `App reachable at ` line when it can infer a browser URL from the app configuration:
```text
App reachable at http://192.168.123.222:3000
App reachable at http://[2001:db8::1]:3000
```
The CLI derives this URL from either:
- `hooks.postStart.openURL`, when the URL contains `WENDY_HOSTNAME`
- `readiness.tcpSocket.port`
The printed URL uses a routable IP address reported by the device instead of the `.local` hostname, which makes it easier to open from browsers that do not resolve mDNS names reliably. If neither an `openURL` hook nor a TCP readiness port is configured, or if the device cannot report an IP address, `wendy run` skips this line.
When the device is reached through Wendy Cloud and the Wendy Mesh VPN is active on your machine, the readiness check, the printed URL, and the `openURL` and `cli` postStart actions use the device's mesh hostname. Without an active mesh route, `wendy run` checks once, for up to 1.5 seconds, whether your machine can connect to the device's agent on its LAN address. If it can (you are on the device's network), those actions work as on a LAN connection. If it cannot, `wendy run` does not print, probe, or open that address: the host-side readiness check and the `openURL` and `cli` postStart actions are skipped with a notice.
When an attached run's readiness probe times out but the app is still running, `wendy run` keeps checking every 5 seconds for up to ten probe timeouts in total (5 minutes with the default 30-second timeout), then warns. `--readiness-timeout` replaces the probe timeout and ends the wait at that deadline.
> **Note:** An attached run prints the `App reachable at` line before its readiness probe finishes (with `--wait-ready`, only once the check passes). If the probe then fails (timeout or connection error), `wendy run` skips the `postStart` hook, including opening the browser, and prints a warning instead. This prevents opening a browser tab pointed at a container that has already exited.
> **Note:** When `wendy.json` is absent, `wendy run` resolves the target device before prompting to create one. If the target is Headless Mac and the detected project type is unsupported, the project/target mismatch error is returned immediately without opening the config creation prompt.
## ESP32 — native ESP-IDF projects
Regular ESP-IDF projects are the recommended app model for ESP32 targets. Wendy recognizes a project by its standard top-level `CMakeLists.txt`/`project.cmake` include or an `sdkconfig` file. Add a `wendy.json` with `"platform": "wendy-lite"`, then run:
```bash
wendy run
```
The connected device must run a firmware variant with native app support. Wendy reads its chip target, ensures ESP-IDF 5.5.4 is available through `eim` (offering to install `eim` with Homebrew when it is missing), runs `idf.py set-target` when needed, builds the project, uploads the native application firmware, reboots, reconnects, and streams its console output. ESP-IDF projects are detected automatically; `--build-type` does not need to be set.
See [ESP32 installation](../../../../../installation/wendy-lite-esp32/) for setup and a minimal project layout.
## Headless Mac — supported project types
Headless Mac (Darwin targets) currently runs native macOS apps only. When the selected agent reports `os: darwin`, `wendy run` rejects Linux/container deployment paths before any build, registry auth, or registry setup.
| Project type | Mac target support |
|---|---|
| Native SwiftPM (`Package.swift`, `platform: "darwin"`) | Supported |
| Native Xcode (`.xcodeproj`, `platform: "darwin"`) | Supported |
| Dockerfile / Containerfile / container image | Rejected |
| Python container path | Rejected |
| Docker Compose | Rejected |
| Multi-service `wendy.json` (`services` map) | Rejected |
| `platform: "linux/..."` or `platform: "wendyos"` | Rejected |
The error explains the project/target mismatch and tells you to set `platform: "darwin"` with a Mac-compatible native SwiftPM or Xcode project, or to target a Linux/WendyOS device. Linux container support on Mac is planned for a future release.
## Image build-args
When building a Dockerfile or Containerfile project, `wendy run` passes the target device's hardware parameters as `--build-arg` values so the build file can branch on platform, GPU vendor, or CUDA version. Declare any arg you want to use with `ARG`:
On Apple silicon Macs with Apple's `container` runtime, Wendy tries
Apple Container first when `--builder` is omitted. If Apple Container is
unavailable or the build fails, Wendy falls back to Docker. Use
`--builder docker` to force Docker, or `--builder apple-container` to require
Apple Container:
```sh
wendy --device my-wendy.local run
```
Wendy automatically checks for the `container` CLI and offers to install it via Homebrew if missing, and starts the `system` and `builder` services if they are not running.
For builds deployed to a WendyOS device, `--builder buildkit` uses buildctl and
exports an OCI image for Wendy's existing deployment path. If you explicitly
start the optional Local Build Service from the Wendy menu-bar app, the CLI
discovers its private socket automatically. `WENDY_BUILDKIT_HOST`,
`BUILDKIT_HOST`, and buildctl's normal local-daemon default remain supported.
This service only solves and caches builds; Apple `container` continues to run
local Mac applications.
If Apple Container reports an empty build context for a project under `/tmp` or
`/private/tmp`, Wendy returns an error with the known workaround: move the
project to a non-`/tmp` directory and retry.
For local-only Dockerfile or Containerfile runs on the Mac itself, use `wendy run --device
apple-container` instead. Compose projects still require the Docker provider for
local runs, but compose service builds targeting a WendyOS device can use
`--builder apple-container`.
The interactive device picker hides local run targets (this machine,
Docker/OrbStack, Apple Container) by default so it lists separate WendyOS
devices first. Select one explicitly with `--device` (as above), or set
`WENDY_SHOW_LOCAL_DEVICES=1` to list them in the picker.
| Build-arg | Values | Notes |
|---|---|---|
| `WENDY_PLATFORM` | `nvidia-jetson` \| `generic` | Platform tier derived from the device type |
| `WENDY_DEBUG` | `true` \| `false` | Set when `--debug` is passed |
| `WENDY_DEVICE_TYPE` | e.g. `jetson-agx-orin` | Raw device type; absent when unknown |
| `WENDY_HAS_GPU` | `true` \| `false` (hardware presence) | Absent on older agents |
| `WENDY_HAS_CUDA` | `true` \| `false` (host CUDA support) | Falls back only to an explicit NVIDIA vendor on older agents |
| `WENDY_GPU_VENDOR` | e.g. `nvidia`, `qualcomm` | Absent when no GPU is reported |
| `WENDY_JETPACK_VERSION` | e.g. `6.0` | Jetson only |
| `WENDY_JETPACK_MAJOR` | e.g. `6`, `7` | Jetson only; JetPack major for per-generation base-image selection |
| `WENDY_CUDA_VERSION` | e.g. `12.6` | Jetson only |
| `WENDY_GPU_ARCH` | e.g. `sm_87` | GPU architecture identifier; absent when no GPU is reported |
`WENDY_PLATFORM` and `WENDY_DEBUG` are always set. The remaining args are only injected when the agent reports them, so Dockerfiles and Containerfiles can define their own `ARG` defaults for devices that predate the field.
## Multi-service projects (`wendy.json` with `services`)
When `wendy.json` contains a `services` map, `wendy run` automatically switches to the multi-service path:
1. All service images are built in parallel (up to 4 concurrent builds). In an interactive terminal a per-service spinner shows build progress; in non-interactive environments plain log lines are printed instead.
2. Containers are created individually in topological dependency order (services listed in `dependsOn` are created first).
3. All containers are started and their logs are streamed to stdout/stderr with a `[serviceName]` prefix per line.
Press **Ctrl-C** to stop all services. A 30-second graceful shutdown window is given before the CLI exits.
Use `--service ` to build and run only a specific service and its transitive `dependsOn` dependencies instead of the full set.
See [Multi-Service Apps with `wendy.json`](../../../apps/wendy-services.md) for a full walkthrough.
> **Note:** Every multi-service run rebuilds and re-pushes each service — the push-skip optimisation is currently inactive for multi-service deployments. See [Push-skip content verification](#push-skip-content-verification) for why.
> **Headless Mac:** Multi-service `wendy.json` projects are not supported when the selected target is Headless Mac. `wendy run` returns an error immediately. Target a Linux/WendyOS device for multi-service workloads.
## Compose projects
If the current directory contains a `docker-compose.yml` (or `compose.yml`) but no `wendy.json`, `wendy run` automatically runs it as a multi-service compose project. Each service is built, pushed, and started on the device in dependency order. See [Multi-Service Apps with Docker Compose](../../../apps/compose.md) for full details.
> **Headless Mac:** Compose projects are not supported when the selected target is Headless Mac. `wendy run` returns an error before performing any registry or Docker setup. To deploy a compose workload, target a Linux/WendyOS device. For Mac targets, use a native SwiftPM or Xcode project with `platform: "darwin"`.
## Swift Package Manager projects (macOS)
From a macOS (Darwin) SwiftPM project, target the Mac agent explicitly:
```bash
wendy run --device :50051
```
When running a Swift Package Manager project on a macOS target, `wendy run`:
1. Builds the project with `swift build -c release` (or `-c debug` when `--debug` is passed). (This is the native macOS build path; for the cross-compiled Linux container target's build configuration, see [Swift Package Manager projects](./build.md#swift-package-manager-projects) in `build.md`.)
2. Resolves the build products directory via `swift build --show-bin-path`.
3. Syncs the compiled binary to the device.
4. Automatically syncs any sibling `.bundle` and `.resources` directories found in the build products directory alongside the binary, so SwiftPM resource bundles are available at runtime.
5. Syncs `sandbox.sb` from the project root if present, and any additional files declared under `files` in `wendy.json`.
6. If a `Brewfile.wendy` or explicitly configured `brewfile` is present, syncs it to the device and the agent runs `brew bundle` before starting the app.
## Swift Package Manager projects — host requirements
Both the macOS-target and Linux-target Swift paths shell out to a host Swift toolchain. The following host OS requirements apply when no `Dockerfile` or `Containerfile` is present (or when `--build-type=swift` is set explicitly):
| Target platform | Supported host OS | Notes |
|-----------------|------------------|-------|
| macOS device | macOS only | Linux's Swift toolchain cannot cross-compile to macOS. |
| Linux device | macOS or Linux | swift-container-plugin does not yet ship for Windows. |
On a **Windows host**, `wendy run` returns an actionable error for Swift projects that would require the host toolchain. Providing a `Dockerfile` or `Containerfile` bypasses these restrictions — the build is routed through the image build path, which works on all platforms.
## Remote build host
`--build-host` delegates the image build to another WendyOS device:
```bash
wendy run --build-host spark-office
```
The build runs on that device, and it delivers the finished image to the target
device over the mesh — LAN-direct when possible, via the cloud broker otherwise.
It addresses the provisioned target by asset ID without resolving a hostname, so
delivery does not depend on the build host resolving `device-.cloud.wendy.dev`.
The image never travels through your machine.
Delivery works the way `wendy run` deploys from your laptop: the build host asks
the device which layers and chunks it already holds and sends only the missing
bytes into its content store, so a rebuild that changed one layer transfers a
few chunks rather than the image. A link that drops mid-transfer is resumed —
chunks the device already staged are never re-sent — and a deploy you cancel
and re-run picks up where it stopped. A device whose agent predates chunked
delivery receives a registry push instead, and the build log says so.
`--chunking` governs this leg too; see [Deploy path: `--chunking`](#deploy-path---chunking).
Use a remote host for builds that need its GPU or CPU architecture, such as an
arm64 build that would otherwise use QEMU emulation on an x86 development machine.
A multi-service `wendy.json` builds remotely too: every selected service is
built on the host and delivered to the target. See
[Multi-service apps: remote build host](../../../apps/wendy-services.md#remote-build-host)
for how it differs from a local build. Compose projects cannot use `--build-host`.
Your machine does not need a container builder. With `--build-host`, the CLI
does not start Docker, Apple Container, or a local BuildKit daemon. The
`--builder` flag selects a local builder and cannot be combined with `--build-host`.
To set a default so you do not pass the flag every time, set `defaultBuildHost`
in the CLI config. The flag always wins over the default. This is a
per-developer setting rather than a project one, because the right build host
depends on which network you are on.
For a complete example, follow [Build Once, Deploy to Several Devices](../../../../../guides/fleet-deployment/).
### Requirements
- A Linux build host with the builder role enabled, BuildKit available, and
support for the target platform.
- A user certificate in the build host's organisation.
- A provisioned target device reachable from the build host over the mesh.
See [`wendy device build-host`](device/build-host.md) for setup, access rules,
cache-space requirements, and how shared builds use the host.
If a requirement is missing, `wendy run` fails and names the host. It does not
fall back to building locally.
### Errors
A failed remote build reports which half failed, because the fixes differ:
- *build on `` failed*: check your Dockerfile or Stagefile.
- *image built on `` but could not be delivered*: check mesh reachability
between the two devices and registry credentials on the build host.
## Watch mode
Pass `--watch` to rebuild and redeploy automatically whenever source files in the
project directory change:
```sh
wendy run --watch
wendy run --watch --debounce 800 --verbose
```
Watch mode runs **attached** and **non-interactive** (equivalent to `--yes`), so
the watch loop never blocks on a prompt. A rapid sequence of saves is coalesced
by the debounce window (default 400 ms) so a single redeploy runs after edits
settle. Build output is hidden unless a build fails; pass `--verbose` to always
show it, or `--debounce ` to tune the quiet period.
Logs remain visible for the whole watch session and continue across redeploys.
For multi-service apps, this includes output from unchanged services that remain
running. A new session starts with new output rather than replaying recent lines
from before watch began. With an older agent, a small number of recent lines may
appear once at startup. Each cycle reports itself after the changed containers
have started, readiness has completed, and any first-run actions have launched:
```text
↻ change detected — redeploying...
✓ redeployed in 1.98s
listening on :3000
```
If you save again during a redeploy, Wendy cancels that redeploy and moves on to
the latest change once cancellation finishes. Deploys do not overlap.
**`openURL` and `cli` postStart actions run once per watch session for each
container, after its first successful readiness check.** Later saves do not
reopen the browser or rerun the local command. If readiness is canceled or
fails, a later successful redeploy can still run the action. Restart watch to
run it again. In a multi-service project, each service and the top-level action
run once independently. `postStart.agent` runs on the device after every
corresponding container start.
Ctrl-C stops watching and leaves the app running on the device; use
`wendy device apps stop` to stop it. Add `--detach` to keep watching and
redeploying without streaming logs or running `openURL` and `cli` actions.
Attached watch requires a Wendy agent target. For provider targets, use
`--watch --detach`.
For multi-service `wendy.json` and Compose projects deployed to WendyOS, watch
redeploys only services whose build inputs or runtime configuration changed.
Unchanged services that are still running are not rebuilt, recreated, or
restarted. Changed services are redeployed in dependency order. Missing or
stopped services are deployed again even when their files have not changed.
When the primary of a `shared-network` or `shared-ipc` group changes, the group
is restarted together because its other containers share that primary's Linux
namespaces.
Watch mode does not forward stdin to a container. Use a plain `wendy run` for
an app that reads stdin.
> **Note:** `wendy watch` is a hidden alias for `wendy run --watch`. Prefer
> `wendy run --watch`; both forms accept `--detach`.
## Deploy path: `--chunking`
`wendy run` normally attempts a fast content-based chunking (CBC) chunk-diff deploy and falls back to a full registry push when it fails (`auto`, the default). Use `--chunking` to override this:
| Value | Behaviour |
|-------|-----------|
| `auto` (default) | Try chunk-diff; fall back to a registry push on failure. |
| `force` | Use chunk-diff only. If chunk-diff fails the error is returned and no registry-push fallback is attempted. Cancellation still exits cleanly. |
| `off` | Skip chunk-diff entirely; go straight to the registry push. |
> **Note:** With `--build-host`, the same modes govern the build host's delivery
> to the device. `auto` delivers by chunks and falls back to a registry push only
> for a device whose agent predates chunked delivery, saying so in the build
> log. `force` turns that fallback into a delivery failure, and is refused up
> front against a build host too old to honour it. `off` takes the registry
> route for every device, as build hosts delivered before chunked delivery
> existed.
> **Note:** When `--deploy` is also passed, `--chunking force` and `--chunking off` are no-ops — `--deploy` always uses the registry path because it must create the container without starting it.
> **Note:** The `postStart` hook fires on both the chunk-diff and registry-push
> paths. The deploy path does not affect hook execution.
Any value other than `auto`, `force`, or `off` is rejected with an error before the build starts.
## Environment variables
Environment variables reach the container from two places:
```sh
wendy run --env LOG_LEVEL=debug --env OTEL_LOGS_EXPORTER=console
```
and the `env` map in `wendy.json`, which is where they belong when they are part of the app rather than one run of it:
```json
{
"appId": "my-app",
"env": {
"LOG_LEVEL": "info",
"API_TOKEN": "${MY_API_TOKEN}"
}
}
```
`${VAR}` (or `$VAR`) is expanded from the deploying machine's environment, so secrets stay out of the file. An entry whose value expands to empty is dropped, leaving whatever the image itself sets.
For a multi-service app, the top-level `env` is the default for every service and a service's own `env` overrides it key by key. `--env` overrides both.
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.
## Push-skip content verification
When a detached run (`--detach`) finds that nothing has changed since the last successful deploy to this device, `wendy run` can skip the build and push entirely and just ensure the existing container is running. So this never leaves the device on stale or partial content, the skip is content-verified — it happens only when **all** of the following hold:
1. The build inputs (context, Dockerfile/Containerfile, platform, and build-args) hash the same as the last deploy.
2. A local deploy record for this app on this device exists and lists the image layer diff IDs that were deployed.
3. The device confirms it still holds every one of those recorded layers.
If any check fails — an older agent that cannot answer the layer query, a layer garbage-collected on the device, a partial push, or a locally rebuilt base image that never changed the input hash — `wendy run` falls back to a full build and push, recording fresh layer IDs on success.
Deploy records written before this version carry no layer IDs, so they cannot be verified and never skip. In practice:
- The first deploy after upgrading always does a full build and push.
- A legacy record (or any record without verifiable layer IDs) is treated as unverifiable rather than skipped, so you see a full rebuild with unchanged inputs instead of a silent skip onto possibly-stale content.
> **Note:** Push-skip is currently inactive for multi-service deployments. Registry-push content cannot be verified via layer diff IDs, so every multi-service run rebuilds and re-pushes each service; a registry-digest pre-check to restore the optimisation is planned.
## postStart hooks
In an attached run, `wendy run` runs `openURL` and `cli` postStart actions after
the app reports readiness. This applies to both registry-push and chunk-diff
deploys. If readiness fails, Wendy skips these actions and prints a warning.
`--detach` returns after the selected containers start and does not run
readiness checks (unless `--wait-ready` is set), `openURL`, or `cli`;
`postStart.agent` still runs on the device. See [Readiness and lifecycle hooks](../../../apps/wendy-services.md#readiness-and-lifecycle-hooks)
for multi-service details.
`--deploy` creates the app without starting it, so no postStart action runs.
Under attached `--watch` the host-side actions run after the first successful
readiness check only. `--watch --detach` skips them; see [Watch mode](#watch-mode).
> **Note:** When the CLI connects to the device at an IPv6 address (for example, one discovered via mDNS), the hook targets the device's best self-reported IP address instead — the same address shown in the `App reachable at` line — for both `openURL` and `cli`. This avoids pointing at a rotating RFC 4941 temporary privacy address that may not be reachable later. If the device cannot be queried, the dialed address is used (and bracketed for URL safety in `openURL`).
### `openURL`
`openURL` opens a URL in the developer's default browser without a shell. It works uniformly on macOS, Linux, and Windows and is the recommended way to open a URL at startup:
```json
{
"hooks": {
"postStart": {
"openURL": "http://${WENDY_HOSTNAME}:3001"
}
}
}
```
When `${WENDY_HOSTNAME}` is substituted and the device address is an IPv6 literal, `wendy run` automatically brackets it (e.g. `2001:db8::1` → `[2001:db8::1]`) so the resulting URL is parseable by browsers. Zone IDs are percent-escaped per RFC 6874. IPv4-mapped IPv6 addresses (`::ffff:x.x.x.x`) are unmapped to plain IPv4. The `cli` hook receives the raw (unbracketed) address.
If the browser cannot be opened, a warning is printed and `wendy run` continues normally. `openURL` is fire-and-forget and does not affect the process tracked by `wendy run`.
`openURL` opens a browser only when `wendy run` runs in an interactive terminal without `--json`. In CI, from a coding agent, or with piped output, it prints the URL instead.
### `cli`
`cli` runs a free-form shell command on the developer's machine. It is dispatched through the platform shell (`sh -c` on Unix, `cmd.exe /S /C` on Windows). `wendy run` tracks this child process for waiting and cancellation; the returned handle is used to clean up when `wendy run` exits.
`openURL` and `cli` can be set together — `openURL` fires first, then `cli` is spawned.
> **Note:** `open`, `xdg-open`, and `start` inside `cli` are platform-specific. Use `openURL` to open a URL portably. WendyOS warns at config load time when `hooks.postStart.cli` begins with one of these commands.
### Hook process lifetime
On **Windows**, the entire process tree spawned by a `cli` hook — including grandchildren started via `start /B` — is terminated when `wendy run` exits or is interrupted. If the primary mechanism is unavailable, `wendy run` falls back to `taskkill /T /F`, which terminates the direct child and its descendants as long as the parent process is still alive.
On **Unix**, the default shell process-group cleanup is sufficient; no additional termination logic is applied.
### Attached-mode hook lifetime
In a normal attached run, the `cli` hook process is tied to the run. When the
container exits or you press **Ctrl-C**, Wendy cancels the hook and waits for it
to exit before returning. In watch mode, the hook is tied to the watch session
and is canceled when you stop watching.
Detached mode (`--detach`), deploy-only mode (`--deploy`), and
`--watch --detach` do not fire the host-side hook at all, so there is no child
process to reap. Attached watch hooks are owned and reaped by the watch session.
## Container image signature
`wendy run` optionally includes a detached **ML-DSA65** signature with every `RunContainer` call. The agent verifies the signature over the SHA256 digest of the OCI image config before assembling or starting the container.
Set `WENDY_IMAGE_SIGNATURE_PATH` to the path of the detached signature file; when the variable is unset or points to an empty file, no signature is sent. Verification is currently dormant on the agent side (the per-org publisher key is not yet wired in), so omitting the signature does not block container creation today. Once the publisher key is provisioned, sending an unsigned or tampered image causes the agent to refuse the run.
CUDA-selecting Dockerfiles must use `WENDY_HAS_CUDA`. `WENDY_HAS_GPU`
reports hardware presence, including Broadcom and other GPUs without CUDA.
Device info exposes `gpuCapabilities`, one entry per detected GPU with its
`vendor`, `path`, and `computeBackends` (`cuda`, `rocm`, `metal`). A GPU whose
backend list is empty has no supported backend; no entries at all on a device
that reports a GPU means an older agent. An NPU runtime such as `qnn` is
reported separately, in `npuBackends`. `containerStorage` identifies the filesystem used by
containerd; the existing disk scalar fields continue to describe the root filesystem.
Attached runs keep observing slow startup after the initial readiness budget.
If the relevant service is still running when its probe timeout passes, the CLI
reports “still starting” and checks every five seconds while streaming logs,
for up to ten probe timeouts in total, then warns. `--readiness-timeout` is the
whole deadline instead: no extended observation follows it. Browser opening and
host `postStart` commands run once readiness succeeds. Stopping the service,
canceling the session, or replacing a watch deployment cancels its probes. With
`--wait-ready`, its own check replaces this observation. Detached and
create-only runs skip host `postStart` commands; a detached run checks
readiness only with `--wait-ready`.
## Native commands on Mac
A single native Darwin app can name a target executable directly:
```json
{
"appId": "sh.example.chat",
"platform": "darwin",
"files": [{"path": "launcher.py"}, {"path": "data"}],
"run": {"command": "/usr/bin/python3", "args": ["../launcher.py"], "cwd": "data"},
"env": {"MAX_MODEL": "HuggingFaceTB/SmolLM2-135M-Instruct"}
}
```
`run.command` is an absolute executable on the target or an executable relative
to the synced app directory. Relative scripts must have executable permission.
`run.cwd` defaults to the app directory and must stay inside it, including after
symlink resolution. Arguments are passed directly; shell expressions are literal
unless you explicitly choose a shell executable. The CLI selects this mode before
project detection and rejects conflicting build flags and non-Darwin targets.
The target must advertise `native-process`; update Wendy Agent for Mac if the
CLI requests it. Declared files, `wendy.json`, an optional `sandbox.sb`, and the
configured Brewfile (or auto-detected `Brewfile.wendy`) use native file sync.
Homebrew installation completes before the agent validates the executable.
Request environment values apply to native command, SwiftPM, and Xcode launches;
agent identity and telemetry settings take precedence. `WENDY_APP_ID` identifies
the app. Keep runtime data outside the synced source directory, for example under
`~/Library/Application Support//runtime/`, to retain it across file sync.
Attached watch compares the command, working directory, arguments, effective
environment, files, configuration, and restart policy. Unchanged running commands
remain running. Host hooks and browser opening wait for readiness.
## Diagnosing an interrupted log stream
Quiet log subscriptions receive an empty transport heartbeat every 15 seconds.
The CLI and MCP omit these messages from logs, history transitions, and batch
counts. Go agent gRPC keepalive acknowledgements allow 20 seconds. A stream that
ends with a connection error still reports that error.
For a recurrence, record the CLI, agent, and OS versions, timestamps, device
address, direct WiFi or cloud route, and the exact gRPC error. Capture agent and
cloud tunnel logs for the same interval; compare an idle subscription with one
that emits a new log after ten minutes. Check WiFi roaming, link loss, NAT/proxy
idle limits, and HTTP/2 GOAWAY/keepalive diagnostics before assigning a cause.
The original direct-WiFi failure has no confirmed transport root cause.
## Cloud catalog registration
Before deploying to an enrolled device, `wendy run` registers each distinct
`appId` in Cloud Apps using an operator session matching the device's Cloud
endpoint and organization. Existing entries retain their metadata and grants.
Multi-service and Compose deployments register shared app IDs only once.
Registration does not enable notification permission; an owner or admin must
grant it in Cloud app settings.
Registration errors stop deployment. Use `--skip-cloud-registration` for an
offline deployment, then deploy again without it to register later. Unenrolled
devices skip registration automatically.
## Recording a release
`--record-release ` records the deploy in Cloud as a release of the app:
the CLI creates the release under the tag before the deploy, the device labels
the container with it, and the CLI stores the image digest the device reports.
It is off by default. Without it nothing is recorded, and the device still
reports the app, which Cloud keeps as an unassigned app. It needs a
single-container app and one WendyOS device enrolled with Wendy Cloud, checked
before anything is built. It does not combine with `--watch`,
`--skip-cloud-registration`, several devices in one run, Compose projects or
provider targets such as `--device docker`. To record a release without
deploying, use [`wendy app release`](app/release.md).
---
# wendy tour
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/tour/
---
title: "wendy tour"
description: "Watch a simulated Go2 walk in your browser, or set up your first physical Wendy device."
---
# `wendy tour`
Watch a simulated Go2 walk in your browser, or set up your first physical Wendy device.
## Choose your path
```sh
wendy tour
```
In an interactive terminal, **Walk a simulated robot** appears first and is
selected by default. Choose **Set up a physical device** when you have hardware ready.
You can select either route directly:
```sh
wendy tour --simulator
wendy tour --hardware
```
## Simulated robot
The managed Go2 virtual device requires macOS or Linux, QEMU with ARM64 firmware,
and a Docker builder with Buildx for the initial build or runtime updates. It uses
four virtual CPUs and 4 GiB of memory. Apple Silicon uses hardware acceleration;
x86 hosts use slower emulation. The first run downloads the OS and robot assets
and builds the runtime.
Allow several minutes and disk space for the images. No physical robot, cloud
account, or host Python installation is required.
On macOS with Homebrew, the tour offers to install QEMU and repair missing
firmware. On Linux with apt-get or dnf, it offers to install QEMU and its
firmware; that install runs `sudo`, which may ask for your password. On macOS it
also offers to install or start a Docker runtime when a build needs one; on
Linux, install and start Docker yourself. These prompts default to Yes. A healthy
existing simulation can run again without starting Docker.
See the [quickstart](../../../../../#try-the-robot-tour) for prerequisites. Windows users
can use the physical-device route or the separate
[local Docker robot demo](https://wendy.dev/quickstart?target=simulator).
For device inspection with a generic virtual device on native Windows, see
[Windows foreground VMs](../../../../../installation/wendyos-virtual-machine/#windows-foreground-vms).
To build and deploy local apps to a VM on Windows, run the Linux Wendy CLI, QEMU,
and your container builder inside WSL2.
The tour creates a dedicated Go2 VM and opens its browser viewer. The robot walks
a short arc. Success requires a measured change in its position and confirmation
that the movement stopped. An interactive guide then helps you program the robot,
change its walk, or drive it in the browser.
### Program your robot
Choose **Build a robot app** after the first walk. Pick an idea:
| App | What you can build |
| --- | --- |
| Patrol | Follow a square or your own waypoints. |
| Roam | React to lidar readings and avoid obstacles. |
| Teleop | Build a browser remote control. |
| Sensors | Display the camera, lidar, and robot state. |
The guide creates an editable Python project under your tour's `apps` directory,
shows you a first change to make, and offers to open the code in your editor.
Choose **Deploy** to run it on this tour's virtual Go2, then edit and deploy again
without leaving the guide. Returning to a project preserves your edits.
These apps use ROS 2 to read sensors and send movement commands. The simulator
handles balance and walking. Driving apps have explicit controls; deploying
Patrol alone does not start its route. The next menu selects **Start** or **Open
your app** for you. Returning to the main guide stops movement apps and releases
their robot controls, so you can try another idea or drive in the viewer. Your code
stays saved for the next deployment.
Choose **Finish for now** when done. The guide offers to stop the simulator or
leave it running, and shows how to resume later. Ctrl+C exits the current tour.
### Change the movement and repeat
Choose **Change the walk** to try a straight line or turn the other way immediately.
The tour saves your choice in `~/Documents/wendy-robot-tour/tour.json`. You can also
edit `turn_rps` to `0` for a straight walk, then rerun:
```sh
wendy tour --simulator
```
This reuses the saved VM. You can edit `duration_seconds` within the supported
range of 1 to 8 seconds, or `turn_rps` between -1 and 1. The starter's
`forward_mps` is bounded between 0.55 and 0.6 metres per second. These settings
control the simulated robot only.
To keep a separate starter and VM:
```sh
wendy tour --simulator --project ./my-robot-tour
```
If a step fails in an interactive terminal, the tour explains the error and offers
to retry that step. Fix the issue and press Enter to continue, or decline and
resume later with the same command. Existing VMs are reused. If VM creation was
interrupted, the tour preserves the incomplete files and restarts creation under
the saved VM name. If another controller owns the simulated robot, release
its controls in the viewer before retrying. Ctrl+C interrupts the demonstration;
use the printed VM stop command when you also want to shut down its virtual device.
## Physical device
`wendy tour --hardware` guides you through device setup, discovery, optional
Wi-Fi configuration, and a sample app. Use an interactive terminal. Check the
[board-specific setup guide](../../../../../#2-set-up-a-device) for storage, power,
recovery mode, and cable requirements. Review the selected storage before
confirming a flash, which erases that storage.
At the Wi-Fi step, the wizard tries to prefill the host computer's current SSID.
If it cannot detect one, enter the network name yourself. Cloud enrollment and
coding-agent setup are optional. The physical-device route does not command
robot motion.
### Cancellation and exit status
Declining an installer's erase confirmation cancels that setup step. The tour
does not treat it as a completed installation or proceed to first-boot discovery.
An installer launched by the tour returns exit status `130` to its parent when
cancelled. The tour translates that internal signal into user cancellation. A
normally launched `wendy tour` exits with status `0` for this cancellation, as it
does when you quit the wizard, so status `0` alone does not prove setup completed.
Other failures use the CLI's [exit status contract](../global-flags.md#exit-status).
---
# wendy update
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/update/
---
title: "wendy update"
description: "Keep your Wendy CLI current without guessing which installer to use."
---
# `wendy update`
Keep your Wendy CLI current without guessing which installer to use.
```sh
wendy update
```
Wendy identifies the running CLI's installation and checks the latest stable
release for stable installations. Nightly updates use their package manager
without a stable-release lookup. Wendy shows the update commands before asking
for confirmation. The prompt defaults to Yes. To approve an update in a script
or AI terminal:
```sh
wendy update --yes
```
Without an interactive terminal, `--yes` is required before any package manager
runs. JSON output also requires `--yes` when updating.
## Check without installing
```sh
wendy update --check
wendy update --check --json
```
`--check` makes a fresh stable-release request, including for nightly installations.
It does not install software, write the update cache, refresh authentication, or
emit update telemetry. If the release service cannot be reached, the command
fails rather than reporting that your CLI is current.
The JSON report contains `current_version`, `latest_stable_version`, `status`,
`update_available`, and `installation`. The installation includes its method,
channel, executable path, supported update commands, and any manual instructions.
These local paths are printed for troubleshooting, not included in update
telemetry. An available update is a successful check and exits with code 0.
## Installation methods
| Installation | What happens |
| --- | --- |
| Homebrew stable | Runs `brew update`, then `brew upgrade wendylabsinc/tap/wendy`. |
| Homebrew nightly | Keeps the nightly channel and upgrades `wendylabsinc/tap/wendy-nightly`. The stable release check does not establish whether nightly is current. |
| Recognized WinGet package | Runs `winget upgrade --id WendyLabs.Wendy --exact --source winget`. |
| Debian / Ubuntu package | Refreshes APT and upgrades the exact owning package with `apt-get install --only-upgrade`. |
| RPM package | Upgrades the exact owning package with DNF or YUM. |
| Arch / AUR package | Prints the owning package's upgrade instructions. |
| Manually copied binary or unrecognized installation | Prints instructions to update that installation explicitly. |
| Development build | Leaves the source-built binary alone. |
APT and RPM updates use sudo when needed. In CI or without a terminal, sudo runs
without a password prompt; arrange elevation first or follow the manual
instructions. Package confirmations are approved only after the Wendy confirmation
or explicit `--yes`. A package installed from a downloaded file may need a newer
file if no repository provides it.
For a manual update, complete the printed instructions,
then run `wendy update --check` again. An unsupported automatic update exits with
an error and instructions; it does not claim that an update happened.
## Verify the result
After an automatic update, Wendy runs the newly PATH-resolved `wendy --version`.
It only reports success when that executable is newer than the running CLI and
has reached the expected stable version, when one was supplied. A failed package
command, pinned package, or old executable still taking precedence on PATH is
reported as a failure.
If a second Wendy binary shadows the running CLI, correct PATH first. Use
`type -a wendy` on macOS/Linux or `Get-Command wendy -All` in PowerShell to find
copies. A package repository can also lag behind the published release; retry
after the package is available.
Restart the Wendy MCP server or your AI app after updating. An existing server
process continues using its old binary and tools until restarted. Rerun
`wendy mcp setup` if you also want to refresh the bundled assistant skills.
## Automatic reminders
Ordinary commands check for releases in the background. A successful check is
cached for 24 hours; a failed or cancelled check can retry after one hour. The
reminder waits at most one second for a background release request, with separate
bounded installation and cache lookups, so an unavailable release service does
not hold up your command.
In an interactive terminal, you can accept an update immediately. Declining
snoozes that release's prompt for 24 hours. JSON output and automated invocations
never open a prompt. A command that failed can show a short, noninteractive
reminder while preserving its original error and exit status. An update failure
after a successful command also preserves the original command's success.
CI, read-only checks, and completion helpers skip background reminders. Set
`WENDY_NO_UPDATE_CHECK=1` to disable background checks and notices; an explicit
`wendy update` or `wendy update --check` still works.
These behaviors become available after installing a release that includes them.
If your older CLI does not recognize `update`, upgrade it once using its existing
package manager or original installation method, then verify `wendy --version`.
---
# Open Browser
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/commands/utils/open-browser/
---
title: "Open Browser"
description: "Opens a URL using the preferred mechanism on the current OS."
---
Opens a URL using the preferred mechanism on the current OS.
This is useful for `wendy.json` postRun scripts that want to open your (web) app in a web-browser.
---
# Device Selection
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/device-selection/
---
title: "Device Selection"
description: "Many CLI operations run on the remote device. When a connection is to be established, the CLI goes down a list in order to connect to this device."
---
Many CLI operations run on the remote device. When a connection is to be
established, the CLI goes down a list in order to connect to this device.
## 1. `--device`
If the `--device` flag is specified, a connection is made against that target IP
address or hostname. Include a port when the target should not use the default
agent port:
```sh
wendy --device 192.168.1.42 device apps list
wendy run --device my-mac.local:50051
```
Failing to connect to an explicit device results in a failure.
A value made only of digits, such as `283`, is rejected: it is not a hostname or IP address, and the system resolver would turn it into an unrelated address. If it is a Wendy Cloud asset ID, use its cloud selector (`--device cloud:///org//asset/283`) or `wendy cloud device --device 283`. The error prints the exact selector when you are logged in.
> **TODO (test)**: If the target device is outdated, and `--json` is not specified, a warning will be printed to indicate an update is available.
## 2. `WENDY_DEVICE`
If `--device` is not given and the `WENDY_DEVICE` environment variable is set, the CLI connects to that device exactly as if it had been passed with `--device`. Use it to give one terminal, script or AI session its own target without changing the saved default that every session shares:
```sh
WENDY_DEVICE=my-pi.local wendy run
```
`--device` still takes precedence. A blank value is ignored.
## 3. Default Device
If a Default Device is set using [`wendy device set-default`](./commands/device/set-default.md),
the CLI attempts to connect to it. The saved value may be a hostname, IP
address, provider key, or explicit `host:port` value.
```sh
wendy device set-default my-mac.local:50051
wendy device info --json
wendy run
```
Use [`wendy device get-default`](./commands/device/get-default.md) to check
the current default. If the default device cannot be reached from an
interactive terminal, the CLI shows the picker so you can select another
device. In non-interactive shells, the command fails with a clear message
that the default is set but unreachable instead of opening a picker.
> **TODO (test)**: If the target device is outdated, and `--json` is not specified, a warning will be printed to indicate an update is available.
## 4. Show Picker
The picker finds nearby [WendyOS](../../../../#2-set-up-a-device), Wendy Agent and
[Wendy Lite](../../../../advanced/wendy-lite/) devices using local-network and Bluetooth discovery.
Finding a device here is separate from enabling
[Wendy Net Bluetooth connections](../../../../hardware/bluetooth/#wendy-net-over-bluetooth)
between devices.
A device picker is shown only when the terminal is interactive, so a user can
select their target device for the current command invocation.
Cached LAN devices stay hidden until their agent answers a probe or mDNS
resolves their service during the current scan.
When logged into Wendy Cloud, press `e` on a highlighted Local device to run
the `wendy device enroll` flow using the active cloud account. Enrollment
requires a LAN or USB network connection. After enrollment, the picker reopens;
press Enter to select a device for the original command.
Headless Mac advertises over Bonjour/mDNS as `_wendyos._udp` and appears
as a LAN device when local discovery is allowed by the network and macOS Local
Network permissions.
The picker table shows the same columns as
[`wendy discover`](./commands/discover.md#interactive-tui). The leading
marker column displays provisioned state (`●`/`○`) for LAN devices alongside
the `✦` default marker. If the CLI cannot verify the highlighted device's agent,
a footer hint explains what went wrong and what to try next.
In scripts, CI, SSH sessions without a TTY, or any other non-interactive
context, no picker is shown. Pass `--device`, or configure a default with
`wendy device set-default` (confirm it with `wendy device get-default`),
before running commands that need a target.
> **TODO (test)**: If the target device is outdated, and `--json` is not specified, a warning will be printed to indicate an update is available.
> If the terminal is interactive, a prompt will be made to update the device right now.
## Simulator startup and authentication failures
While waiting for a simulator to boot, the CLI retries connection failures
such as an agent that is not yet listening. It stops immediately if the
connection fails because of a TLS authentication rejection, a device identity
refusal, an organization mismatch, or a gRPC `Unauthenticated` or
`PermissionDenied` response.
A TLS authentication failure can mean a wrong device clock or incompatible
credentials. If the device clock may be wrong, run `wendy device sync-time`
on the same LAN, then retry. Refresh certificates when the CLI reports expired
client credentials, after checking this computer's clock. Use
`WENDY_TLS_DEBUG=1` for connection details.
Authentication failures omit the simulator's boot-console output. Ordinary
startup timeouts still include the available boot-console tail to help
diagnose guest startup problems.
## Automatic connection recovery
After a possible clock-related TLS failure, the CLI attempts to broadcast a
signed time proof on the LAN and retries the connection. It reports whether
sending failed and whether the retry connected; sending a proof does not confirm
that the device received it or changed its clock.
The CLI offers to refresh certificates when it has evidence that the client
credentials need renewal. A timeout or generic TLS rejection alone does not
trigger that offer.
## Local Targets
The picker can also show local provider targets when the host supports them.
These are not WendyOS devices.
### Docker
Use Docker for local container runs. Dockerfile, Containerfile, and Compose projects run
through the local Docker daemon. On macOS and Windows, Docker runs Linux
containers inside Docker's Linux environment rather than as native macOS or
Windows processes.
Use this target when you want to test container build and runtime behavior
without deploying to WendyOS hardware. Hardware-specific WendyOS behavior, device
entitlements, and target filesystem assumptions still need a real WendyOS target.
You can select it directly with:
```sh
wendy run --device docker
```
### Apple Container
On Apple silicon Macs, Wendy can use Apple's `container` CLI for local
Dockerfile and Containerfile runs without Docker Desktop:
```sh
wendy run --device apple-container
```
Wendy automatically checks for the `container` CLI and offers to install it via Homebrew if missing, and starts the `system` and `builder` services if they are not running.
Use this target when you want to build and run a single Dockerfile or
Containerfile project with Apple's lightweight Linux container runtime. Compose
projects still require the Docker target.
To deploy to a WendyOS device while using Apple Container only as the image
builder, keep `--device` pointed at the WendyOS device. On Apple silicon Macs,
Apple Container is tried first by default when it is installed and running, then
Docker is used as a fallback. Set `--builder apple-container` to require Apple
Container, or `--builder docker` to force Docker:
```sh
wendy --device my-wendy.local run --builder apple-container
```
### Local
Use the local target for host-native apps. The app runs directly on the computer
that is running the `wendy` CLI:
- On macOS, it runs as a macOS process.
- On Windows, it runs as a Windows process.
- On Linux, it runs as a Linux process.
The local target is intended for native Swift, Go, and Python projects. It does not
run inside Docker's Linux environment, does not emulate WendyOS, and does not
provide WendyOS container semantics, hardware entitlements, or device filesystem
layout.
You can select it directly with:
```sh
wendy run --device local
```
---
# Global Flags
Source: https://docs.wendy.dev/latest/advanced/clients/wendy-cli/global-flags/
---
title: "Global Flags"
description: "These flags are available on every wendy command."
---
# Global Flags
These flags are available on every `wendy` command.
## `--json`
Outputs command results as JSON instead of the default interactive TUI or table format.
```sh
wendy device list --json
```
For ordinary single-device deployments to a Wendy agent,
`wendy --json run --detach` emits one result on stdout, with progress and
pre-start logs on stderr.
See [`wendy run` — Detached output](./commands/run.md#detached-output) for the
result shape and supported modes. Attached run and build commands still emit
progress/log output.
When stdout is not a TTY (for example, when piping output, running in CI, or executing from a script), `--json` is automatically enabled. An explicit `--json` or `--json=false` always takes precedence over the automatic detection.
```sh
# JSON output without passing --json explicitly
wendy device list | cat
# Suppress JSON even in a non-TTY context
wendy device list --json=false | cat
```
In JSON mode stdout carries only JSON. Notices that change what a result means are written to stderr as single plain lines instead of being dropped: the device a command picked for you (`Using default device wendyos-abc.local.`) and certificate warnings. Onboarding hints such as "Next: run `wendy run` …" are not shown in JSON mode.
> **Note:** For live, full-screen TUI commands such as [`wendy device top`](./commands/device/top.md), `--json` does not stream the interface — it switches the command to a one-shot **snapshot** mode that prints a single JSON object and exits, instead of rendering the interactive dashboard.
### Errors in JSON mode
When a command fails in JSON mode, it writes nothing more to stdout, and the last line on stderr is one JSON object:
```json
{"error":{"code":"device_unreachable","exit":5,"message":"Could not connect to device at 192.168.1.42:50051. Is it powered on and connected to the network?","retryable":true,"next_steps":[]}}
```
| Field | Meaning |
|---|---|
| `code` | The failure category, for example `cli_usage`, `auth_required` or `device_unreachable`; `error` when the failure has no category. |
| `exit` | The process exit status (see [Exit status](#exit-status)). |
| `message` | The error as plain text, without colour or the `✗` marker. It can span several lines. |
| `retryable` | `true` when running the same command again, unchanged, can succeed, for example once the device is back online. |
| `next_steps` | Recovery steps, in order. Always an array, often empty. |
Earlier stderr lines can hold progress output and notices, so read the last line. Outside JSON mode the same failure prints as styled text. So does a failure in automatic JSON mode when stderr is a terminal, as in `wendy device apps list | grep my-app`: stdout is piped, but the error is read by a person. Pass `--json` explicitly to get the JSON error there too.
## Exit status
`wendy` exits with the same status whether or not JSON mode is on.
| Status | Meaning | `code` values |
|---|---|---|
| 0 | Success | |
| 1 | Any failure not listed below | every other code, including `error` |
| 2 | The command line is wrong: unknown command or flag, wrong number of arguments, a missing required flag, an invalid or conflicting flag value, or a confirmation that needs `--force` because there is no terminal | `cli_usage` |
| 3 | Credentials are missing, expired or ambiguous | `auth_required`, `auth_session_ambiguous`, `auth_certificate_failed`, `device_auth_required`, `registry_auth`, `grpc_unauthenticated` |
| 4 | No usable target device: none given, several match, or the device cannot run this project | `no_device`, `device_ambiguous`, `project_target_mismatch` |
| 5 | The device could not be reached: it refused or did not answer the connection, its host name did not resolve, or it rejected the TLS handshake | `device_unreachable`, `device_offline`, `device_not_resolved`, `device_tls_rejected`, `simulator_unavailable` |
| 6 | The build failed, or a build tool is missing | `build_failed`, `builder_unavailable`, `tool_not_found` |
| 7 | The app was deployed but did not start or stay up | `container_start_failed` |
| 8 | The app started but did not become ready in time | `readiness_timeout` |
| 10 | A trust decision only a person can make: the device's identity or organization changed | `device_identity_mismatch`, `device_org_mismatch` |
| 70 | An internal error: a bug in `wendy` itself. The stack trace is printed on stderr (in JSON mode before the envelope line, otherwise after the message); include it when you report the bug. Only a panic in the command's own goroutine is caught: one in a background goroutine still crashes with Go's own output and exit status | `internal_error` |
`device_not_resolved` means DNS reported that the host name does not exist. That can also happen while this machine is offline, so check the network before the name. A device name resolved over mDNS (a `.local` name, or a bare name such as `wendyos-abc`) that does not resolve is `device_unreachable` instead, since it resolves only while the device is on the network.
`retryable` is `true` for `device_unreachable`, `device_offline`, `transfer_failed`, `registry_unavailable`, `readiness_timeout`, and the timeouts `grpc_deadline` and `network_timeout`.
## `--device`
Specifies a target device by IP address, hostname, provider key, or explicit `host:port`, bypassing [device selection](./device-selection.md).
```sh
wendy --device 192.168.1.42 device apps list
wendy --device my-mac.local:50051 device info --json
```
When `--device` is not given, the CLI uses the `WENDY_DEVICE` environment variable if it is set, and only then the saved default device. Set `WENDY_DEVICE` per terminal, or in an AI tool's MCP server `env` block, to give each session its own target without changing the default all sessions share:
```sh
export WENDY_DEVICE=my-pi.local
wendy device info # my-pi.local
wendy --device my-mac.local:50051 device info # --device still wins
```
Commands that connect to a device this way print `Using device from WENDY_DEVICE.` The notice is not printed in `--json` mode, which the CLI also switches to on its own when it runs without an interactive terminal: from an AI tool's MCP server, a script, or a pipe.
## Automatic update notifications
Ordinary commands check for a newer release in the background. A successful check
is cached for 24 hours; a failed or cancelled check can retry after one hour.
In an interactive terminal, the CLI prompts `Update now?` with Yes selected.
Declining snoozes that release's prompt for 24 hours. Non-interactive and `--json`
invocations print notices to stderr without prompting.
Accepted updates use the installation's owning package manager. For Homebrew
stable, the commands are `brew update` and `brew upgrade wendylabsinc/tap/wendy`.
See [`wendy update`](./commands/update.md#installation-methods) for supported
methods, manual instructions, and verification after an update.
CI, read-only checks, and completion helpers skip background reminders. Set
`WENDY_NO_UPDATE_CHECK=1` to disable them for other commands. Explicit
`wendy update` and `wendy update --check` still work.
## Automatic shell-completion prompt
When shell completions aren't installed, the CLI offers — at most once per 24-hour window — to install them with an ambient `Install them now? [y/n]` prompt after a command finishes. It is never shown in non-interactive or `--json` contexts, on commands that handle completions themselves (`wendy completion …`, [`wendy tour`](./commands/tour.md)), or once completions are installed or the prompt is dismissed. See [`wendy completion`](./commands/completion.md#automatic-prompt-to-install-completions) for the full behavior.
Its state is persisted in `~/.wendy/config.json`:
| Field | Type | Meaning |
|---|---|---|
| `completionInstalled` | bool | Completions were installed through the CLI; permanently suppresses the prompt. |
| `completionPromptDismissed` | bool | The user answered `n` to the prompt; permanently suppresses it. |
| `lastCompletionPromptCheck` | RFC3339 timestamp | When the prompt was last shown; throttles it to once per 24-hour window. |
## Environment variables
| Variable | Description |
|----------|-------------|
| `GITHUB_TOKEN` | When set, the CLI uses it as a bearer token for GitHub API release checks and agent update lookups. When absent, those requests are made unauthenticated. |
| `WENDY_ANALYTICS` | Set to `false` to disable analytics. |
| `WENDY_APPSTORE_API` | Override the Wendy AppStore resolution API base URL used by `wendy app install` / `wendy device apps install`. Takes precedence over the built-in default; the `--api` flag takes precedence over this variable. |
| `WENDY_BUILD_PROGRESS` | Override the progress format requested from `docker buildx` during image builds. Accepts `plain` or `rawjson` (case-insensitive); any other value is ignored. When unset, the CLI probes the installed buildx version once per run and picks `rawjson` for buildx 0.13 or newer, `plain` otherwise. `rawjson` is what supplies per-step byte counters and download rates, so forcing `plain` reduces build output to step names only. |
| `WENDY_BUILDKIT_HOST` | Override the BuildKit endpoint used by `--builder buildkit`, for example `unix:///path/to/buildkitd.sock`. It takes precedence over `BUILDKIT_HOST`, Wendy's discovered `/runtime/buildkitd.sock`, and buildctl's normal local default. The daemon must use a containerd worker for `wendy build` image-store output. |
| `WENDY_CLOUD_INSECURE` | Set to `true` (or `1`) to dial Wendy Cloud over plaintext gRPC instead of TLS. Only for a local development cloud that serves plaintext; every cloud dial is TLS otherwise, whatever the port. The CLI prints a warning naming the address on every plaintext dial, because session tokens and enrollment credentials then travel unencrypted. The agent honors the same variable for its own enrollment dial. |
| `WENDY_DEVICE` | Target device for commands run without `--device`; takes precedence over the saved default device. Accepts the same values as `--device`. A blank value is ignored. |
| `WENDY_STAGEFILE_BACKEND` | Select the Stagefile compiler backend when `--stagefile-backend` is not passed. Accepts `dockerfile` (default) or experimental direct `llb`; the command-line flag takes precedence. |
| `WENDY_AGENT_SIGNATURE_PATH` | Path to a detached ML-DSA65 signature file for the agent update binary. When set, `wendy device update` includes the signature in the `UpdateAgent` RPC. Has no effect until a verification key is embedded in the agent. |
| `WENDY_IMAGE_SIGNATURE_PATH` | Path to a detached ML-DSA65 signature file for the OCI image config. When set, `wendy run` includes the signature in `RunContainer` calls. Has no effect until a per-org publisher key is provisioned on the agent. |
---
# Assets API
Source: https://docs.wendy.dev/latest/advanced/cloud/assets-api/
---
title: "Assets API"
description: "The AssetService gRPC service manages the inventory of assets (devices, peripherals, etc.) registered in a Wendy Cloud organization."
---
# Assets API
The `AssetService` gRPC service manages the inventory of assets (devices, peripherals, etc.) registered in a Wendy Cloud organization.
## Proto package
`wendycloud.v1` — defined in `Proto/cloud/assets.proto`. **The method reference
below documents v1; the separate v2 notes describe device-binding recovery.**
A parallel `wendycloud.v2.AssetService`
(`Proto/wendycloud/v2/assets.proto`) exists, identifies assets and organizations
by **string UUID** rather than `int32`, and adds `ListAssetsByApp` and
`FilterAssets`. The CLI's OIDC path already calls v2 exclusively; legacy
sessions still use the v1 service described here.
## v2 device-binding and deletion evidence
The following applies to `wendycloud.v2.AssetService` only; the methods below
continue to describe numeric v1. Compatible v2 consumers can resolve the exact
enrollment and recover deletion evidence without a local CLI journal.
### v2 `GetAsset`
| Request field | Type | Meaning |
|---|---|---|
| `id` | `string` | Asset UUID; may be empty when both selectors below are supplied. |
| `organization_id` | `optional string` | Exact tenant UUID; required with `device_id` when `id` is empty. |
| `device_id` | `optional string` | Exact PKI device UUID; required with `organization_id` when `id` is empty. |
An authorized active binding returns `Asset`. Removed assets return plain
`NOT_FOUND`; this is not deletion evidence and does not authorize local reset.
### v2 `DeleteAsset`
The optional string `expected_device_id` is a compare-and-delete guard: it must
match the exact enrolled device binding when supplied. Cloud authorizes
`device:delete`, revokes the device's certificates, removes the active asset and
records the deletion audit. A failed revocation does not delete the active asset.
For a local reset, also supply `reset_certificate_sha256` for the installed
certificate. Cloud returns a short-lived `reset_authorization` signed for that
exact request and operator. Its embedded `DeletedAsset` records the deletion:
| `DeletedAsset` field | Type | Meaning |
|---|---|---|
| `id` | `string` | Deleted asset UUID. |
| `organization_id` | `string` | Owning tenant UUID. |
| `device_id` | `string` | Device UUID bound to this asset. |
| `deleted_at` | `google.protobuf.Timestamp` | Recorded deletion time. |
An authorized exact-asset retry can obtain fresh reset authorization from the
original audit record. It preserves the deletion identity and time, checks
current permissions, and refuses if the same device identity has been enrolled
again. Verify the Cloud signature and exact request bindings before trusting the
embedded deletion. A successful delete response alone does not prove that the
device erased its local credentials.
### Device reset
Deleting a Cloud asset revokes its Cloud access; it does not erase credentials
from an offline device. Use [`wendy device unenroll`](../../../advanced/clients/wendy-cli/commands/device/unenroll/)
to coordinate Cloud removal and a local reset. The
[unenrollment guide](../../../device/unenrollment/) explains recovery and supported devices.
For API integrations, the agent advertises `cloud_authenticated_reset_supported`
when it supports Cloud-authorized reset. The older
`cloud_unenrollment_supported` flag is not sufficient. After erasure, the agent
may return `NotProvisionedResponse.unenrollment_completion`, a signed receipt for
the prior enrollment. It is historical completion evidence, not the identity of
the device currently answering. See the repository's
[reset protocol reference](https://github.com/wendylabsinc/WendyOS/blob/main/go/docs/v2-device-unenrollment.md)
for validation requirements.
### v2 reported hardware facts
Optional `Asset` fields `soc_compatible` (36), `serial_number` (37),
`kernel_version` (38), `l4t_version` (39), and `gpu_arch` (40) describe reported
hardware/software facts. Missing reports leave them absent. They are not device
identity, permissions or authorization evidence. These self-reported facts
correspond to values displayed by [`wendy device info`](../clients/wendy-cli/commands/device/info.md).
## Methods
These method descriptions apply to numeric v1.
### `CreateAsset`
```
CreateAsset(CreateAssetRequest) → Asset
```
Creates a new asset in the organization.
---
### `GetAsset`
```
GetAsset(GetAssetRequest) → Asset
```
Returns a single asset by ID.
---
### `UpdateAsset`
```
UpdateAsset(UpdateAssetRequest) → Asset
```
Updates mutable fields of an existing asset.
---
### `DeleteAsset`
```
DeleteAsset(DeleteAssetRequest) → DeleteAssetResponse
```
Deletes an asset by ID.
---
### `ListAssets` *(server-streaming)*
```
ListAssets(ListAssetsRequest) → stream ListAssetsResponse
```
Returns a stream of assets matching the request filters. Each streamed message contains one asset. The stream closes when all matching assets have been sent.
#### `ListAssetsRequest`
| Field | Type | Description |
|-------|------|-------------|
| `organization_id` | `int32` | *(required)* Organization to query. |
| `is_compute_device` | `optional bool` | When `true`, restricts results to compute devices. |
| `offset` | `optional int32` | Number of assets to skip (for manual pagination). |
| `limit` | `optional int32` | Maximum number of assets to return. |
| `filter` | `optional string` | Filter expression matching on name, details, asset_type, device_type, or tags. |
| `online_only` | `optional bool` | When `true`, only assets with an active broker presence are returned. |
#### `ListAssetsResponse` *(one message per streamed asset)*
| Field | Type | Description |
|-------|------|-------------|
| `asset` | `Asset` | The asset for this stream message. |
| `total` | `int32` | Total number of matching assets (may be sent on the first or last message). |
---
### `ListAssetChildren` *(server-streaming)*
```
ListAssetChildren(ListAssetChildrenRequest) → stream ListAssetChildrenResponse
```
Returns a stream of direct children of a given parent asset.
#### `ListAssetChildrenRequest`
| Field | Type | Description |
|-------|------|-------------|
| `parent_asset_id` | `int32` | *(required)* Parent asset ID. |
| `offset` | `optional int32` | Number of assets to skip. |
| `limit` | `optional int32` | Maximum number of assets to return. |
#### `ListAssetChildrenResponse` *(one message per streamed asset)*
| Field | Type | Description |
|-------|------|-------------|
| `asset` | `Asset` | The child asset for this stream message. |
| `total` | `int32` | Total number of matching children. |
---
### `GetAssetLineage`
```
GetAssetLineage(GetAssetLineageRequest) → GetAssetLineageResponse
```
Returns the full asset lineage (root asset, all intermediate assets) for a given asset ID.
## Streaming behaviour
`ListAssets` and `ListAssetChildren` are **server-streaming** RPCs. Clients must open a stream and call `Recv()` in a loop until `io.EOF` is returned:
```go
stream, err := client.ListAssets(ctx, req)
if err != nil {
return err
}
var assets []*cloudpb.Asset
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
assets = append(assets, resp.GetAsset())
}
```
> **Migration note:** Prior to PR #601 `ListAssets` and `ListAssetChildren` were unary RPCs that returned a paginated response (`Assets []Asset`, `NextPageToken string`). They are now server-streaming. The old `page_size` / `page_token` fields have been replaced by `offset` / `limit`. Update any existing client code accordingly.
## Online-only filtering
Pass `online_only = true` to receive only devices with an active broker presence. The `wendy cloud discover` and `wendy cloud tunnel` commands both use this filter by default; `wendy cloud discover --all` omits it to include offline devices.
---
# Connectivity
Source: https://docs.wendy.dev/latest/advanced/cloud/connectivity/
---
title: "Connectivity"
description: "The Cloud provides two endpoints; One for Wendy Agent devices, and one API for end-users (Web Dashboards, mobile apps etc.)"
---
The Cloud provides two endpoints; One for Wendy Agent devices, and one API for end-users (Web Dashboards, mobile apps etc.)
The agent communicates over **gRPC**, which enables us to (conditionally) expose relevant services.
When an agent connects to the cloud, it does so using mTLS by providing a client-certificate. The Cloud verifies and authenticates the device based on this certificate.
Refer to the [PKI](../pki/) documents for more info on certificates, auth and verification.
## Network changes
An agent's broker connection keeps the address it was opened with, even after the device's routing changes. When a new connection would leave from another address, such as after Ethernet returns to a device on Wi-Fi, the agent reconnects within about 30 seconds, once nothing is connected to the device through the cloud and only if the broker answers over the new path. Devices that connect through the Cloud v2 relay do not switch yet.
---
# Cloud Discover
Source: https://docs.wendy.dev/latest/advanced/cloud/discover/
---
title: "Cloud Discover"
description: "wendy cloud discover lists devices enrolled in Wendy Cloud."
---
# Cloud Discover
`wendy cloud discover` lists devices enrolled in Wendy Cloud.
## Usage
```sh
wendy cloud discover [flags]
```
## Output
In an interactive terminal the command shows a **live TUI** that auto-refreshes every 10 seconds. Each row displays the device name, type, IP address, and running agent version. In non-interactive environments (or when `--json` is passed) a JSON array is written to stdout.
### TUI keyboard shortcuts
| Key | Action |
|-----|--------|
| `↑` / `↓` | Navigate the device list |
| `enter` | Copy selected device info as JSON to clipboard |
| `a` | Copy all devices as JSON to clipboard |
| `u` | Update the selected device's agent binary in place |
| `q` / `Ctrl+C` | Quit |
If no devices are found, a descriptive message is shown instead:
- Without `--all`: `No online devices found. Use --all to include offline devices.`
- With `--all`: `No enrolled devices found.`
## Asset limit
`cloud discover` caps results at **10,000 devices**. If more are returned, the command exits with an error:
```
cloud returned more than 10000 devices
```
## Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--all` | `false` | Include offline (not currently connected) devices. By default only online devices are shown. |
| `--broker-url` | `$WENDY_BROKER_URL` (or derived from cloud endpoint) | Tunnel broker `host:port` used for version fetching and in-place updates. |
| `--cloud-grpc` | `""` | Cloud gRPC endpoint. Overrides session selection. When multiple sessions are stored and no default is set, an interactive terminal shows a session picker; a non-interactive environment errors. |
## Related
- [`wendy cloud tunnel`](./tunnel.md) — open a tunnel to a cloud device
- [Cloud connectivity](./connectivity.md)
- [Cloud requirements](./requirements.md)
---
# Notifications API
Source: https://docs.wendy.dev/latest/advanced/cloud/notifications-api/
---
title: "Notifications API"
description: "The NotificationService gRPC service creates, lists, and manages operator-facing Wendy Notifications in a Wendy Cloud organization."
---
# Notifications API
The `NotificationService` gRPC service creates, lists, and manages
operator-facing Wendy Notifications in a Wendy Cloud organization.
## Proto package
`wendycloud.v1` is defined in `Proto/cloud/notifications.proto`; the UUID-native
Cloud v2 service is defined in `Proto/wendycloud/v2/notifications.proto`.
## App-facing API (`wendy.system.v1`)
Apps with the [`notifications` entitlement](../device/entitlements.md#notifications)
call `wendy.system.v1.NotificationService` over the Unix socket at
`$WENDY_SYSTEM_SOCKET` (`/run/wendy/system/system.sock`). This creates a
canonical Wendy Notification in the recipients' Companion inboxes. Cloud then
attempts APNs delivery; apps do not send an arbitrary APNs payload directly.
WendyKit is currently the only application SDK for this API, and it is
Swift-only. Apps written in other languages call this gRPC service directly.
See [Send notifications from a device app](../../../guides/device-notifications/)
for local entitlement setup, Swift and direct gRPC examples, and delivery behavior.
Cloud v2 accepts Notifications from an enrolled device for its own organization;
the app or campaign name is only a label. No per-app grant, catalog entry, or
Cloud deployment assignment is required.
The private socket binds every call to trusted app identity. The request cannot
supply an app ID, device ID, or organization ID; the agent adds app identity and
Cloud derives device and organization identity from device mTLS.
### `Send`
```
Send(SendRequest) → SendResponse
```
The agent forwards the request to Cloud. `notification_id` is the caller-chosen resource
identity, not a retry token. After successful creation, every reuse of its
canonical UUID—including an otherwise identical request or a differently cased
spelling—returns `ALREADY_EXISTS`; the prior success is never replayed.
Local validation and rate-limit failures happen before forwarding and do not
claim the UUID. After correcting the request or waiting for the local rate limit,
the caller may retry with the same `notification_id`. These rate-limit and
idempotency semantics are the same on the ACME Cloud v2 and legacy Cloud v1
forwarding paths.
#### `SendRequest`
| Field | Type | Description |
|---|---|---|
| `audience` | `NotificationAudience` | Union of `user_ids` and `roles` for Cloud v2. |
| `title` | `string` | Notification title. |
| `body` | `string` | Notification body. |
| `severity` | `NotificationSeverity` | `INFO`, `WARNING`, `ERROR`, or `CRITICAL`. |
| `deep_link` | `string` | Absolute `wendy://` URI. |
| `notification_id` | `string` | Caller-chosen Notification resource UUID v4. Cloud returns canonical lowercase; any canonical reuse after creation returns `ALREADY_EXISTS`. |
| `metadata` | `optional Struct` | Structured JSON-compatible metadata. |
#### `NotificationAudience`
User and role selectors have union semantics. At most 100 raw selectors may be
supplied. Cloud resolves and deduplicates recipients inside the device's
organization. A user selected through more than one field receives one
Notification. The legacy numeric team field is not forwarded to Cloud v2.
| Field | Type | Description |
|---|---|---|
| `user_ids` | `repeated string` | User IDs to include. |
| `roles` | `repeated OrganizationRole` | Organization roles to include. |
#### `SendResponse`
| Field | Type | Description |
|---|---|---|
| `notification_id` | `string` | Canonical lowercase UUID of the newly created Notification resource. |
Recipient totals are intentionally omitted because team and role counts can disclose
organization membership.
Device-app sources may create 10 accepted Notifications per minute, and the
agent also smooths bursts locally. Repeated rate-limit violations can quarantine
that app/device source for 15 minutes.
Device-originated deep links are restricted to the source device;
`wendy://devices/current/live` is the portable form. For device-originated APNs
alerts, Cloud uses `Wendy · ` as the banner title while retaining
the supplied title on the stored Notification.
## `Notification` message
Fields 1–8 are the stable legacy Companion wire contract. V2 adds canonical
identity, content, audience, and `created_by` attribution without changing those
legacy fields.
| Field | Type | Description |
|---|---|---|
| `id` | `int32` | Legacy numeric ID. |
| `user_id` | `string` | Recipient user ID in legacy and per-user read views. |
| `organization_id` | `int32` | Organization ID. |
| `body` | `string` | Notification body. |
| `severity` | `NotificationSeverity` | Severity level. |
| `related_entities` | `Struct` | Legacy structured context. |
| `created_at` | `Timestamp` | Creation time. |
| `deleted_at` | `optional Timestamp` | Soft-deletion time. |
| `title` | `string` | Notification title. |
| `deep_link` | `string` | Absolute `wendy://` URI. |
| `notification_id` | `optional string` | Canonical caller-chosen resource UUID v4; absent on legacy records. |
| `metadata` | `Struct` | Structured JSON-compatible metadata. |
| `audience` | `optional NotificationAudience` | Original normalized selector union. |
| `created_by_user_id` | `optional string` | Authenticated user that created the Notification. |
| `created_by_asset_id` | `optional int32` | Authenticated device asset that created the Notification. |
| `created_by_app_id` | `optional string` | Trusted app identity stamped by the Wendy agent. |
## Cloud API methods
### `CreateNotification` *(legacy)*
```
CreateNotification(CreateNotificationRequest) → Notification
```
Creates a Notification for one user. This RPC and `CreateNotificationRequest`
are deprecated in their protobuf descriptors and remain intact for existing
Dashboard and legacy clients. `wendycloud.v2` is no longer hypothetical — it is
vendored at `Proto/wendycloud/v2/` and generated into `go/proto/gen/cloudpb/v2/`
— and this RPC is still carried there, still marked `deprecated`. The migration
marker records that it is to be removed from v2, not that it already has been.
---
### `CreateNotificationV2`
```
CreateNotificationV2(CreateNotificationV2Request) → CreateNotificationV2Response
```
Claims the caller-chosen canonical UUID and creates one Notification resource for
the resolved recipient union. The first canonical UUID use may succeed; every
later use returns `ALREADY_EXISTS`, regardless of whether request content is
identical or changed. Cloud does not replay the original response or send pushes
a second time.
User-authenticated callers provide `organization_id`. Provisioned-device callers
omit it because Cloud derives the organization and device from their certificate;
the Wendy agent stamps `app_id` from trusted container state.
#### `CreateNotificationV2Request`
| Field | Type | Description |
|---|---|---|
| `organization_id` | `optional int32` (v1), `optional string` (v2) | Required for user-authenticated callers; omitted by provisioned devices. |
| `audience` | `NotificationAudience` | Union of repeated `user_ids` and `roles` for Cloud v2. |
| `title` | `string` | Notification title. |
| `body` | `string` | Notification body. |
| `severity` | `NotificationSeverity` | `INFO`, `WARNING`, `ERROR`, or `CRITICAL`. |
| `deep_link` | `string` | Absolute `wendy://` URI. |
| `notification_id` | `string` | Caller-chosen Notification resource UUID v4; Cloud stores and returns canonical lowercase form and rejects every canonical reuse with `ALREADY_EXISTS`. |
| `metadata` | `optional Struct` | Structured JSON-compatible metadata. |
| `app_id` | `optional string` | Required for provisioned-device calls and stamped from trusted app identity by the Wendy agent. |
ACME-enrolled devices use the Cloud v2 service over device mTLS; the Agent
forwards user and role selectors and omits the retired team selector.
#### `CreateNotificationV2Response`
| Field | Type | Description |
|---|---|---|
| `notification_id` | `string` | Canonical lowercase UUID of the newly created Notification resource. |
Recipient totals are intentionally omitted because team and role counts can disclose
organization membership.
---
### `ListNotifications` *(server-streaming)*
```
ListNotifications(ListNotificationsRequest) → stream ListNotificationsResponse
```
Returns one Notification per streamed message.
#### `ListNotificationsRequest`
| Field | Type | Description |
|---|---|---|
| `organization_id` | `int32` | Organization to query. |
| `user_id` | `string` | User whose Notifications are requested. |
| `offset` | `optional int32` | Number of matching Notifications to skip. |
| `limit` | `optional int32` | Maximum number of Notifications to return. |
| `severity_filter` | `optional NotificationSeverity` | Restrict results by severity. |
| `include_deleted` | `bool` | Include soft-deleted Notifications. |
#### `ListNotificationsResponse`
| Field | Type | Description |
|---|---|---|
| `notification` | `Notification` | Notification carried by this stream message. |
| `total` | `int32` | Total number of matching Notifications. |
> **Migration note:** `ListNotifications` previously returned one unary response
> containing `repeated notifications`, `next_page_token`, and `total_count`.
> Clients must now receive the server stream until it closes and use
> `offset`/`limit` instead of `page_size`/`page_token`.
---
### `GetNotification`
```
GetNotification(GetNotificationRequest) → Notification
```
Returns one Notification by ID.
### `DeleteNotification`
```
DeleteNotification(DeleteNotificationRequest) → DeleteNotificationResponse
```
Deletes one Notification by ID.
### `MarkAsRead`
```
MarkAsRead(MarkAsReadRequest) → MarkAsReadResponse
```
Marks the requested Notification IDs as read and returns the number marked.
## Campaign notifications
A campaign with `notify.on: event` and `notify.event: ` sends an immediate
notification for the matching event. `notify.on: detection` sends on an inference
appearance. Omit `notify.webhook` for Wendy Cloud delivery, or supply an HTTP(S)
endpoint for webhook delivery. `episode_committed` remains manifest intent for
notification after upload; it is separate from immediate delivery.
Campaigns use the same device-authenticated Cloud sender as apps. On ACME-enrolled
devices they call the Cloud v2 service over device mTLS with `app_id` set to
`campaign:` and owner/admin recipients. The campaign label needs no app
catalog entry, grant, or deployment assignment. Legacy numeric enrollments retain
the Cloud v1 sender. An explicit webhook still selects HTTP delivery instead.
The agent uses its enrolled device credentials and a bounded, nonpersistent
queue. There are at most three attempts, with 10-second timeouts and the same
event UUID. `InvalidArgument`, `Unauthenticated`, `PermissionDenied`,
`FailedPrecondition`, `AlreadyExists`, `Unimplemented`, and `DataLoss` stop
retries. A mismatched response notification ID is `DataLoss`. Other failures
are retried with backoff. Errors appear in agent logs and campaign
`inference_status.notification_error`.
### App catalog API
The v1 `AppService` exposes `UpsertApp`, `GetApp`, `UpdateApp`, `DeleteApp`, and
`ListApps`. It replaces the former `CreateApp` RPC; clients must regenerate
against `Proto/cloud/apps.proto`. Mutations and `GetApp` carry both `id` and
`organization_id`. `UpsertApp` accepts optional `name` and `details`; `UpdateApp`
also accepts the owner/admin-controlled `can_send_notifications` grant.
`ListApps` uses `organization_id`, optional `offset`, `limit`, and `filter`,
returning `apps` and `total`. Pagination is offset-based, not page-token-based.
Deployment registration uses GetApp first and UpsertApp only when absent, so it
preserves existing metadata and never automatically grants notifications.
The vendored AppService contract matches Cloud's current v1 server. Earlier
Companion schemas used different tags for update fields and page-token
pagination, so those clients must be rebuilt with the current schema. The
coordinated client ports are [Companion SDK #1](https://github.com/wendylabsinc/wendy-companion-sdk/pull/1)
and [Companion iOS #223](https://github.com/wendylabsinc/wendy-companion-ios/pull/223).
The shared wire fixtures cover organization scoping, metadata updates,
notification grants and offset pagination. These source changes do not upgrade
installed clients; release the updated clients against the matching Cloud v1
contract. They do not change Cloud's separate v2 API.
---
# Cloud Functional Requirements
Source: https://docs.wendy.dev/latest/advanced/cloud/requirements/
---
title: "Cloud Functional Requirements"
description: "Organisation — A named workspace that groups users, devices, and applications. Every resource in the cloud belongs to exactly one organisation. Organisations are isolated from one "
---
# Cloud Functional Requirements
## Definitions
**Organisation** — A named workspace that groups users, devices, and applications. Every resource in the cloud belongs to exactly one organisation. Organisations are isolated from one another; the users and resources of one organisation are not visible to members of another.
**Provisioned user** — A user whose account exists in the cloud. An account may be created by the administrative API, by accepting an invitation, or by signing in with an email address that matches an organisation's approved domain. A provisioned user with no organisation membership may create a new organisation or accept an invitation but has no access to any organisational resource until they do.
**Device** — A physical piece of hardware running Wendy OS. A device may be registered in the cloud before it has been enrolled or has ever connected.
**Service account** — A non-human member of an organisation, intended for automated systems. A service account authenticates with a registered private key, has no interactive login, and cannot be invited or created through the external identity provider.
**Enrollment token** — A short-lived, single-use secret generated by the cloud and delivered to a device out-of-band. It is the only credential a device may use before it has a certificate, and it is consumed the moment it is used.
**Deployment** — The act of expressing and delivering a desired running state for one or more applications across a targeted set of devices. A deployment is created in Wendy Cloud and delivered to devices asynchronously.
**Release** — The act of publishing a named, immutable version of an application to Wendy Cloud. A release references a specific container image by its content digest and may be targeted by deployments once created.
---
## 1. Organisations
1.1 Every resource in the cloud belongs to exactly one organisation.
1.2 A user may be a member of multiple organisations simultaneously and holds an independent role in each. A user who is an owner in one organisation may hold any role in another.
1.3 A provisioned user may create a new organisation. The user who creates the organisation becomes its owner. There is no limit on the number of organisations a user may create.
1.4 An organisation may not be deleted while it has active devices or members other than the owner.
1.5 Organisations are isolated. A user may only access resources within organisations they are a member of. Resources of one organisation are not visible to members of another.
---
## 2. Identity and Access
### 2.1 Authentication
2.1.1 Users authenticate through an external identity provider. The cloud does not store or manage passwords directly.
2.1.2 The method of authentication (for example, email link or single sign-on provider) does not affect the user's role or permissions within an organisation. Role assignment is always explicit.
2.1.3 A user who joins an organisation through an invitation receives the role specified in that invitation, regardless of how they authenticated.
### 2.2 User Provisioning
The cloud does not permit self-registration. A user account must exist before the user can sign in for the first time.
2.2.1 User accounts are provisioned via an administrative API. This allows external systems — such as a purchase flow or helpdesk tool — to create accounts on behalf of Wendy before the user ever signs in.
2.2.2 A user whose account has been provisioned but who has no organisation membership may create a new organisation or accept an invitation. They have no access to any organisational resource until they do.
2.2.3 An organisation may configure one or more approved email domains. A user whose email address matches an approved domain may sign in without prior API provisioning; an account is created automatically upon first sign-in, and the user is added to the configuring organisation with the role specified in the domain configuration.
2.2.4 A user may not sign in if their email address does not match an approved domain for any organisation and their account has not been provisioned via the administrative API.
### 2.3 Roles
The cloud defines four membership roles. Each role is a strict superset of the one below it.
| Role | Description |
|---|---|
| Owner | Full control over the organisation, including ownership transfer and organisation deletion. Exactly one owner per organisation at all times. |
| Admin | Manages devices, applications, deployments, members, enrollment tokens, and service accounts. Cannot transfer ownership or delete the organisation. |
| Member | Creates and manages applications, releases, and deployments. Cannot manage members, enrollment tokens, or service accounts. |
| Viewer | Read-only access to device fleet, application state, deployment status, and logs. Cannot create or modify any resource. |
2.3.1 An organisation has exactly one owner at all times.
2.3.2 Ownership may be transferred by the current owner to any existing member of the organisation. The previous owner becomes an admin upon transfer.
2.3.3 An owner or admin may change any member's role, subject to the constraint that no member may be assigned a role exceeding that of the user making the change.
2.3.4 A member may not modify their own role.
2.3.5 An owner may remove any member. An admin may remove members and viewers, but not other admins or the owner.
### 2.4 Invitations
An invitation is a one-time, time-limited credential sent to an email address that allows the recipient to join an organisation with a pre-assigned role. It is the only mechanism by which a manually provisioned user may join an organisation they did not create.
2.4.1 An owner or admin may invite a user by specifying an email address and the role the invited user will receive upon joining.
2.4.2 The role assigned in an invitation may not exceed the role of the user sending it.
2.4.3 An invitation is valid for seven days from the time it is sent. A user who does not accept within that period must be re-invited.
2.4.4 An invitation may be revoked by an owner or admin before it is accepted. A revoked invitation cannot be used to join the organisation.
2.4.5 Accepting an invitation adds the user to the organisation with the specified role. If the recipient already has an account, their existing account is used. If they do not, accepting the invitation also provisions their account.
2.4.6 A manually provisioned user may not join an organisation without a valid invitation, unless they are the organisation's creator.
### 2.5 Directory Provisioning
An organisation may connect to an external identity provider to synchronise user membership and roles automatically. When a directory integration is active, the external directory is the authoritative source for membership and roles within that organisation.
2.5.1 Owners and admins may configure a directory integration for their organisation.
2.5.2 Users added to the organisation in the external directory are automatically added to the organisation in the cloud with the role assigned in the directory. If the user has no cloud account, one is provisioned automatically.
2.5.3 Users removed from the organisation in the external directory are automatically removed from the organisation in the cloud.
2.5.4 Role changes made in the external directory are automatically applied. A role assigned by the directory may not exceed the role of the admin who configured the integration.
2.5.5 Manual changes to directory-managed users may be overwritten on the next directory sync. The directory is the authoritative source while the integration is active.
2.5.6 Directory-managed users are clearly identified as such in the member list.
2.5.7 A directory integration is configured with both a directory connection and an associated identity provider. These are typically the same external system (for example, an enterprise identity platform that manages both user records and authentication). When a provisioned user signs in, the cloud identifies the correct identity provider from the organisation's directory configuration and redirects authentication there.
2.5.8 Directory synchronisation runs automatically on a fixed schedule. The default sync interval is 15 minutes. Owners and admins may also trigger a manual sync at any time.
2.5.9 An owner or admin may disconnect a directory integration at any time. Existing members are not automatically removed when the integration is disconnected; their membership reverts to manual management.
### 2.6 Service Accounts
A service account is a non-human member of an organisation, intended for scripts, CI/CD pipelines, monitoring agents and other automated systems that call the API without an interactive session and act on behalf of the organisation rather than a specific person. Personal access tokens are not supported: no bearer string is accepted as an API credential.
2.6.1 Tenant admins create, disable, and delete service accounts in the organisation's identity realm. A service account is a member of the organisation like a human user; its roles and groups determine what it may do. It has no email address, no password, and no interactive login.
2.6.2 A service account authenticates with a private key, never a shared secret. A tenant admin mints a one-time enrollment secret for the account; the holder generates a key pair locally and registers the public key with that secret. The secret is consumed on first use. The private key never leaves the holder.
2.6.3 Accepted key algorithms are ML-DSA-65, ML-DSA-87, and ES256. A realm that requires post-quantum keys accepts only ML-DSA. The Wendy CLI generates ML-DSA-65 keys.
2.6.4 To call the API, the holder signs a short-lived assertion with its key and exchanges it for an access token. Every access token is sender-constrained (DPoP) to the registered key, and every API call carries a fresh proof signed with that key. There is no refresh token; a new token is minted from a new assertion.
2.6.5 Access tokens expire after the realm's access-token lifetime (one hour by default).
2.6.6 A service account may hold more than one registered key. A key is added by minting another enrollment secret for the same account.
2.6.7 Disabling or deleting a service account stops it from minting new tokens immediately. Tokens already issued remain valid until they expire, as for human users.
2.6.8 Service accounts are visible in the member list of an organisation and are clearly distinguished from human users.
2.6.9 The Wendy CLI registers a key with `wendy auth service-account enroll` and signs in headlessly with `wendy auth login --service-account ` (or the `WENDY_SERVICE_ACCOUNT_KEY` environment variable in CI).
### 2.7 Machine Users
Machine users are service accounts (section 2.6). The earlier machine-user model, which authenticated with personal access tokens, is retired.
---
## 3. Device Provisioning
### 3.1 Enrollment Tokens
An enrollment token is a short-lived, single-use secret that authorises a physical device to request its first identity certificate from the cloud. It solves a bootstrapping problem: a device that has never connected to the cloud has no existing credential, so it cannot use certificate-based authentication to prove who it is. The token is its one permitted exception — a temporary shared secret agreed upon before the device has any identity of its own.
The token is generated on the cloud side and delivered to the device out-of-band — for example, written to the device's configuration partition during flashing or first-time setup — before the device ever attempts to connect.
3.1.1 Enrollment tokens may only be generated by admins and owners. Members may manage existing devices (metadata, deployments, ad-hoc control) but may not enroll new devices into the fleet.
3.1.2 Each token is scoped to a specific device record within the organisation. A token issued for device A may not be used to enroll device B.
3.1.3 A token has a default validity period of 24 hours from creation. Owners and admins may configure a different default validity period for their organisation. Owners and admins may also override the validity period for an individual token at creation time.
3.1.4 A token is invalidated immediately upon use. It may not be reused.
3.1.5 A token may not be used to authenticate any API call other than certificate issuance. It grants no access to any cloud resource.
3.1.6 The cloud stores only a one-way hash of the token value. The full token value is displayed once at creation time and is never retrievable thereafter.
3.1.7 Admins and owners may list and revoke unused tokens for their organisation.
3.1.8 Enrollment tokens may be created programmatically via the API when authenticated as a service account with the admin role. Programmatic token creation is enabled by default and may be disabled at the organisation level by owners and admins.
### 3.2 Certificate Issuance
3.2.1 Wendy OS generates a cryptographic key pair on the device before initiating enrollment.
3.2.2 The device presents the enrollment token and its public key to request an identity certificate.
3.2.3 The cloud validates the token, signs the public key, and issues a certificate that encodes the device's organisation identifier and device identifier.
3.2.4 The private key never leaves the device. The cloud does not have access to it.
3.2.5 The issued certificate is the sole credential the device uses for all subsequent cloud communication.
3.2.6 The cloud records the certificate, its validity period, and its association to the device.
### 3.3 Certificate Renewal
Certificates have a fixed validity period. A device must renew its certificate before expiry to maintain uninterrupted access to the cloud.
3.3.1 A device may renew its certificate before expiry by presenting its current valid certificate and a new public key.
3.3.2 Renewal does not require human intervention.
3.3.3 Upon renewal, the previous certificate is immediately invalidated.
### 3.4 Certificate Expiry
A certificate that reaches its expiry date without having been renewed is no longer valid. To reduce the risk of devices becoming unreachable due to expiry, Wendy OS initiates renewal well before the certificate's expiry date.
To handle cases where a device is offline during the normal renewal window — for example, a remote device with intermittent connectivity — a grace period applies after expiry during which the device may still renew without requiring out-of-band intervention.
3.4.1 Wendy OS initiates certificate renewal automatically when the certificate has consumed a configurable percentage of its validity period. The default threshold is 80%.
3.4.2 A device whose certificate has expired but is within the grace period may still renew by presenting its expired certificate and a new public key. During the grace period, an expired certificate is accepted exclusively for renewal requests; it cannot be used for any other cloud operation.
3.4.3 The grace period after certificate expiry is configurable by owners and admins. The default grace period is 72 hours.
3.4.4 A device that has passed the grace period is denied access to the cloud and cannot self-recover. An admin or owner must generate a new enrollment token scoped to the existing device record and deliver it to the device out-of-band.
3.4.5 When a device re-enrolls after grace period expiry, the cloud issues a new certificate against the same device record. The device's name, metadata, tags, and historical records are unchanged.
3.4.6 Re-enrollment after grace period expiry is identical to initial enrollment from a protocol standpoint. No new device record is created.
### 3.5 Revocation
3.5.1 Admins and owners may revoke a device's certificate at any time.
3.5.2 A revoked device is immediately denied access to the cloud. No grace period applies to revocation. To reconnect, the device must re-enroll using a new enrollment token, following the same process described in section 3.4.4.
---
## 4. Device Management
### 4.1 Fleet Overview
4.1.1 Viewers, members, admins, and owners may view all provisioned devices in the organisation.
4.1.2 The fleet view shows each device's name, hardware type, connection status, and time of last contact.
4.1.3 Connection status is one of: online (the device has an active connection to the cloud), offline (the device has not contacted the cloud within its expected check-in interval), or never connected (the device has been provisioned but has not yet established a first connection).
### 4.2 Device Detail
4.2.1 Each device exposes its installed applications, their running state, the time it last reported its state, and any active deployments targeting it.
4.2.2 A device is considered to have reported its state when it sends a state update to the cloud. If no update has been received within the expected check-in interval, the device is marked offline and its last known state is displayed.
### 4.3 Device Metadata
4.3.1 Each device record holds a name, an optional physical location, one or more tags, and hardware specifications (device type, Central Processing Unit (CPU) architecture, RAM, and storage).
Physical location supports two independent formats, both of which may be set simultaneously on a single device:
- **Geographic coordinates**: latitude and longitude expressed as decimal degrees (for example, 51.5074° N, -0.1278° E). Intended for integration with maps or asset tracking systems.
- **Location label**: a free-form text field for hierarchical or room-based naming (for example, "Factory A / Production Hall 3 / Zone Beta"). The cloud imposes no structure on this field; the organisation defines its own naming conventions.
Tags are free-form string labels applied to devices to group them for targeting. A device may carry any number of tags. Tags are used when creating deployments (section 4.5) and notification rules (section 6.4) to address groups of devices without naming them individually.
4.3.2 Members, admins, and owners may update device metadata, including adding and removing tags.
4.3.3 Viewers may view device metadata but may not modify it.
4.3.4 Tags and their meanings are defined by the organisation. The cloud imposes no structure or validation on tag values.
4.3.5 A device may declare tags in its on-device configuration. Tags declared in this way are automatically applied to the device record in Wendy Cloud when the device connects. Manifest-declared tags coexist with tags set manually through Wendy Cloud; neither set overrides the other.
### 4.4 Device Removal
4.4.1 Admins and owners may remove a device from the organisation's fleet.
4.4.2 Removing a device revokes its certificate, cancels all active deployments targeting it exclusively, and removes it from any future deployment targets evaluated after removal.
4.4.3 Historical records associated with the device (audit log entries, past deployment records, log history) are retained and attributed to the device identifier.
### 4.5 Targeting
4.5.1 Devices may be targeted individually by identifier, by one or more tags, or as an entire fleet.
4.5.2 A deployment target is evaluated at the time of deployment creation. Devices provisioned after a deployment is created are not automatically included.
---
## 5. Application Management
### 5.1 Applications
An application is a containerized workload intended to run on one or more Wendy OS devices. It is delivered as an OCI-compliant container image containing an embedded manifest file that declares the application's identity, required device resources, and runtime configuration. Developers are responsible for building and publishing the container image; Wendy Cloud does not build images.
The lifecycle of an application is: an application record is created in Wendy Cloud; one or more releases are published referencing specific container images; deployments install and run those releases on targeted devices; a deployment with the Absent state removes the application from devices; the application record may be deleted once all devices have converged on Absent or all deployments have been cancelled.
The manifest embedded in the image declares:
- A unique application identifier in reverse-domain format (for example, `com.example.myapp`).
- The application version.
- The device resources the application requires, declared as entitlements. An application may only access a device resource if it has declared the corresponding entitlement. Entitlements are enforced at runtime by Wendy OS.
- Optional runtime configuration including a readiness probe and lifecycle hooks.
The following entitlements are supported:
| Entitlement | Description |
|---|---|
| `network` | Network access. Configured as host (full network access) or none (fully isolated). |
| `gpu` | GPU access for ML inference and compute workloads. |
| `persist` | A named persistent storage volume. The volume persists across application restarts and updates. Two applications on the same device declaring a volume with the same name share that volume. |
| `audio` | Audio device access for recording or playback. |
| `camera` | Camera and video device access. |
| `bluetooth` | Filtered Bluetooth access via a D-Bus proxy. |
| `usb` | Raw USB device access. |
| `i2c` | I2C bus access. Requires a specific device to be named. |
| `gpio` | GPIO pin access. Specific pins may be declared or all available pins claimed. |
| `spi` | SPI device access. |
| `input` | HID input device access. |
| `serial` | Serial tty device access (USB-serial adapters, servo buses, etc.). Requires a specific tty node to be named. |
| `admin` | Full local device control. Grants the wendy-agent's local unix socket (no mTLS). App can start, stop, and delete apps and read all device data locally. Grant only to fully-trusted first-party apps. |
5.1.1 An application has a display name set in Wendy Cloud and a system identifier declared in the embedded manifest. The display name is a human-readable label and may be changed at any time. The system identifier is set by the developer in the manifest and must be consistent across all releases of the same application; it is how the cloud and Wendy OS track which application a release belongs to. The system identifier must be unique within the organisation.
5.1.2 Members, admins, and owners may view applications. Viewers may not view applications.
5.1.3 Members, admins, and owners may create applications.
5.1.4 Admins and owners may delete an application. An application may not be deleted while any of its releases have an active deployment on any device.
5.1.5 Admins and owners may issue a fleet-wide removal for an application. This creates a single Absent deployment targeting every device that currently has the application installed, regardless of how those devices were originally targeted. Devices that are offline at the time will receive the removal instruction when they next connect.
5.1.6 Deleting an application record may only be done after all devices have converged on the Absent state, or after all active deployments targeting it have been cancelled.
### 5.2 Releases
5.2.1 A release is a named, immutable version of an application. It references a container image by its content digest, ensuring the artifact cannot change after the release is created. The manifest embedded in the image at the time of release creation is the version of record for that release's entitlements and runtime configuration.
5.2.2 The cloud does not host container images. A release references an image stored in an external container registry. The cloud verifies the image is accessible at the time of release creation.
5.2.3 Viewers, members, admins, and owners may view releases.
5.2.4 Members, admins, and owners may create releases.
5.2.5 Releases may not be modified or deleted after creation.
### 5.3 Deployments
5.3.1 A deployment expresses the desired state for a set of devices. It specifies a release and one of three running states:
| State | Meaning |
|---|---|
| Running | The application must be present on the device and actively running. |
| Stopped | The application must be present on the device but not running. |
| Absent | The application must be removed from the device entirely. |
5.3.2 Members, admins, and owners may create deployments.
5.3.3 Supersession is per-device. When a new deployment is created for an application, it supersedes the prior deployment only for the devices it targets. For each such device, the prior deployment's desired state is replaced by the new one. Devices targeted by the prior deployment that are not targeted by the new deployment continue to follow the prior deployment unchanged.
5.3.4 Devices receive and apply deployment instructions asynchronously. The cloud does not wait for device confirmation before recording the deployment as created.
5.3.5 A device is considered converged on a deployment when its reported state matches the deployment's desired state for every targeted application.
5.3.6 Admins and owners may cancel a deployment that has not yet converged. Cancelling a deployment stops the cloud from delivering further instructions for it but does not revert any changes already applied on devices.
### 5.4 Deployment Status
5.4.1 For each targeted device, the cloud reports one of the following per-device states:
| Status | Meaning |
|---|---|
| Pending | The instruction has not yet been delivered to the device. |
| Delivered | The instruction has been delivered but the device has not yet reported a matching state. |
| Converged | The device's reported state matches the deployment's desired state. |
| Superseded | A newer deployment has replaced this deployment's desired state on this specific device. Other devices targeted by this deployment that are not covered by the newer deployment continue to follow this deployment. |
| Cancelled | The deployment was cancelled before this device converged. |
5.4.2 The cloud records the time the instruction was last delivered to each device and the time each device last reported its state.
5.4.3 Viewers, members, admins, and owners may view deployment status.
### 5.5 Ad-hoc Control
5.5.1 Members, admins, and owners may instruct a specific device to start, stop, or remove a specific application immediately, outside of a versioned deployment. This is intended for out-of-band operational needs where bypassing the normal deployment workflow is appropriate.
5.5.2 An ad-hoc instruction does not create a deployment record and does not affect the desired state of any active deployment. If an active deployment targets the same application on the same device, the deployment's desired state will be re-delivered on the next check-in and will override the ad-hoc change. This is intentional: the deployment represents declared intent, and ad-hoc control is explicitly an out-of-band intervention. To make a change permanent, create a new deployment.
### 5.6 Application Runtime
Once installed on a device, an application runs as a container managed by Wendy OS. Runtime behaviour — restart policies, readiness probes, and lifecycle hooks — is specified in the Wendy OS requirements. The following requirements describe what the cloud tracks as a result of that runtime behaviour.
5.6.1 The cloud receives the cumulative restart count and last exit signal from each device as part of reported state. This data feeds the crash loop notification condition in section 6.5.
5.6.2 If an application fails to start on a device for any reason, the cloud reports that device as not converged for the active deployment.
**Persistent storage**
5.6.3 An application may declare one or more named persistent storage volumes in its manifest. A volume persists across application restarts, updates, and re-deployments.
5.6.4 Two applications on the same device that declare a volume with the same name share that volume. Shared volumes are not isolated between applications.
5.6.5 Deploying an application with the Absent state removes the application from the device but does not delete its persistent volumes. Volume deletion is a separate action and is not currently managed through Wendy Cloud.
**Entitlement enforcement**
5.6.6 An application may only access a device resource if it has declared the corresponding entitlement in its manifest. Access to undeclared resources is blocked by Wendy OS.
5.6.7 If a target device does not have a hardware resource required by an entitlement, the application will fail to start on that device. The cloud reports the device as not converged.
---
## 6. Observability
Devices emit structured telemetry to the cloud following the OpenTelemetry (OTEL) specification. Three signal types are supported, each serving a distinct purpose:
- **Logs**: Timestamped text records of discrete events, such as application start, errors, or status changes. Use logs to diagnose specific incidents and understand what happened at a particular moment.
- **Metrics**: Numeric measurements recorded over time, such as CPU usage, temperature, or application-defined values. Use metrics to monitor trends and detect anomalies.
- **Traces**: End-to-end records of an operation as it flows through one or more components, capturing how long each step took. Use traces to diagnose latency and understand dependencies between application components.
Each signal is attributed to its originating device and application.
### 6.1 Logs
6.1.1 The cloud receives and retains structured log entries from all connected devices.
6.1.2 Each log entry carries at minimum: originating device identifier, application identifier, severity level, timestamp, and message body.
6.1.3 Log entries are retained for a period configurable by owners and admins. The default retention period is 30 days.
6.1.4 Members, admins, and owners may query logs filtered by device, application, severity level, and time range.
6.1.5 Members, admins, and owners may follow the live log output of a specific application on a specific device as it arrives.
6.1.6 Viewers may not query logs or access live log output. Log data is accessible to viewers only through dashboards shared with them.
### 6.2 Metrics
Metrics are divided into two categories:
- **System metrics**: Hardware and operating system measurements reported automatically by Wendy OS, including CPU usage, memory usage, storage usage, device temperature, battery level (where applicable), and network throughput.
- **Application metrics**: Custom numeric measurements defined and emitted by the application using the OpenTelemetry SDK. These are defined entirely by the application developer and may represent anything relevant to the application's behaviour.
6.2.1 The cloud receives and retains both system metrics and application metrics from all connected devices.
6.2.2 System metrics are reported automatically by Wendy OS without any application-level configuration.
6.2.3 Application metrics are defined and emitted by the application using the OpenTelemetry SDK.
6.2.4 Data retention for raw metric data points and aggregated data is configurable by owners and admins per organisation. The default retention periods are 30 days for raw data points and 12 months for hourly and daily aggregates.
6.2.5 Members, admins, and owners may query metrics for a specific device and application over a selectable time range.
6.2.6 Viewers may not query metrics directly. Metric data is accessible to viewers only through dashboards shared with them.
### 6.3 Dashboards
6.3.1 Members, admins, and owners may create dashboards composed of log queries and metric charts.
6.3.2 A dashboard may be shared with any member of the organisation, including viewers.
6.3.3 Viewers may view dashboards that have been shared with them. They may not create or modify dashboards.
### 6.4 Application Data
Applications may produce data beyond logs and metrics — for example, files generated by processing, recorded video, inference results, or sensor readings. The cloud does not provide a general-purpose data storage or retrieval mechanism for this output. Applications are responsible for exposing or exporting their own generated data. The following mechanisms are available:
6.4.1 Structured numeric output may be emitted as application metrics via the OpenTelemetry SDK and queried through the cloud.
6.4.2 Textual event output may be emitted as log entries via the OpenTelemetry SDK and queried through the cloud.
6.4.3 File-based output is written to named persistent volumes on the device (section 5.6). Members, admins, and owners may browse and download files from a device's named persistent volumes through Wendy Cloud. The device must be online for this operation.
### 6.5 Notifications
**Notification destinations**
Notification destinations are reusable, named resources configured at the organisation level. A destination specifies a delivery mechanism and its credentials. Once configured, a destination may be referenced by any number of notification rules without reconfiguration.
6.5.1 Owners and admins may add, update, and delete notification destinations.
6.5.2 Supported destination types include: email address, webhook URL, Slack channel, Microsoft Teams channel, and PagerDuty service.
6.5.3 Destination credentials are stored securely and are not exposed after initial configuration. They may be updated or deleted at any time by owners and admins.
**Notification rules**
A notification rule specifies a trigger condition, the set of devices it monitors, and one or more destinations to deliver to when the condition fires.
6.5.4 Owners and admins may create, modify, and delete notification rules.
6.5.5 A notification rule targets one of: all devices in the organisation, all devices matching one or more tags, or a specific set of devices by identifier.
6.5.6 The following trigger conditions are supported:
| Condition | Configurable parameters |
|---|---|
| Application crash loop | Number of restarts within a configurable time window. |
| Device offline | Duration since last contact. |
| Deployment not converging | Duration a device has remained in the Delivered state. |
| System metric threshold | Metric name, comparison operator, and threshold value. |
| Application metric threshold | Metric name, comparison operator, and threshold value. |
| Log pattern match | A structured filter applied to incoming log entries. The rule fires when a matching entry is received. Matching records are included in the notification. |
6.5.7 Each notification rule may include a custom message to be included in the notification.
6.5.8 Each notification rule references one or more configured destinations. All referenced destinations receive the notification simultaneously when the rule fires.
6.5.9 Notification delivery is not gated by organisation role. A destination may be any address or endpoint regardless of whether the recipient is a member of the organisation.
---
## 7. Audit
7.1 The cloud maintains an append-only audit log of all write actions performed within an organisation. Read-only actions (viewing, querying, tailing logs) are not audited.
7.2 Audited actions include:
- Member invitations and removals
- Role changes
- Directory integration configuration, synchronisation, and disconnection
- Directory-provisioned membership changes
- Enrollment token generation and revocation
- Certificate revocation
- Application and release creation
- Application deletion
- Deployment creation and cancellation
- Ad-hoc device control
- Service account creation, disablement, and deletion
- Service account key enrollment
- Notification destination creation, modification, and deletion
- Notification rule creation, modification, and deletion
- Dashboard creation, modification, sharing, and deletion
7.3 Each entry records the actor (human user or service account), the action performed, the affected resource identifier, and the UTC timestamp.
7.4 Failed actions are not recorded in the audit log. Only actions that were accepted and processed by the cloud are audited.
7.5 No user, including the owner, may modify or delete audit log entries.
7.6 Admins and owners may query the full audit log for their organisation. Members and viewers may query only entries where they are the actor.
7.7 Audit log entries are retained for a minimum of 12 months and may not be deleted before that period expires.
---
# Telemetry
Source: https://docs.wendy.dev/latest/advanced/cloud/telemetry/
---
title: "Telemetry"
description: "Wendy devices produce their structured logs, metrics and tracing through OpenTelemetry."
---
Wendy devices produce their structured logs, metrics and tracing through [OpenTelemetry](https://opentelemetry.io/).
These logs are received by a local on-device collector, which forwards the stream to other targets. One of those targets is the Wendy Cloud.
The OTel spec is a standard wire protocol, also based on gRPC, that we can implement and codegen. Wendy Cloud then collects the OTel data, and "processes" it. Any queries over structured logs, visualisation and forwarding of data happens on these streams.
## Environment variables injected into app containers
When the agent starts an app container it injects a set of environment variables
on top of the image's own env. Two are always present (all network modes):
| Variable | Value |
|---|---|
| `WENDY_HOSTNAME` | For single-container apps: the device's mDNS hostname (omitted when unresolvable). For multi-service apps (`serviceName` set): `{serviceName}.local`, giving each service a distinct hostname identity. |
| `WENDY_DEVICE_HOSTNAME` | The device's own mDNS hostname, injected unconditionally. Unlike `WENDY_HOSTNAME` it is always the host's name, never a per-service `{serviceName}.local`, so a multi-service app can still identify the device it runs on. |
| `WENDY_APP_ID` | The `appId` from `wendy.json` (omitted when empty). |
| `WENDY_APP_GROUP` | The `appId` of the owning app. **Multi-service only** — injected when `serviceName` is non-empty so a service can discover its siblings. Absent for single-container apps. |
The private app connection variable is conditional on an entitlement:
| Variable | Value | Notes |
|---|---|---|
| `WENDY_SYSTEM_SOCKET` | `/run/wendy/system/system.sock` | Injected only with the `notifications` entitlement. Points to the app's private app-facing socket. |
The OpenTelemetry variables are injected **only when the app has the host
`network` entitlement**, because the agent's local OTLP receiver listens on the
device loopback and is reachable only from a host-networked container. Each is
left untouched if the image already sets it, so image-provided values win:
| Variable | Value | Notes |
|---|---|---|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://127.0.0.1:4317` (port from `WENDY_OTEL_PORT`, default `4317`) | Only set if the image hasn't configured an endpoint. |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | Only set together with the default endpoint above (i.e. when the image hasn't set an endpoint) and only if absent. An image that sets its own endpoint keeps full control of the protocol. |
| `OTEL_SERVICE_NAME` | `` for a single-container app; `_` for a multi-service app | Only set if absent and `appId` is non-empty. |
| `OTEL_RESOURCE_ATTRIBUTES` | `wendy.app.name=` | Only set if absent and `appId` is non-empty. |
`OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` set the `service.name` and
`wendy.app.name` resource attributes that `wendy device logs --app ` filters
on, so telemetry an app exports directly via OTLP is attributed to its app id
without the app having to hardcode it. (Container stdout/stderr is already
stamped with these attributes by the agent's log bridge.) The identity variables
are set even when the image preconfigured a custom endpoint, so an app pointing
at its own collector still produces app-filterable logs.
The agent uses the OTLP receiver port (`4317`, gRPC) for the injected endpoint;
an HTTP/protobuf receiver is also available on `4318` for clients that prefer it.
---
# Cloud Tunnel
Source: https://docs.wendy.dev/latest/advanced/cloud/tunnel/
---
title: "Cloud Tunnel"
description: "The wendy cloud tunnel command forwards a local TCP or UDP port to an authorized service on a cloud-enrolled device. Cloud v2 authorizes each port through its service catalog; lega"
---
# Cloud Tunnel
The `wendy cloud tunnel` command forwards a local TCP or UDP port to an authorized
service on a cloud-enrolled device. Cloud v2 authorizes each port through its
service catalog; legacy sessions use the legacy broker. It listens on
`127.0.0.1` on your development machine and reaches the remote service through
the Cloud broker. Start with the
[Cloud v2 or Legacy tab](../../../cloud/) if you have not selected a session yet.
## Prerequisites
- Authenticated with the intended Cloud context. Use `wendy cloud login` for v2,
or `wendy cloud login --legacy` for v1, then check `wendy cloud status`.
- At least one compute device enrolled and **online** in your organization.
## Usage
```sh
wendy cloud tunnel :[/udp] [--cloud-grpc ] [--device ]
```
For example, forward local port 15052 to the Wendy Agent service:
```sh
wendy cloud tunnel 15052:50052 --device shop-floor-01
```
This is a transport for an authenticated Agent client, not an HTTP endpoint.
Forwarding does not bypass the Agent's authentication. Keep the command running
while using it, then press Ctrl+C to close the forward.
Without a protocol suffix, the command uses TCP. Append `/udp` to forward UDP
datagrams. A single port uses that port at both ends.
Cloud v2 supports any TCP or UDP port from 1 through 65535 on the device's
loopback interface. For example:
```sh
wendy cloud tunnel 8080:3007 --device shop-floor-01
wendy cloud tunnel 9000:9000/udp --device shop-floor-01
```
Arbitrary ports require an updated Cloud service, plus CLI and agent
[2026.10.22-211023](https://github.com/wendylabsinc/WendyOS/releases/tag/2026.10.22-211023)
or newer. Older v2 versions support only TCP ports 22 and 50052. Legacy
sessions retain their existing TCP and UDP forwarding behavior.
Each v2 UDP session authorizes one remote port. Separate local peers use
separate flows, with up to 64 active flows and 16 new flows per second.
Empty datagrams are supported; the maximum payload is 65507 bytes.
Port 22 requires an SSH server on the target. Release Jetson and Raspberry Pi images do not
include one; listing `ssh` in the catalog does not make SSH available there.
Before moving a custom application port or UDP workflow from v1, test it on v2
with the updated versions. See [Moving to Cloud v2](../../../cloud-migration/).
The CLI:
1. Lists the online compute devices enrolled in your organization, up to a cap of **10,000 devices**. If more are returned, the command exits with an error: `cloud returned more than 10000 devices`.
2. Selects the target device:
- When `--device` is set, matches it against device names (case-insensitive exact match). On a legacy session a plain integer is also accepted as the numeric asset ID, which is how devices enrolled without a name are reached; on a cloud (OIDC) session there is no numeric fallback — pass the device's name or its UUID.
- When `--device` is unset and exactly one device is online, connects to it directly.
- When `--device` is unset and more than one device is online:
- In an **interactive terminal**, presents the cloud discover TUI in picker mode (`↑/↓` to navigate, `enter` to select, `u` to update a device before connecting, `q` to cancel).
- In a **non-interactive environment**, exits with an error that enumerates available devices as `id=name` pairs (unnamed devices show as `(unnamed)`). Pass `--device ` to select one directly.
3. Starts the local listener and forwards traffic to the requested port on the selected device.
Only online devices are shown. To inspect enrolled but offline devices, use
[`wendy cloud discover --all`](../clients/wendy-cli/commands/cloud/discover.md).
Use `wendy cloud discover --json` to obtain selectors: UUIDs for Cloud v2,
numeric asset IDs for legacy Cloud, or device names in either environment.
## Flags
| Flag | Description |
|------|-------------|
| `--cloud-grpc` | Override the cloud gRPC endpoint. Overrides session selection. When multiple sessions are stored and no default is set, an interactive terminal shows a session picker; a non-interactive environment errors. |
| `--device` | Target a specific device by name (case-insensitive exact match), by UUID, or — on legacy sessions only — by numeric asset ID. When omitted in a non-interactive context with multiple devices, the command exits with an error listing `id=name` pairs. |
| `--broker-url` | Override the tunnel broker host and port. **Legacy sessions only**; defaults to `WENDY_BROKER_URL` when set, otherwise the cloud endpoint's broker address. On a cloud (OIDC) session Cloud selects the authorized relay and passing this flag errors. |
## Related
- [Cloud Connectivity](./connectivity.md)
- [`wendy cloud discover`](../clients/wendy-cli/commands/cloud/discover.md)
- [Reach a UDP service through Wendy Cloud](../../../guides/tutorials/python/cloud-udp-service/)
---
# Debugging
Source: https://docs.wendy.dev/latest/advanced/debugging/
---
title: "Debugging"
description: "WendyOS developer documentation."
---
---
# Python
Source: https://docs.wendy.dev/latest/advanced/debugging/python/
---
title: "Python"
description: "WendyOS developer documentation."
---
---
# Swift
Source: https://docs.wendy.dev/latest/advanced/debugging/swift/
---
title: "Swift"
description: "WendyOS developer documentation."
---
---
# Developer Guide
Source: https://docs.wendy.dev/latest/advanced/development/
---
title: "Developer Guide"
description: "This guide covers everything you need to contribute to the wendy-agent repository, which contains both the Wendy CLI (wendy) and the on-device daemon (wendy-agent)."
---
# Developer Guide
This guide covers everything you need to contribute to the `wendy-agent` repository, which contains both the Wendy CLI (`wendy`) and the on-device daemon (`wendy-agent`).
## Contents
| Document | Covers |
|---|---|
| [setup.md](setup.md) | Prerequisites, building from source, shell aliases |
| [testing.md](testing.md) | Unit tests, integration tests against real hardware, test coverage |
| [contributing.md](contributing.md) | Branching model, commit style, PR workflow, code owners |
| [debugging.md](debugging.md) | Debugging the agent itself: logs, env vars, gRPC tracing, common failures |
| [docs-site.md](docs-site.md) | Fumadocs local development, CI, hosting, and release docs URLs |
| [screencasts.md](screencasts.md) | Developer-only screencast production tooling and CI checks |
## Repository Layout
```
wendy-agent/
go.mod Go module root
go/ Go source tree (CLI + agent)
cmd/wendy/ CLI entry point
cmd/wendy-agent/ Agent entry point
internal/
agent/ Agent subsystems (containerd, bluetooth, network, mTLS, …)
cli/ CLI commands, gRPC client, TUI
shared/ Code shared between CLI and agent
proto/gen/ Generated protobuf bindings (do not edit by hand)
scripts/ Build helpers, proto generation, CI test runner
Makefile Build targets (run from go/)
proto-defs/ Protobuf definitions (shared source of truth)
docs/ Documentation (symlink -> go/internal/cli/assets/docs)
screencast/ Developer-only narrated screencast tooling
plugins/ Claude Code plugins
swift/ macOS companion app (WendyAgentMac)
packaging/ Linux .deb/.rpm packaging scripts
.github/workflows/ CI pipelines
```
## Quick Start
```sh
# Clone
git clone https://github.com/wendylabsinc/wendy-agent.git
cd wendy-agent
# Build both binaries (run make from go/)
cd go && make build
# Run unit tests
make test
```
See [setup.md](setup.md) for full prerequisites and [testing.md](testing.md) for how to run integration tests against a device.
---
# Contributing to WendyOS
Source: https://docs.wendy.dev/latest/advanced/development/contributing/
---
title: "Contributing to WendyOS"
description: "Thank you for contributing to WendyOS! This document covers the development workflows, CI pipelines, and guidelines for contributing to the project."
---
# Contributing to WendyOS
Thank you for contributing to WendyOS! This document covers the development workflows, CI pipelines, and guidelines for contributing to the project.
## CI / GitHub Actions
### Docs Review Workflow (`.github/workflows/docs-review.yml`)
The docs-review workflow runs on every pull request targeting `main` and posts a structured docs coverage review as a PR issue comment when Claude finds documentation gaps.
**Key behaviours:**
- **Structured output:** Claude returns either `NO_CHANGES_NEEDED` or JSON with up to 8 findings. Each finding includes severity, title, docs path, one-line overview, and detailed rationale or suggested content.
- **Severity sorting:** Findings are sorted blockers-first, then concerns, then info, with 🛑 Error, ⚠️ Concern, and 💡 Info labels in the comment.
- **Inline path summaries:** Each visible finding paragraph starts with the affected docs path, followed by a colon and the overview prose.
- **Collapsible details:** Longer rationale and suggested diffs are placed behind a workflow-owned `` disclosure block.
- **Output safety:** LLM-provided text is length-limited, angle brackets are rendered as safe Unicode characters, detail text is placed in fenced code blocks, unsafe URI schemes are neutralised, and docs paths are accepted only when they match the strict existing `docs/.../*.md` allowlist.
- **Edit-in-place comment:** Existing docs-review comments are identified by the `` marker and edited instead of posting duplicates. Duplicate marker comments are deleted.
- **Stale comment cleanup:** When no findings are produced, any existing docs-review comment is deleted.
- **Body truncation:** The prepared comment is capped at 65,000 characters, with space reserved so the identifying marker remains present after truncation.
- **GitHub API retries:** Comment list, create, update, and delete requests retry transient `429` and `5xx` responses before failing the step.
### Security Review Workflow (`.github/workflows/security-review.yml`)
A dedicated **AI Security Review** workflow runs on every pull request targeting `main` and posts a structured security and compliance report as a PR comment.
#### Trigger
```yaml
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
```
Concurrent runs for the same PR are cancelled automatically (`cancel-in-progress: true`).
#### What it does
1. **Fetches the PR diff** using the GitHub CLI (`gh pr diff`).
2. **Fetches prior review state** from the existing security-review comment identified by the `` marker.
3. **Invokes Claude** (`CLAUDE_MODEL`, currently `claude-opus-4-8`, `max_tokens=16000`) with a detailed system prompt covering security, compliance, prior review state, and explicit `SECURITY:` silencing comments.
4. **Renders a structured Markdown comment** from Claude's JSON response: one glanceable line per finding, with full details behind per-finding `` disclosures.
5. **Posts or updates a PR comment** headed `# AI Security Review`. Existing comments are edited in place via the stable marker; duplicate security-review comments are deleted.
6. **Blocks merging only on open high-severity findings** — a final step exits non-zero only when an unsilenced `open` finding is `HIGH` or `CRITICAL`.
#### Security analysis scope
The model analyses the diff for:
- Injection vulnerabilities (SQL, command, LDAP, XPath, template, etc.)
- Authentication and authorisation flaws
- Sensitive data exposure (secrets, PII, tokens hardcoded or logged)
- Cryptographic weaknesses
- Input validation and output encoding issues
- Security misconfiguration (overly broad permissions, debug flags, unsafe defaults)
- Insecure dependencies or version pins
- Race conditions and TOCTOU vulnerabilities
- Path traversal and SSRF
- Denial-of-service vectors (unbounded loops, large allocations, missing rate limits)
#### Compliance frameworks checked
| Framework | Controls evaluated |
|---|---|
| **SOC 2** | CC6, CC7, CC8, CC9, A1, C1, P-series |
| **ISO/IEC 27001:2022** | A.8 (technological controls), A.9 (access control), A.12 (logging & monitoring) |
| **PCI DSS v4.0** | Req 3, 4, 6, 10 — flagged only when the diff touches payment/card data |
| **GDPR / Privacy** | Lawful basis, PII logging, data-subject rights |
| **HIPAA** | PHI encryption, access controls — flagged only when the diff touches health data |
| **NIST SP 800-53 / CSF 2.0** | AC, AU, IA, SC, SI control families |
Each finding is rated **CRITICAL**, **HIGH**, **MEDIUM**, **LOW**, or **INFORMATIONAL** and tagged with the relevant standard(s) (e.g. `[SOC2-CC6] [ISO27001-A.8]`).
#### Report structure
The comment is rendered from a JSON payload with:
| Field | Description |
|---|---|
| `summary` | One-line overview of the review |
| `findings[]` | Up to 10 security findings |
| `compliance_summary` | Short Markdown compliance summary |
Each finding includes severity, status, standards, title, path, line(s), visible overview, and detailed Markdown remediation. Valid statuses are `open`, `addressed`, `cancelled`, and `silenced`.
#### Stateful review and silencing
The previous AI Security Review comment is included as untrusted prior review state. Claude preserves still-relevant findings, marks fixed findings as `addressed`, marks no-longer-applicable findings as `cancelled`, and appends genuinely new findings as `open`.
A nearby developer comment containing `SECURITY: ` explicitly accepts the risk. The renderer also checks the PR diff for nearby `SECURITY:` comments and marks matching open findings as `silenced`, keeping them visible but non-blocking.
#### Severity gate
The gate counts only findings whose status remains `open` and whose severity is `HIGH` or `CRITICAL`. Silenced, addressed, and cancelled findings do not fail the check. `MEDIUM`, `LOW`, and `INFORMATIONAL` findings are reported in the comment but do not fail the check.
#### Permissions
The job requests only `contents: read` and `pull-requests: write`. It only runs on PRs from within the same repository (`github.event.pull_request.head.repo.full_name == github.repository`), preventing untrusted forks from triggering the workflow.
#### Prompt injection mitigation
PR title, body, and diff are wrapped in `` tags. Prior review state is wrapped in `` tags, and malformed model output sent through the repair pass is wrapped in `` tags. The prompts explicitly instruct the model to ignore instructions embedded within those untrusted sections.
#### Required secrets
| Secret | Purpose |
|---|---|
| `ANTHROPIC_API_KEY` | Authenticates calls to the Anthropic API |
No additional secrets are required; the `GH_TOKEN` is provided automatically by GitHub Actions.
---
# Debugging the wendy-agent
Source: https://docs.wendy.dev/latest/advanced/development/debugging/
---
title: "Debugging the wendy-agent"
description: "This page covers debugging the agent daemon itself — its startup, gRPC server, container management, and internal subsystems. For debugging apps running on a device (attaching a de"
---
# Debugging the wendy-agent
This page covers debugging the agent daemon itself — its startup, gRPC server, container management, and internal subsystems. For debugging *apps running on* a device (attaching a debugger to your Python or Swift container), see [apps/debugging](../apps/).
## Log Output
The agent uses `go.uber.org/zap` for structured logging. By default it emits production-format JSON logs.
### Enable verbose / development logging
Set `WENDY_DEBUG` to any non-empty value to switch to the development log format (human-readable, all levels including debug):
```sh
WENDY_DEBUG=1 ./wendy-agent
```
In production (systemd service), override the environment in `/etc/default/wendy-agent`:
```
WENDY_DEBUG=1
```
Then restart the service:
```sh
sudo systemctl restart wendy-agent
```
### Reading systemd journal logs
```sh
# Follow logs in real time
journalctl -u wendy-agent -f
# Show logs with timestamps since boot
journalctl -u wendy-agent -b
# Show the last 200 lines
journalctl -u wendy-agent -n 200
```
### Internal telemetry log stream
The agent tees its own `zap` logger into the telemetry broadcaster, which means internal agent logs are forwarded through the OTEL telemetry stream alongside container logs. You can observe them via `wendy device telemetry-stream` from the CLI (requires the device to be provisioned).
## Environment Variables
All environment variables are read at startup. Restart the agent after changing them.
| Variable | Default | Description |
|---|---|---|
| `WENDY_DEBUG` | _(unset)_ | Set to any value to enable development-mode logging (verbose, human-readable) |
| `WENDY_TLS_DEBUG` | _(unset)_ | Set to any value to log TLS handshake details for debugging mTLS connection issues |
| `WENDY_MDNS_DEBUG` | _(unset)_ | Set to any value to log mDNS query failures during discovery |
| `WENDY_MDNS_TIMEOUT` | `4s` | Timeout for mDNS browse fallback on Linux/Windows hosts (range: 1s–30s) |
| `WENDY_CONFIG_PATH` | `/etc/wendy-agent` | Directory for provisioning certificates and config |
| `WENDY_AGENT_HOST` | `::` | Host address for the plaintext gRPC server (Swift agent only; accepted values: `127.0.0.1`, `::1`, `localhost`) |
| `WENDY_AGENT_PORT` | `50051` | Port for the plaintext gRPC server (pre-provisioning only) |
| `WENDY_CONTAINERD_ADDR` | _(containerd default)_ | Unix socket path for the containerd client |
| `WENDY_REGISTRY_ADDR` | _(internal default)_ | Address for the embedded OCI registry |
| `WENDY_BROKER_URL` | _(derived from cloud host)_ | WebSocket URL for the cloud tunnel broker |
| `WENDY_OTEL_PORT` | `4317` | Port for the OTEL gRPC receiver |
| `WENDY_OTEL_HTTP_PORT` | `4318` | Port for the OTEL HTTP/protobuf receiver |
| `WENDY_MTLS_ORG_ENFORCEMENT` | `strict` | mTLS client organization enforcement on the device's mTLS gate: `off` (no org check), `grace` (reject any client cert whose organization differs from the device's own org, but allow legacy certs that carry no org identity), `strict` (as `grace`, and additionally reject certs that carry no org identity). Unrecognized values also default to `strict`. See note below. |
### `WENDY_MTLS_ORG_ENFORCEMENT` migration
The device derives its own organization from its provisioning certificate and rejects connecting client certificates from a different organization. The default mode is `strict`: client certificates that carry no org identity are rejected.
If your fleet has certs that predate org identity (no `urn:wendy:org:…` SAN URI and no org-bearing CN), set `WENDY_MTLS_ORG_ENFORCEMENT=grace` during the migration window. In `grace` mode, cross-org certs that *do* carry an org are still rejected, while org-less legacy certs are allowed and logged at WARN.
After user certs have rotated to carry the org SAN URI, remove the override and let the agent run in the default `strict` mode. If the device cannot determine its own org from its certificate, enforcement is disabled for safety and logged at ERROR.
An unrecognized value for `WENDY_MTLS_ORG_ENFORCEMENT` is treated as `strict` (fail-safe), and the agent logs a warning.
## gRPC Ports and Provisioning State
The agent runs different gRPC servers depending on provisioning state:
| Port | Protocol | When active |
|---|---|---|
| `50051` (default) | Plaintext gRPC | Before the device is provisioned |
| `50052` (default, `agentPort + 1`) | mTLS gRPC | After the device is provisioned |
| `4317` | OTEL gRPC | Always |
| `4318` | OTEL HTTP | Always |
| `/var/lib/wendy/agent-control/agent.sock` | Plaintext gRPC (unix socket) | Always; gated by `admin` entitlement mount. Appears inside an entitled container as `/run/wendy/agent/agent.sock` (`WENDY_AGENT_SOCKET`) |
Once a device is provisioned, the plaintext port is shut down with `GracefulStop()`. The local unix socket serves the full gRPC API with no authentication; access is gated by the `admin` entitlement which bind-mounts the socket into entitled containers only. If you are connecting to a provisioned device and getting connection-refused errors on port 50051, check whether the device is already provisioned (look for certificates in `WENDY_CONFIG_PATH`).
## mTLS Authentication
After provisioning, all gRPC traffic uses mutual TLS backed by ML-DSA (post-quantum) certificates issued by the Wendy PKI. The agent enforces `tls.RequireAnyClientCert` and verifies the chain with a custom `VerifyPeerCertificate` callback because Go's `crypto/x509` does not natively handle ML-DSA signatures.
If you see TLS handshake failures:
- Confirm the CLI has valid certificates: `wendy auth status`
- Re-fetch certificates: `wendy auth refresh-certs`
- Check that `WENDY_CONFIG_PATH` on the agent contains `cert.pem`, `chain.pem`, and `key.pem`
## gRPC Tracing
The standard Go gRPC environment variables work with the agent and CLI:
```sh
# Log all gRPC events (very verbose)
GRPC_GO_LOG_VERBOSITY_LEVEL=99 GRPC_GO_LOG_SEVERITY_LEVEL=info ./wendy discover
```
You can also use `grpcurl` to call individual agent RPCs directly (useful when the CLI behaviour is not what you expect):
```sh
# List services on an unprovisioned device
grpcurl -plaintext :50051 list
# Call GetDeviceInfo
grpcurl -plaintext :50051 wendy.agent.services.v1.WendyAgentService/GetDeviceInfo
```
For a provisioned device you need the mTLS certificates:
```sh
grpcurl \
-cert ~/.config/wendy/cert.pem \
-key ~/.config/wendy/key.pem \
-cacert ~/.config/wendy/chain.pem \
:50052 list
```
## Common Issues
### Agent fails to start: "Failed to connect to containerd"
The agent logs a warning (not a fatal error) if containerd is unavailable:
```
{"level":"warn","msg":"Failed to connect to containerd (container features will be unavailable)"}
```
Container-related RPCs will return errors, but the rest of the agent (network management, provisioning, OTA updates) continues to function. Ensure containerd is running:
```sh
sudo systemctl start containerd
sudo systemctl status containerd
```
If you are using a non-standard socket path, set `WENDY_CONTAINERD_ADDR`.
### Agent fails to start: "Failed to listen on agent port"
Port 50051 is already in use. Find what is holding it:
```sh
ss -tlnp | grep 50051
```
Override the port via `WENDY_AGENT_PORT` if necessary.
### xdg-dbus-proxy not found
```
{"level":"warn","msg":"xdg-dbus-proxy not found, Bluetooth containers will have unfiltered D-Bus access"}
```
Install `xdg-dbus-proxy` for proper D-Bus isolation inside Bluetooth containers:
```sh
sudo apt install xdg-dbus-proxy
```
The agent continues to work without it, but Bluetooth container isolation is degraded.
### Network manager not detected
The Go `wendy-agent` drives WiFi through `nmcli` only — it does not auto-detect
between ConnMan and NetworkManager, and it ignores `WENDY_NETWORK_MANAGER`
(that variable is a Swift-agent feature). If `nmcli` is not on the agent's
`PATH`, every WiFi management RPC fails with `Unavailable: WiFi management is
not available (nmcli not found)`. Check:
```sh
which nmcli && nmcli general status
```
### Device not visible via `wendy discover`
`wendy discover` uses mDNS (`.local` hostnames). On macOS, the CLI runner must have the Local Network TCC permission. If running in a CI or non-interactive context, use `ssh localhost` to get a session that holds the permission (see the `integration-tests.yml` workflow for the pattern).
On Linux, check that `avahi-daemon` is running on the device:
```sh
sudo systemctl status avahi-daemon
```
The agent advertises itself via Avahi using the service definition in `/etc/avahi/services/wendy-agent.service`.
The CLI itself performs an mDNS browse on Linux hosts (shipped binaries are CGO_ENABLED=0 and cannot use nss-mdns). If discovery fails, the CLI prints hints about `avahi-daemon` and UDP port 5353. Set `WENDY_MDNS_DEBUG=1` to log mDNS query failures, or `WENDY_MDNS_TIMEOUT=8s` to extend the browse window on slow networks.
### mTLS handshake fails after re-provisioning
If the CLI reports expired client certificates, check this computer's clock, then refresh them:
```sh
wendy auth refresh-certs
```
If the device certificate was regenerated, the old mTLS port listener may still be running from the previous provisioning. Restart the agent to pick up the new certificate:
```sh
sudo systemctl restart wendy-agent
```
### mTLS handshake fails with "certificate not yet valid"
If the device's real-time clock (RTC) is not synchronized (common on first boot or after power loss), the device clock may predate the provisioning certificate's `NotBefore` time. This causes all client certificates to appear "not yet valid" from the device's perspective.
From a computer on the same LAN, send a signed time proof and retry the connection:
```sh
wendy device sync-time
```
The CLI also attempts this broadcast automatically after a possible clock-related
TLS failure. It reports whether sending failed and whether the connection retry
succeeded. A successful broadcast does not confirm that the device received the
proof or changed its clock. Giving the device internet access also allows its
own time synchronization to recover.
The agent logs a warning at startup if it detects clock skew. For TLS handshake details, run the CLI with:
```sh
WENDY_TLS_DEBUG=1 wendy device version
```
## Inspecting Agent Internals
### Container state
```sh
# List containers visible to containerd
sudo ctr -n wendy containers list
sudo ctr -n wendy tasks list
# List via agent gRPC
wendy apps list
```
### OTA update state
The agent tracks OTA update state in `WENDY_CONFIG_PATH`. To see the current provisioning info:
```sh
wendy device info
```
### Agent metrics
The agent collects CPU and memory metrics for itself (via `services.CollectAgentMetrics`) and for each running container (via `services.CollectContainerMetrics`). These are streamed through the OTEL broadcaster and available via:
```sh
wendy device telemetry-stream
```
### Running the agent under a debugger
Build with debug symbols (omit the `-s -w` ldflags stripping):
```sh
cd go
go build -o bin/wendy-agent-dbg ./cmd/wendy-agent
```
Then attach with `dlv`:
```sh
dlv exec ./bin/wendy-agent-dbg
# or attach to a running process:
dlv attach $(pgrep wendy-agent)
```
---
# Docs Site
Source: https://docs.wendy.dev/latest/advanced/development/docs-site/
---
title: "Docs Site"
description: "The public WendyOS documentation site is built with Fumadocs and Next.js, then published as a static export to https://docs.wendy.dev."
---
# Docs Site
The public WendyOS documentation site is built with Fumadocs and Next.js, then
published as a static export to `https://docs.wendy.dev`.
## URLs
| Purpose | URL |
|---|---|
| Latest stable docs | `https://docs.wendy.dev/latest/` |
| Latest nightly docs | `https://docs.wendy.dev/latest-nightly/` |
| Specific stable release | `https://docs.wendy.dev/release-