# 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-/` | | Specific nightly release | `https://docs.wendy.dev/release-nightly-/` | | Branch preview | `https://docs.wendy.dev/branch--/` | | Agent docs index for the latest stable docs | `https://docs.wendy.dev/llms.txt` | ## Source Layout The Fumadocs app is reached through `docs/`, which is a repository-root symlink to `go/internal/cli/assets/docs`. Top-level docs content lives as MDX and `meta.json` files in that tree. Existing lower-level Markdown reference files are still read from the same tree, but the prep script publishes them under the `advanced/` section at build time. ``` docs/ app/ Next.js app router routes, layout, and search components/ MDX components, search dialog, and providers guides/ Top-level guides and tutorials installation/ Top-level setup guides lib/ Fumadocs source loader and shared layout config scripts/ Content preparation for Fumadocs next.config.mjs package.json ``` Generated build output is ignored by git: `content/`, `public/`, `.source/`, `.next/`, `out/`, and `export/`. ## Local Development ```sh cd docs npm ci npm run dev ``` The dev server starts at `http://localhost:3000/`. When editing source Markdown or MDX files, rerun the content prep script to refresh generated Fumadocs content: ```sh cd docs node scripts/prepare-content.mjs ``` ## Local Build The deployed site uses a path prefix. Set `NEXT_PUBLIC_BASE_PATH` when testing a prefixed build: ```sh cd docs NEXT_PUBLIC_BASE_PATH=/branch-local npm run build ``` The static export is written to `docs/out/`. ## CI And Deploy The `.github/workflows/fumadocs.yml` workflow runs when `docs`, `docs/**`, `go/internal/cli/assets/docs/**`, or the workflow file changes. | Trigger | Behavior | |---|---| | `main` branch push | Builds and deploys a branch preview | | Pull request to `main` from this repository | Builds and deploys a branch preview, then posts or updates a sticky PR comment with the preview URL. Fork PRs do not receive preview comments. | | Published stable release | Deploys `release-/`, updates `latest/`, and publishes the root `llms.txt` and `llms-full.txt` | | Published prerelease/nightly | Deploys `release-nightly-/` and updates `latest-nightly/` | | Manual dispatch (no inputs) | Builds a branch-style preview artifact without deploying | | Manual dispatch with `release_tag` input | Deploys a release, identical to a published-release trigger. The `release_prerelease` input selects the target: `false` (default) deploys `release-/` and updates `latest/`; `true` deploys `release-nightly-/` and updates `latest-nightly/`. The dispatch ref must match `release_tag` (dispatch with `--ref ""`), otherwise the deploy fails fast so docs built from one ref are never published under a different release path. | Preview URLs are posted to same-repository PR comments and are visible to anyone who can read the PR. This is intentional; deploy paths are public preview paths and should not include sensitive information. The deploy job authenticates to GCP with Workload Identity Federation and syncs static files to `gs://wendy-docs-public/`. Static exports include SHA-256 manifests that are verified before each deploy path is synced. Release deploys attempt to enable bucket object versioning before updating `latest/` or `latest-nightly/` so alias overwrites remain recoverable. If the deploy identity lacks bucket-update permission, the deploy verifies the current state instead: it aborts only when versioning is confirmed disabled (an overwrite would be unrecoverable), and continues with a warning when versioning is already enabled out-of-band or cannot be read. Required GitHub environment variables: | Variable | Description | |---|---| | `GCP_WORKLOAD_IDENTITY_PROVIDER` | Workload Identity provider resource name | | `GCP_SERVICE_ACCOUNT` | Deploy service account email | | `GCP_PROJECT_ID` | GCP project ID | ## Hosting Static files are served from the public `wendy-docs-public` Cloud Storage bucket through the global external HTTP(S) load balancer for `docs.wendy.dev`. The load balancer terminates HTTPS and adds security response headers for the public docs host. Branch-preview objects under `branch-*` are cleaned up by CI after 30 days. ### Vanity redirects Short marketing paths are served as static redirect pages uploaded to the bucket root by the `Deploy vanity redirects` step in `.github/workflows/fumadocs.yml` (main-branch pushes and release deploys only): | Path | Target | |---|---| | `/pi` | `/latest/installation/wendyos-raspberry-pi-5/` | | `/thor` | `/latest/installation/wendyos-nvidia-jetson-agx-thor/` | | `/jetson`, `/jetson-orin`, `/jetson-orin-nano` | `/latest/installation/wendyos-nvidia-jetson-orin-nano/` | Add new entries to the `REDIRECTS` map in that workflow step. Slugs must not collide with deploy path prefixes (`latest`, `latest-nightly`, `release-*`, `branch-*`). ### Security headers The load balancer adds these headers to every `docs.wendy.dev` response. They are custom response headers on the backend bucket `wendy-docs-backend`, which the URL map `wendy-docs-url-map` routes `docs.wendy.dev` to, in the GCP project from `vars.GCP_PROJECT_ID`. No file in this repository manages them. `install.wendy.dev` and `templates.wendy.dev` share the load balancer but use other backend buckets without these headers. | Header | Value | |---|---| | `X-Content-Type-Options` | `nosniff` | | `X-Frame-Options` | `DENY` | | `Referrer-Policy` | `strict-origin-when-cross-origin` | | `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | | `Content-Security-Policy` | The policy below | ```text default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; frame-src https://www.youtube.com https://www.youtube-nocookie.com; img-src 'self' data: blob: https://www.googletagmanager.com https://*.google-analytics.com; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; connect-src 'self' https://www.googletagmanager.com https://*.google-analytics.com https://*.google.com; worker-src 'self' blob: ``` What the policy means for docs changes: - Images must come from the docs site itself. Commit them to the docs tree instead of linking to another host. The `YouTubeVideo` component uses local thumbnails in `images/youtube/` for this reason. - Iframes can only embed YouTube. - The Google Analytics hosts follow Google's list for GA4 without Ads features. Until 2026-09-27 the policy blocked `gtag.js`, so docs page views before that date were not recorded. Turning on Google Signals or linking Google Ads needs more hosts. Read the current headers: ```sh gcloud compute backend-buckets describe wendy-docs-backend \ --project= --format='yaml(customResponseHeaders)' ``` To change a header, pass the complete list. `gcloud compute backend-buckets update --custom-response-header` replaces every custom header, so any header you leave out is removed: ```sh gcloud compute backend-buckets update wendy-docs-backend --project= \ --custom-response-header="X-Content-Type-Options: nosniff" \ --custom-response-header="X-Frame-Options: DENY" \ --custom-response-header="Referrer-Policy: strict-origin-when-cross-origin" \ --custom-response-header="Permissions-Policy: camera=(), microphone=(), geolocation=()" \ --custom-response-header="Content-Security-Policy: " ``` Changes reach responses within a few minutes, including responses the CDN has already cached, so no cache invalidation is needed. Verify with `curl -sI https://docs.wendy.dev/latest/`, then open a docs page and confirm the browser console shows no Content Security Policy errors. ## Agent Docs (llms.txt) Each deploy path also publishes Markdown for AI agents. The "View raw Markdown" and "Agent docs index" page actions link to these files. | File | Contents | |---|---| | `/llms.txt` | Index in the [llms.txt](https://llmstxt.org) format: a summary, the docs pages, and the generated CLI and detailed reference under `## Optional` | | `/llms-full.txt` | The source of every page in one file | | `/markdown/.md` | The source of one page | `scripts/publish-markdown.mjs` writes these files during content preparation. Stable release deploys also copy `latest/llms.txt` and `latest/llms-full.txt` to the site root, where agents look first, in the `Publish root llms.txt` step of `.github/workflows/fumadocs.yml`. Nightly releases and branch previews leave the root copies unchanged. ## Release Notifications The release workflow adds docs links to Discord notifications: | Release type | Links | |---|---| | Stable | `release-/` and `latest/` | | Nightly | `release-nightly-/` and `latest-nightly/` | ## Validation Run these before opening or updating a PR that changes the docs app: ```sh cd docs npm run types:check npm run build ``` --- # Install Script Hosting Source: https://docs.wendy.dev/latest/advanced/development/install-hosting/ --- title: "Install Script Hosting" description: "The public install one-liners are served from Google Cloud, not GitHub Pages:" --- # Install Script Hosting The public install one-liners are served from Google Cloud, not GitHub Pages: ```bash curl -fsSL https://install.wendy.dev/cli.sh | bash curl -fsSL https://install.wendy.dev/agent.sh | bash ``` ## Why `install.wendy.dev` was previously a GitHub Pages custom domain (deployed by the now-removed `.github/workflows/static.yml`). GitHub Pages soft-throttles a single IP and returns `403` when many people install at once from one network (e.g. a workshop behind one NAT). The scripts also resolved their version from the GitHub REST API (60 requests/hour per IP), which failed the same way. Both are now served from GCS. ## What is hosted | Object | Served at | Content-Type | |---|---|---| | `agent.sh` | `https://install.wendy.dev/agent.sh` | `text/x-shellscript` | | `cli.sh` | `https://install.wendy.dev/cli.sh` | `text/x-shellscript` | | `manifest.json` | `https://install.wendy.dev/manifest.json` | `application/json` | `manifest.json` carries `{ "latest": "", "latest_nightly": "" }`. The scripts read `latest` to resolve the install version and fall back to the GitHub API only if the manifest is unreachable. ## Publishing The `publish-install-scripts` job in `.github/workflows/build.yml` uploads the two scripts and merges the channel pointer into `manifest.json` on every `main` push and on stable release, using `.github/scripts/publish-install-scripts.sh`. It authenticates with the same Workload Identity Federation identity as the docs and agent-mirror publishers. Objects are uploaded with `Cache-Control: public, max-age=300`, so changes take effect within about five minutes. ## Infrastructure Static files live in the public bucket `wendy-install-public` and are served through the **same** global external HTTP(S) load balancer that fronts `docs.wendy.dev` (see `docs-site.md`). The load balancer terminates HTTPS; a host rule routes `install.wendy.dev` to a backend bucket for `wendy-install-public`. ### One-time maintainer runbook (LB + cert + DNS) > **Provision before merge:** the bucket and its IAM bindings (step 1 below) > must exist BEFORE this branch merges to `main` — the `publish-install-scripts` > job runs on every `main` push and will fail if the bucket or write IAM isn't > in place yet. Run these once, from a shell authenticated to the Wendy GCP project. Replace `` with the project id, `` / `` / `` with the existing docs load-balancer resource names (find them with `gcloud compute url-maps list`, `gcloud compute target-https-proxies list`, `gcloud compute ssl-certificates list`). ```bash # 1. Create the public bucket and make its objects world-readable. gcloud storage buckets create gs://wendy-install-public \ --project= --location=us --uniform-bucket-level-access gcloud storage buckets add-iam-policy-binding gs://wendy-install-public \ --member=allUsers --role=roles/storage.objectViewer # Grant the CI/WIF deploy service account (vars.GCP_SERVICE_ACCOUNT) write access # so the publish-install-scripts job can upload objects and update the manifest. gcloud storage buckets add-iam-policy-binding gs://wendy-install-public \ --member=serviceAccount: --role=roles/storage.objectAdmin # 2. Seed the bucket so the LB has objects to serve before DNS cutover. # (CI republishes these on every build; this is just for verification.) gcloud storage cp go/internal/cli/assets/docs/agent.sh gs://wendy-install-public/agent.sh \ --content-type=text/x-shellscript --cache-control="public, max-age=300" gcloud storage cp go/internal/cli/assets/docs/cli.sh gs://wendy-install-public/cli.sh \ --content-type=text/x-shellscript --cache-control="public, max-age=300" # 3. Back the bucket with a backend bucket and add a host rule to the docs LB. gcloud compute backend-buckets create wendy-install-backend \ --gcs-bucket-name=wendy-install-public --enable-cdn --project= gcloud compute url-maps add-path-matcher \ --path-matcher-name=install \ --default-backend-bucket=wendy-install-backend \ --new-hosts=install.wendy.dev \ --global \ --project= # 4. Add install.wendy.dev to a Google-managed certificate and attach it. # Managed certs take minutes-to-tens-of-minutes to become ACTIVE. gcloud compute ssl-certificates create wendy-install-cert \ --domains=install.wendy.dev --global --project= gcloud compute target-https-proxies update \ --ssl-certificates=,wendy-install-cert --global --project= # 5. Find the LB's global anycast IP. gcloud compute forwarding-rules list --global --project= ``` Then, in the DNS zone for `wendy.dev`: - **Remove** the existing `install.wendy.dev` record pointing at GitHub Pages (`CNAME -> wendylabsinc.github.io`) and the GitHub Pages custom-domain binding in the repository's Pages settings. - **Add** `install.wendy.dev` `A` (and `AAAA` if the LB has IPv6) records → the load balancer's global IP from step 5. Verify before announcing: ```bash curl -fsSL https://install.wendy.dev/manifest.json curl -fsSL https://install.wendy.dev/agent.sh | head -1 # -> #!/usr/bin/env bash ``` ### After cutover Once `install.wendy.dev` resolves to the load balancer, flip the `publish-install-scripts` job's "Verify scripts are reachable" step in `.github/workflows/build.yml` from `https://storage.googleapis.com/wendy-install-public/` back to `https://install.wendy.dev/` (a comment in that step already flags this). --- # Connect Wendy Lite to Wendy Net over Bluetooth Source: https://docs.wendy.dev/latest/advanced/development/local-mesh-radio/ --- title: "Connect Wendy Lite to Wendy Net over Bluetooth" description: "Connect an enrolled Wendy Lite board to a nearby WendyOS device without joining a Wi-Fi access point. The WendyOS device carries the board's Wendy Net traffic onward. The Lite boar" --- # Connect Wendy Lite to Wendy Net over Bluetooth Connect an enrolled Wendy Lite board to a nearby WendyOS device without joining a Wi-Fi access point. The WendyOS device carries the board's Wendy Net traffic onward. The Lite board is an endpoint: it does not relay traffic for other devices. This is an advanced setup that requires compatible Lite firmware and explicit peer configuration. For ordinary WendyOS-to-WendyOS Bluetooth links, use [Wendy Net over Bluetooth](../../../hardware/bluetooth/#wendy-net-over-bluetooth). ## Before you start - Enroll both devices in the same Wendy Cloud organization. - Use a WendyOS device with Bluetooth and full Wendy Net support. - Ask your firmware administrator for the board's enrolled device UUID, Bluetooth address, address type, and signed-time service URL. - The Lite firmware must enable Bluetooth networking, trust the time service, and be configured with the WendyOS device's enrolled UUID. Automatic Lite peer discovery and rotating Bluetooth addresses are not supported. ## Configure the WendyOS device Merge the following fields into its existing `/etc/wendy-agent/local-mesh.json`, preserving the other settings: ```json { "leafBLEPeers": [{ "device": "12345678-9abc-4def-8123-000000000002", "address": "80:45:6b:00:00:01", "addressType": "public", "psm": 129 }], "leafTimeURL": "https://your-time-service.example/timestamp" } ``` Replace all example values. `device` is the Lite board's enrolled UUID, not its hardware ID. Use a different UUID for each board. Keep the file and its directory owned by the agent's system user and unwritable by other users. Restart `wendyos-agent.service` after editing. ## Check the connection ```sh wendy device wendy-net status --device wendy device wendy-net topology --device ``` Check for an authenticated link to the Lite board. A running Bluetooth provider alone does not mean the board connected. On a cold boot, allow at least two minutes for the board's replay protection before expecting normal traffic. If it does not connect, check the configured identities, Bluetooth address, firmware, and access to the signed-time service. Lite connections share the adapter's capacity with other Wendy Net Bluetooth links. Bluetooth is intended for low-bandwidth traffic; use Ethernet or Wi-Fi where available. ## Advanced configuration reference | Field | Meaning | | --- | --- | | `leafBLEPeers` | Up to eight explicitly configured Lite boards; physical Bluetooth connection limits still apply. | | `addressType` | `public` (default) or `random`. Each address/type pair must be unique. | | `psm` | Bluetooth service port, 128–255; defaults to 129. Keep 128 free if WendyCom management uses it. The firmware must use the same value. | | `leafTimeURL` | Required HTTPS signed-time URL when Lite peers are configured. Credentials, query strings and fragments are not accepted. | For custom firmware, enable `WENDY_MESH_BLE`, reserve at least two NimBLE connection-oriented channels, and set `WENDY_MESH_ROUTER_DEVICE` to the WendyOS device UUID. These are firmware settings, not Wendy CLI options. A Bluetooth address locates a board; enrollment authenticates it. --- # Screencast Tooling Source: https://docs.wendy.dev/latest/advanced/development/screencasts/ --- title: "Screencast Tooling" description: "The screencast/ directory is developer-only repository tooling for producing narrated engineering screencasts. It is not part of WendyOS, the Wendy CLI, the device runtime, or publ" --- # Screencast Tooling The `screencast/` directory is developer-only repository tooling for producing narrated engineering screencasts. It is not part of WendyOS, the Wendy CLI, the device runtime, or public product documentation. See `screencast/README.md` in the repository for the source of truth: authoring workflow, local commands, CI checks, voiceover setup, generated output rules, and hook safety. The `Screencast` GitHub Actions workflow (`.github/workflows/screencast.yml`) runs on pull requests to `main` that touch `screencast/**`, the workflow file, or Dependabot configuration. --- # Development Environment Setup Source: https://docs.wendy.dev/latest/advanced/development/setup/ --- title: "Development Environment Setup" description: "Use the Go version declared in the repo root’s go.mod. The CI workflows use actions/setup-go with go-version-file: go.mod, so the version is always read from that file — follow the" --- # Development Environment Setup ## Prerequisites ### Go Use the Go version declared in the repo root’s `go.mod`. The CI workflows use `actions/setup-go` with `go-version-file: go.mod`, so the version is always read from that file — follow the same practice locally. ```sh go version # should match or exceed the `go` line in go.mod ``` ### Separate routing module `go/babel` has its own `go.mod`. Work inside that directory when changing its library or running its tests; parent-module `./...` patterns skip it. CI uses the repository root's Go toolchain version for both modules. The library keeps a separate minimum language version for consumers. See the [routing test instructions](./testing.md#routing-engine-tests). ### System Dependencies #### macOS The CLI uses CoreBluetooth for BLE device discovery. CGO must be enabled when building on macOS (it is enabled by default): ```sh # If you have explicitly disabled CGO, re-enable it export CGO_ENABLED=1 ``` No additional system packages are needed on macOS for a standard CLI build. #### Swiftly and the clang shim If [Swiftly](https://github.com/swiftlang/swiftly) is installed, its `clang` shim (`~/.swiftly/bin/clang`) often appears ahead of `/usr/bin` on `PATH`. The shim refuses to run when the repository's `.swift-version` does not match an installed Swift toolchain, causing cgo builds to fail with an opaque `exit status 2` and no output. The `go/Makefile` works around this by setting `CC ?= /usr/bin/clang` on Darwin so Go's cgo toolchain falls back to Apple's real clang whenever `CC` isn't already set. This is applied automatically when you use any `make` target. If you invoke `go build` directly (not via `make`), set the variable yourself: ```sh export CC=/usr/bin/clang ``` #### Linux The audio subsystem uses ALSA. Install the development headers before running tests or building the agent: ```sh # Debian / Ubuntu sudo apt-get install -y libasound2-dev libusb-1.0-0-dev pkg-config # Fedora / RHEL sudo dnf install -y alsa-lib-devel libusb1-devel pkgconf-pkg-config ``` `libusb-1.0-0-dev` and `pkg-config` are required by the CLI's CGO dependency (USB recovery flashing via `gousb`) and by the SBOM integration test (`scripts/test/generate-sbom.integration.sh`), which builds `wendy` with `CGO_ENABLED=1`. #### Windows No additional system packages are required for a standard CLI build on Windows. Some local setup scripts are unsigned, so Windows may block them even when you trust the repository. If you need to run a local, trusted PowerShell setup script, use a one-time bypass only after reviewing the script: ```powershell Get-Content .\set-up-windows.ps1 powershell -ExecutionPolicy Bypass -File .\set-up-windows.ps1 ``` The bypass applies only to that PowerShell invocation. Run it from a non-elevated (standard-user) PowerShell window. If a specific step fails with an access-denied error, review that section of the script before re-running as Administrator. ### Additional Tools | Tool | Purpose | Install | |---|---|---| | `protoc` + `protoc-gen-go` + `protoc-gen-go-grpc` | Regenerating protobuf code | See [Protobuf section](#regenerating-protobuf-code) | | `golangci-lint` | Local linting (mirrors CI) | `go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest` | | `containerd` | Running the agent locally on Linux | `sudo apt install containerd` | | Node.js 24 + npm | Optional; only for developer screencast tooling in `screencast/` | [nodejs.org](https://nodejs.org) or `nvm install 24` | | `syft` | Generate SPDX SBOMs locally (`scripts/generate-sbom.sh`) | [github.com/anchore/syft](https://github.com/anchore/syft) or `brew install syft` | | `gh` | Verify release attestations (`gh attestation verify`) | [cli.github.com](https://cli.github.com) or `brew install gh` | | `cosign` | Alternative attestation verification | [docs.sigstore.dev](https://docs.sigstore.dev/cosign/system_config/installation/) or `brew install cosign` | | `cpio` | Building the WendyAgentMac Linux runtime guest initramfs | Included with macOS; install the distribution package on Linux | ### Environment Variables Local `.env*` files are ignored by git, while `.env.example` files may be committed as templates. Do not commit real credentials. For screencast voiceover variables, use `screencast/.env.example` and see [screencast tooling](../../../advanced/development/screencasts/). ## Cloning ```sh git clone https://github.com/wendylabsinc/wendy-agent.git cd wendy-agent ``` All Go code lives under `go/`. Run every `make` target and `go` command from that directory (or use the `make` targets which set the working directory automatically). ## Building The `Makefile` lives in `go/`. All targets below are run from `go/`. ### Build both binaries (native architecture) ```sh cd go make build # Produces: bin/wendy bin/wendy-agent ``` > **Note:** On Windows, only the CLI builds (`bin/wendy.exe`) because `wendy-agent` does not have Windows support. ### Build just the CLI ```sh make build-cli # bin/wendy on Unix, bin/wendy.exe on Windows ``` On Windows, `make build-cli` invokes PowerShell for the native Windows CLI build. ### Build just the agent ```sh make build-agent # bin/wendy-agent ``` > **Note:** `build-agent` is not supported on Windows. Run this target on macOS, Linux, or WSL. ### Cross-compile The Makefile contains pre-wired targets for every supported platform. All cross-compiled targets use `CGO_ENABLED=0`: ```sh make build-agent-linux-amd64 # bin/wendy-agent-linux-amd64 make build-agent-linux-arm64 # bin/wendy-agent-linux-arm64 make build-cli-darwin-arm64 # bin/wendy-darwin-arm64 make build-cli-windows-amd64 # bin/wendy-windows-amd64.exe make build-all # all of the above in one shot ``` Note: macOS CLI builds in CI run on a macOS runner (`macos-15`) with `CGO_ENABLED=1` because CoreBluetooth is required for BLE support. ### Version embedding The build embeds the version string via ldflags: ```sh VERSION=1.2.3 make build-cli # equivalent to: go build -ldflags "-s -w -X github.com/wendylabsinc/wendy/go/internal/shared/version.Version=1.2.3" ./cmd/wendy ``` A `dev` version is embedded when `VERSION` is not set. ### Build WendyAgentMac and its runtime guest Before packaging WendyAgentMac, generate the Linux kernel and initramfs used by the optional Local Build Service: ```sh bash swift/WendyAgentMac/RuntimeGuest/build.sh ``` The script requires Go, `cpio`, and either a reachable `buildctl` daemon or Docker with Buildx. It writes ignored `vmlinuz-arm64` and `initramfs-arm64.img` files under `swift/WendyAgentMac/Resources/runtime/`. Release CI runs this step before `swift/Scripts/Build.sh`; local packaging fails with an explicit message when the guest files are absent. ## Running locally without overwriting your installed binaries ### CLI Add a shell alias so you can iterate on CLI changes without touching your installed `wendy`: ```sh # ~/.zshrc or ~/.bashrc wendy-dev() { (cd /path/to/wendy-agent/go && go run ./cmd/wendy "$@") } ``` Use it wherever you'd use `wendy`: ```sh wendy-dev discover --json wendy-dev run ``` ### Agent ```sh wendy-agent-dev() { (cd /path/to/wendy-agent/go && go run ./cmd/wendy-agent "$@") } ``` ## Installing locally ```sh cd go make install # installs both binaries to $(go env GOPATH)/bin ``` > **Note:** `make install` is not supported on Windows because `wendy-agent` does not have a Windows build yet. Run this target on macOS, Linux, or WSL. ## Regenerating Protobuf Code The generated code under `go/proto/gen/` must not be edited by hand. To regenerate after changing a `.proto` file: 1. Install the required plugins: ```sh go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest ``` 2. Run the generation script: ```sh cd go make proto # runs scripts/generate-proto.sh ``` The script reads `.proto` sources from `proto-defs/` at the repo root and writes generated Go files to `go/proto/gen/` under three packages: `agentpb`, `agentpb/v2`, and `cloudpb`. OpenTelemetry protos are not generated here — they map to the upstream `go.opentelemetry.io/proto/otlp/...` packages. ## Device Setup (for running the agent locally) The agent manages containers via containerd. On a Debian/Ubuntu machine: ```sh sudo apt install containerd sudo systemctl start containerd sudo systemctl enable containerd ``` Then run the agent with: ```sh sudo ./bin/wendy-agent ``` The agent listens on port `50051` (plaintext, pre-provisioning) and port `50052` (mTLS, post-provisioning). It also serves a local unix socket at `/var/lib/wendy/agent-control/agent.sock` for on-device containers with the admin entitlement; inside an entitled container that socket appears at `/run/wendy/agent/agent.sock` (also exported as `WENDY_AGENT_SOCKET`). The OTEL collector listens on `4317` (gRPC) and `4318` (HTTP). All ports can be overridden via environment variables — see [debugging.md](../../../advanced/development/debugging/). ## Dependency License Checks The repo tracks a committed baseline of dependency licenses: ```sh make licenses # verify current deps match licenses.csv make licenses-update # regenerate licenses.csv after adding a dependency ``` Run `make licenses` before opening a PR if you added or upgraded dependencies. --- # Testing Source: https://docs.wendy.dev/latest/advanced/development/testing/ --- title: "Testing" description: "WendyOS uses both unit tests and integration tests to validate the CLI and agent behaviour." --- # Testing WendyOS uses both unit tests and integration tests to validate the CLI and agent behaviour. ## Routing engine tests The routing library in `go/babel` is a separate Go module. A parent-module `go test ./...` or `go vet ./...` does not include it. Run both checks from its own directory: ```sh cd go/babel go test -race -count=1 -timeout=300s ./... go vet ./... ``` The CI aggregate `Test` check requires the dedicated Babel job. The library's [README](https://github.com/wendylabsinc/WendyOS/blob/main/go/babel/README.md) contains additional fuzz and benchmark commands. ## Integration Tests Integration tests verify that specific features work end-to-end on a real (or simulated) Wendy device. They live under `.github/ci-tests/` in the repository. ### Directory structure Each integration test is a self-contained directory: ``` .github/ci-tests// ``` The naming convention is `-`, for example `python-camera` or `swift-filesystem`. Entitlement boundaries use paired positive and negative fixtures where useful: `python-notifications` verifies the private app connection is present, while `python-no-notifications` verifies it is absent without the entitlement. Depending on the language, a test directory contains: **Python tests:** ``` .github/ci-tests// wendy.json # App identity and entitlements main.py # Test application Dockerfile # FROM python:3.11-slim ``` **Swift tests:** ``` .github/ci-tests// wendy.json # App identity and entitlements Package.swift Sources//main.swift ``` **Compose tests:** ``` .github/ci-tests// docker-compose.yml # No wendy.json needed ``` ### `wendy.json` format for CI tests ```json { "appId": "sh.wendy.ci.", "platform": "linux", "version": "1.0.0", "language": "python", "entitlements": [...] } ``` ### Test behaviour - Test apps must print a line containing `PASS` and exit `0` on success. - Negative/blocking tests (verifying a capability is absent) should exit `1` if the blocked capability accidentally succeeds. - Each test is single-purpose: it proves exactly one feature works. Both signals are required. An attached `wendy run` exits `0` whatever the container did, so the harness reads the exit code back from `wendy device apps list --json` instead — and falls back to the printed verdict on agents that record no exit code, or when an app deployed under a `serviceName` reports only a running state. A test that prints no verdict can only be judged by the exit-code path. Assert on something the feature actually changes inside the container. A signal that is present whether or not the entitlement was granted proves nothing: `/sys` is a plain read-only sysfs mount, so device nodes under `/sys/class/*` are visible to every container regardless of entitlements. Write the negative test alongside the positive one and check that each fails when the entitlement is flipped — a test that cannot fail is worse than no test. ### Running integration tests locally Tests are driven by `go/scripts/test-ci.sh`. Its `ALL_TESTS` array is the single source of truth for what runs: adding a name there is all that is needed, and CI runs whatever it lists. ```sh bash go/scripts/test-ci.sh ``` Refer to the `usage()` block in that script for a full list of available test names and options. ### Identifying the device under test Before running anything, the harness logs the agent version, OS, device type and architecture it found via `wendy device info`: ``` ==> Agent version: 2026.07.27-003050 ==> Device OS: wendyos WendyOS-0.17.0 ==> Device type: jetson-agx-thor (arm64) ``` Read this first when triaging a failure. A result is only meaningful against a known agent, and an out-of-date agent produces failures indistinguishable from product bugs. Two values are worth recognising: - `Agent version: dev` — an unstamped binary, so a hand-built agent rather than a release. Push a current one with `wendy device update --device `. - `Device type: none reported` — no `/etc/wendyos/device-type`, so the target is a generic Linux agent install and not a WendyOS image. Such a host is skipped by the nightly OS update and has no auto-updater, so its agent only changes when someone deploys one. ### CI execution Integration tests run in `.github/workflows/integration-tests.yml` on both macOS and Linux runners. Neither job names the tests it runs — the harness runs `ALL_TESTS` and decides what is applicable, so a newly registered test needs no workflow change. The harness skips a test rather than failing it when the host or device cannot support it: | Tests | Skipped unless | | --- | --- | | `swift-*` | the build host has a `swiftly` toolchain (checked instead of `swift`, which on macOS is an Xcode shim that exists with no usable toolchain behind it) | | `*-gpu` | the device reports `gpuVendor: nvidia` and a supported architecture. Orin (`sm_87`) uses the JetPack-6 / CUDA-12 fixtures; Thor (`sm_110`) and Spark (`sm_121`) use Ubuntu-24.04 / CUDA-13 fixtures. An older agent with no architecture uses CUDA 13 when its device type is `jetson-agx-thor` and otherwise retains the CUDA-12 path for compatibility. Other reported architectures are skipped until a matching fixture is hardware-verified. | #### Stable release gate When a release build is triggered with `publish=true`, the full macOS integration test suite runs as a required gate before the `release` and `publish-linux-repos` jobs proceed. If integration tests fail, neither job runs. Nightly builds (triggered by a `push` event or `publish=false`) skip the integration-test gate and proceed directly to release. #### PR gate for CI config changes Pull requests from within the repository that modify files under `.github/` automatically trigger a macOS integration test run via `.github/workflows/pr-integration-tests.yml`. Only one run is active per PR at a time; opening a new commit cancels the previous run. --- ## Automated Integration Test Coverage (CI) `.github/workflows/integration-test-review.yml` checks whether a PR introduces CLI features, device capabilities, entitlements, or deployment modes that no integration test covers, and suggests the missing tests. ### How it works 1. **Trigger:** `pull_request` opened, synchronize, or reopened against `main`, for branches in this repository only. 2. **Diff fetch:** Downloads the PR diff via the GitHub CLI. 3. **Analysis:** Sends the diff, the PR title/body, the existing CI tests, `go/scripts/test-ci.sh`, and `.github/workflows/integration-tests.yml` to Claude, with a prompt describing the test layout and the registration step. It answers `NO_CHANGES_NEEDED` when nothing is missing. 4. **Suggestion:** Posts the reply as a PR review comment containing the full file content for each suggested test. Nothing is committed — apply, adapt, or dismiss the suggestions yourself. The suggestions are only as current as that prompt. When the harness changes how tests are registered, run, or skipped, update the prompt in the workflow too, or it will keep advising authors to edit things that no longer exist. --- # App Entitlements Source: https://docs.wendy.dev/latest/advanced/device/entitlements/ --- title: "App Entitlements" description: "Entitlements grant a containerized app access to host resources — networking, GPU, cameras, serial ports, and more. An app gets only what it declares; anything not listed is unavai" --- # App Entitlements Entitlements grant a containerized app access to host resources — networking, GPU, cameras, serial ports, and more. An app gets **only** what it declares; anything not listed is unavailable. Entitlements apply to WendyOS container targets (`linux`/`wendyos`) and `wendy-lite`; native macOS (`darwin`) apps run non-containerized and don't use them. Declare them in the `entitlements` array of your `wendy.json`, or add one with `wendy project entitlements add `: ```json { "appId": "com.example.app", "platform": "linux", "entitlements": [ { "type": "network", "mode": "host" } ] } ``` > **Complete reference:** for every entitlement type, its options, and security notes, see [wendy.json → Entitlements](../apps/wendy.json.md#entitlements-1). This page is a guide to the common ones and how to choose between similar options. ## Notifications Use `{ "type": "notifications" }` when an app needs to alert operators through Wendy Cloud and Companion. WendyKit is currently the only application SDK for this API and is Swift-only. Apps in other languages must call the app-facing gRPC service directly. See [Send notifications from a device app](../../../guides/device-notifications/) for setup and examples, or the [Notifications API](../cloud/notifications-api.md#app-facing-api-wendysystemv1) for the exact RPC contract. | Boundary | Value | |---|---| | Read-only mount | `/run/wendy/system` (one private app-facing socket per app) | | Injected environment | `WENDY_SYSTEM_SOCKET=/run/wendy/system/system.sock` | | Supplementary group | GID `2000`, so non-root apps can connect without world access | | Stable host directory | Per-app subdirectory under `/var/lib/wendy/app-system` | | Cloud deadline | 15 seconds per `Send` call | | Socket restoration | Recreated from persisted container labels after agent/daemon restart | The app supplies one or more user, organization team, or role selectors plus content, severity, deep link, a caller-chosen Notification UUID v4 resource identity, and optional metadata. After successful creation, any canonical UUID reuse returns `ALREADY_EXISTS` rather than replaying success. A local validation or rate-limit rejection does not claim the UUID, so that UUID remains valid for retry. All selector categories are unioned, normalized, and deduplicated. The app-facing API accepts at most 100 selector entries before deduplication; Cloud resolves at most 100 recipients for a device-app send. The agent/daemon stamps trusted `app_id`; Wendy Cloud stores it as `created_by_app_id` and derives device and organization identity from device mTLS. Apps without the entitlement receive no private app connection mount, environment variable, or socket group. The full administrative Agent socket remains separate and requires `admin`. All entitled services in a multi-service app share the app's stable socket directory and app identity. Running containers reconnect on their next call after socket restoration; no redeploy is required. Stopped containers retain their mount and can reconnect when started again. The directory remains until the last entitled service container is deleted. ## Wendy Data (episode-write) Use `{ "type": "episode-write" }` when an app needs to write into the device's recorded dataset: it submits structured events or predictions to the episode recorder. The agent gives each app a private socket and derives the trusted app identity from that socket; the administrative agent socket is never exposed. The name says write, because that is what it grants, and the grant is stronger than plain logging: campaign triggers match on application event names and prediction attributes, so an app holding `episode-write` can start recordings. It grants no read access to any sensor. Reading is native: an app opens the agent-fed device node granted by `camera`, and frame identity arrives in-band with the frames. | Boundary | Value | |---|---| | Read-only mount | `/run/wendy/data` | | Injected environment | `WENDY_DATA_SOCKET=/run/wendy/data/data.sock` | | Supplementary group | GID `2000` | | Maximum record | 64 KiB, versioned length-prefixed JSON | | Acknowledgements | `buffered`, `recorded`, or `rejected` | | Socket restoration | Recreated from persisted container labels after agent/daemon restart | When no Episode is active, accepted records enter the bounded application pre-roll buffer. They can trigger an armed campaign by event name or model uncertainty. Running containers reconnect after socket restoration without a redeploy. Apps without `episode-write` receive neither this mount nor the environment variable. A `prediction` record may carry an optional `inputs` list of `{source_id, sample_id}` naming the harness samples it was computed from (see Wendy Sensors). The agent records the references in the Episode so `(input, outcome)` pairs can be reconstructed offline; a record without them is still accepted. ## Network The network entitlement allows the container to access the device's network. If the device is connected to WiFi, Ethernet or otherwise, the container will have access to make TCP and UDP connections to the internet. A "network" type entitlement can have the following values: - **mode**: `host` shares the device's network for LAN access; `bridge` provides outbound DNS/downloads through NAT without LAN port publishing; `none` provides only loopback. Declare a mode explicitly. Omitting the entire entitlement gives an ordinary container only loopback, and `wendy run` warns before building. In a shared-network group, secondaries inherit the primary service's namespace. > Note: NetworkMode `none` does not support remote debugging. ```json { "type": "network", "mode": "host" } ``` ### Wendy Net service access Use `network` mode `mesh` with `serviceCIDR: "10.99.0.0/16"` and isolated networking. Declare incoming TCP or UDP ports explicitly. This enables service access, not general outbound Internet access. Follow the [service sharing guide](../../../guides/wendy-net-services/) for complete manifests. ### Private Avahi Add `{ "type": "avahi" }` when an app uses Avahi through D-Bus or compatible client libraries. Apps using their own mDNS socket do not need this grant. Avahi does not grant network access by itself; combine it with the appropriate private network entitlement. See the [Avahi workflow](../../../guides/wendy-net-services/#alternative-use-private-avahi-through-d-bus). ## HTTP Use `http` to declare a web endpoint for clients to discover. It grants no additional network access; combine it with host networking for a container server: ```json [ { "type": "network", "mode": "host" }, { "type": "http", "port": 8080 } ] ``` Ordinary detached agent deployments include HTTP URLs in their [endpoint report](../clients/wendy-cli/commands/run.md#detached-output), without checking application health. See the [full `http` reference](../apps/wendy.json.md#http) for discovery and attached-run behavior. ## Wi-Fi Aware Use `{ "type": "nan" }` only when your app needs to manage Wi-Fi Aware discovery and direct connections itself. Ordinary Wendy Net apps do not need this grant. For an app that sends IP traffic over those direct connections, also add `{ "type": "network", "mode": "host" }`. This grants direct radio control to a trusted app. Wi-Fi Aware shares the device's radio with other apps and Wendy Net, so avoid radio-wide start, stop, or reset commands. See the [configuration reference](../apps/wendy.json.md#nan) for the app's control socket, interface, and reply-directory settings. ## Input The input entitlement allows the container to access Linux input devices such as game controllers, barcode scanners, keyboards, and other devices that appear under `/dev/input/`. This is separate from the USB entitlement — USB covers `/dev/bus/usb` (raw USB access), while input covers the higher-level Linux input subsystem. ```json { "type": "input" } ``` The container receives: - A bind mount of `/dev/input/` (including `by-id/` symlinks for stable device identification) - Membership in the host's `input` group for device permissions - A cgroup device rule allowing access to input devices (major 13) ### Device discovery Event device numbers (`/dev/input/event0`, `event1`, etc.) are assigned dynamically and can change across reboots. Use a stable symlink basename under `/dev/input/by-id/` where one exists, or the evdev device's `uniq` identity, to identify devices reliably: ``` /dev/input/by-id/usb-USBKey_Chip_USBKey_Module_202730041341-event-kbd ``` ### Bluetooth game controllers BlueZ on WendyOS owns pairing, trust, and reconnect. Pair and trust the pad on the device first (both behaviors are enabled by default): ```sh wendy device bluetooth connect
``` Then declare `{ "type": "input" }` on the service that reads the controller and select it by `/dev/input/by-id` basename or evdev `uniq`. No controller API, mapping service, or raw `usb` entitlement is required for standard Linux gamepad event codes. The same input entitlement also covers a controller used over wired USB. ### When to use input vs USB | Entitlement | Access | Use case | |-------------|--------|----------| | `input` | `/dev/input/` (Linux input subsystem) | Reading input events — game controllers, barcode scanners, keyboards | | `usb` | `/dev/bus/usb` (raw USB) | Low-level USB communication — custom protocols, firmware updates, libusb | Most USB HID devices (scanners, keyboards) should use `input`. You only need `usb` if your app talks raw USB protocols. ## USB The USB entitlement allows the container to access USB devices. Wendy bind-mounts `/dev/bus/usb` when that directory exists on the host. On a host without a USB bus, including a VM, it skips the mount so container startup does not fail with `cannot stat /dev/bus/usb`. This grants no virtual USB devices: an app that needs a physical camera still needs that camera and its driver, or an explicit [simulator backend](../apps/wendy-services.md#simulation-backends). If the USB directory appears after deployment, redeploy to mount it. ## Serial / UART **UART access is the `serial` entitlement.** In Wendy, talking to a device over a UART means opening its serial tty node, and that is exactly what `serial` grants — so if you're looking for "UART," this is the section. It is **USB-serial only** (`ttyACM*`/`ttyUSB*`): on-board UARTs (`ttyAMA*`, `ttyS*`) are intentionally not supported, because `ttyS` shares its kernel major with a board's system-console UART and exposing it would add attack surface for no peripheral benefit. To reach a USB device with a raw protocol instead of a tty, see [USB](#usb); for HID event devices, see [Input](#input). The serial entitlement grants a container access to a serial tty node so it can `open()` a port such as `/dev/ttyACM0` or `/dev/ttyUSB0`. The motivating case is USB-serial peripherals — for example the LeRobot SO-101 arm, whose Feetech bus servos are driven over a USB-serial adapter via `pyserial`. ```json { "type": "serial", "device": "ttyACM0" } ``` - **device**: A bare tty node name (not a path), matching `^(ttyACM|ttyUSB)[0-9]+$` — e.g. `ttyACM0`, `ttyUSB0`. A device that does not match this pattern is rejected at validation. The entitlement is **USB-only**: on-board UARTs (`ttyAMA*`, `ttyS*`) are not supported — `ttyS` shares its major with a board's system-console UART, so allowing it would add attack surface for no peripheral benefit. The container receives: - A bind mount of the named node `/dev/` (e.g. `/dev/ttyACM0`) - A cgroup device rule scoped to **exactly that device's** `major:minor` with `rw` access (no `mknod`). The node is resolved by `stat()` on the host at deploy time, so the rule grants access to that one device only — not the whole kernel major. (Expected majors: `ttyACM` = 166, `ttyUSB` = 188.) - Membership in the `dialout` group (GID 20), which owns serial tty nodes on Debian/Ubuntu hosts > The device must be connected when you deploy: Wendy resolves its `major:minor` at container creation and fails fast with a clear error if `/dev/` is absent. > **Replug behavior:** the cgroup rule is pinned to the `major:minor` resolved **at deploy time**. The kernel can hand a USB-serial node a different `minor` when you unplug and replug it (e.g. `/dev/ttyACM0` comes back as `/dev/ttyACM1`, or the same name with a new minor). After reconnecting the device, **redeploy the app** (`wendy run`) so Wendy re-resolves the node and rebuilds the rule — until then the container's access points at the old minor. ### When to use serial vs USB | Entitlement | Access | Use case | |-------------|--------|----------| | `serial` | The kernel tty node (`/dev/ttyACM*`, `/dev/ttyUSB*`, …) | Serial ports — USB-serial adapters, microcontrollers, servo buses, GPS modules; anything `pyserial`/termios opens | | `usb` | `/dev/bus/usb` (raw libusb, major 189) | Low-level USB protocols — custom protocols, firmware updates, libusb | Use `serial` for serial ports and `usb` for raw USB protocols — they expose different device majors. The SO-101's `/dev/ttyACM0` is reached with `serial`, not `usb`. > **Security note:** the cgroup rule is scoped to the named device's exact `major:minor`, so — unlike a whole-major grant — it never exposes other devices that share the major (e.g. other `ttyACM*`/`ttyUSB*` adapters on the host). The entitlement is USB-only, so it can never reach an on-board console UART. See *Replug behavior* above for why a reconnect needs a redeploy. ## SPI The SPI entitlement grants a container access to the host's SPI buses so it can talk to SPI peripherals (displays, ADCs, sensors, radios) through the `spidev` user-space interface at `/dev/spidev.`. ```json { "type": "spi" } ``` The entitlement takes no options — it grants access to the SPI subsystem as a whole. The container receives: - A bind mount of every `/dev/spidev*.*` node present on the host at deploy time (e.g. `/dev/spidev0.0`, `/dev/spidev0.1`) - Membership in the `spi` group, when that group exists on the host, for device permissions - A cgroup device rule allowing the SPI major (153) with `rw` access (no `mknod`) ### Bus and chip-select numbering `spidev` nodes are named `spidev.`. The bus is the SPI controller and the chip-select picks which device on that bus you address — so `/dev/spidev0.0` is bus 0, chip-select 0, and `/dev/spidev0.1` is bus 0, chip-select 1. Your app opens the specific node for the peripheral it drives. ### Enabling SPI on the board The `spidev` nodes only exist if SPI is enabled in the board's device tree — otherwise `/dev/spidev*` is absent and there is nothing to bind. On Raspberry Pi, enable it in `/boot/firmware/config.txt`: ``` dtparam=spi=on ``` then reboot. Confirm the nodes exist before deploying: ``` ls /dev/spidev* # /dev/spidev0.0 /dev/spidev0.1 ``` > The nodes are bind-mounted as they exist **at deploy time**. If you enable SPI (or wire up a device that adds a new `spidev` node) after the app is already running, **redeploy the app** (`wendy run`) so Wendy re-scans `/dev` and mounts the new node. ### Example A minimal read/write with Python's [`spidev`](https://pypi.org/project/spidev/): ```python import spidev spi = spidev.SpiDev() spi.open(0, 0) # /dev/spidev0.0 — bus 0, chip-select 0 spi.max_speed_hz = 1_000_000 resp = spi.xfer2([0x01, 0x02, 0x03]) print(resp) spi.close() ``` > **Security note:** unlike `serial` and `i2c` — which are scoped to a single named node's exact `major:minor` — `spi` is a **whole-major** grant. An app that declares it can open **every** SPI bus on the host, not just one. This is deliberate: the entitlement exposes the SPI subsystem rather than an individual bus, and a host can present many `spidev*.*` nodes whose minors are not known ahead of time. Grant it only to apps you trust with all of the device's SPI buses. ## Audio The audio entitlement grants a container playback and capture access, both through the host's PipeWire session and through raw ALSA. One entitlement covers speakers and microphones alike. ```json { "type": "audio" } ``` The entitlement takes no options. The container receives: - A bind mount of `/dev/snd` for raw ALSA access - Membership in the `audio` group (GID 29) for device permissions - A cgroup device rule allowing the sound major (116) with `rw` access (no `mknod`) - The host's user-session PipeWire socket, mounted at `/run/pipewire/pipewire-0`, with `PIPEWIRE_RUNTIME_DIR=/run/pipewire` set - `PULSE_SERVER=unix:/run/pipewire/pulse-native`, when the host session exposes a PulseAudio compatibility socket ### Prefer PipeWire over raw ALSA Both paths are granted, but they do not reach the same devices. ALSA can only see sound cards. A Bluetooth speaker or microphone has no card behind it — it exists only as a node in the PipeWire graph — so `aplay` and `arecord` cannot reach one at all. Use a PipeWire-aware client (`pw-play`, `pw-record`) or a PulseAudio one (`paplay`, GStreamer's `pulsesink`), which covers sound cards and Bluetooth endpoints alike. Reach for raw ALSA only when you specifically need a wired card. ### When the host has no audio session Only the socket belonging to a **user session** is mounted. WendyOS runs the session manager inside the `wendy` user's session, and a PipeWire instance without one has an empty graph — no sinks, no sources, and no PulseAudio socket beside it. If no user session is running, the container still gets `/dev/snd` and the audio group, but no socket and no environment variables. This is deliberate: handing an app a socket onto an empty graph would give it something that looks like working audio and plays nothing. Check for `PIPEWIRE_RUNTIME_DIR` if your app needs to distinguish the two cases. ## Persist The persist entitlement allows the container to persist data across restarts. Data is stored on the host filesystem and mounted into the container at the specified path. ```json { "type": "persist", "name": "my-volume", "path": "/mnt/data" } ``` - **name**: A unique name for the volume. Volumes with the same name are shared across apps. - **path**: The path inside the container where the volume is mounted. ### Shared Volumes Volumes are identified by name only (not by app ID), so multiple apps can share data by using the same volume name. This is useful for sharing caches or data between apps. ### Recommended Shared Volume Names | Name | Path | Description | |------|------|-------------| | `huggingface-cache` | `/app/.cache/huggingface` | Shared cache for Hugging Face models (transformers, datasets, etc.). Avoids re-downloading large ML models for each app. | Example for a Python ML app: ```json { "type": "persist", "name": "huggingface-cache", "path": "/app/.cache/huggingface" } ``` ## Build The build entitlement 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). > **Warning: Privileged-equivalent: a container→host escape surface.** Used so a device can build apps for itself (see the `claude-on-device` example). Grant only to fully-trusted, first-party apps. At most one per app. --- # MCPExample Source: https://docs.wendy.dev/latest/advanced/Examples/MCPExample/ --- title: "MCPExample" description: "A minimal reference implementation of a WendyOS app that exposes device tools over the Model Context Protocol (MCP). Deploy it with wendy run and its tools become instantly availab" --- # MCPExample A minimal reference implementation of a WendyOS app that exposes device tools over the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). Deploy it with `wendy run` and its tools become instantly available inside `wendy mcp serve`. ## What it does The example runs a Python MCP server built with [FastMCP](https://github.com/jlowin/fastmcp) and served over HTTP by `uvicorn`. It exposes three tools: | Tool | Description | |------|-------------| | `ping` | Returns `"pong"` — useful for checking reachability | | `device_info` | Returns hostname, CPU architecture, OS, and Python version | | `run_command` | Runs an arbitrary shell command and returns `exit_code`, `stdout`, and `stderr` | ## File layout ``` MCPExample/ Dockerfile # Python 3.11-slim image main.py # FastMCP server definition requirements.txt # mcp[cli] + uvicorn[standard] wendy.json # App config with mcp entitlement ``` ## wendy.json ```json { "appId": "com.wendylabs.examples.mcp-example", "platform": "linux", "version": "1.0.0", "language": "python", "python": { "container": { "sourceRoot": "/app" } }, "entitlements": [ { "type": "network", "mode": "host" }, { "type": "mcp", "port": 3000 } ] } ``` Two entitlements are required: - **`network` (host mode)** — lets the agent reach the container's MCP port over loopback (`127.0.0.1:3000`). - **`mcp`** — tells the agent that this container runs an MCP server on port `3000`. The agent stores the port in the `sh.wendy/mcp.port` container label and exposes the container's tools through `wendy mcp serve`. ## Authentication `wendy mcp serve` uses the auth session stored by `wendy auth login`. If multiple sessions are stored, MCP tools inherit whichever session is active — either the persisted default (set with `wendy auth use`) or a session selected via `cloud_grpc` in the tool call. **Multiple auth sessions:** When more than one Wendy Cloud session is stored, pass `cloud_grpc` in the tool call to select one explicitly, or run `wendy auth use` in your terminal to set a persistent default that MCP tools will use automatically. ## Running the example ```sh cd Examples/MCPExample wendy run ``` `wendy run`: 1. Builds the Docker image for the target device architecture. 2. Pushes and starts the container on the device. 3. Because of the `mcp` entitlement, the container's port (`3000`) is registered with the wendy agent. Once the container is running, start the MCP server: ```sh wendy mcp serve ``` The `ping`, `device_info`, and `run_command` tools appear prefixed with the app name (e.g. `com.wendylabs.examples.mcp-example/ping`) in any connected AI assistant. ## Environment variables | Variable | Default | Description | |----------|---------|-------------| | `MCP_PORT` | `3000` | TCP port the MCP server listens on. Must match `wendy.json`. | ## Dependencies ``` mcp[cli]>=1.9.4 uvicorn[standard]>=0.27.0 ``` ## How the proxy works When an AI assistant calls a tool on this container, the request flows through: ``` AI Assistant │ MCP (stdio) ▼ wendy mcp serve │ StreamMCP gRPC (bidirectional streaming) ▼ wendy-agent (on device) │ TCP ▼ container MCP server (127.0.0.1:3000) ``` ### Result size cap `wendy mcp serve` applies the same 100,000-byte output cap to proxied container tool results that Wendy's built-in tools use. If a container tool's JSON result exceeds this limit, the proxy replaces its content with a truncation envelope: ```json { "truncated": true, "max_bytes": 100000, "note": "proxied container tool output exceeded max_bytes; ask the app's tool for a narrower result" } ``` The `isError` flag is preserved through truncation, so an error result still surfaces as an error with bounded content. To avoid truncation, design tools to return focused, paginated, or summarized output instead of raw file, log, or model-output dumps. The wendy agent's `StreamMCP` RPC proxies raw bytes between the gRPC stream and the container's TCP port. The agent verifies that: - The container has an `mcp` entitlement (non-zero `sh.wendy/mcp.port` label). - The container is in the `RUNNING` state before attempting the TCP connection. See the wendy-agent MCP documentation for the full API. --- # Issue Response Plan Source: https://docs.wendy.dev/latest/advanced/incident_response/ --- title: "Issue Response Plan" description: "This document outlines the procedure for dealing with any security issues the arise related to WendyOS and its associated tools and libraries." --- # Issue Response Plan This document outlines the procedure for dealing with any security issues the arise related to WendyOS and its associated tools and libraries. ## Communication All communication throughout this process, except for public announcements should happen exclusively through private channels. Not through public issues, PRs or public Discord Channels. Use the **private Github Security Advisory thread** for communication with the reporter. ## Response Process ### Triage Acknowledge receipt of report within 5 working days. Read the report and validate vulnerability exists. If a PoC (Proof of Concept) is provided, verify it demonstrates the vulnerability. Determine whether the vulnerability is in WendyOS, if not report upstream to relevant parties. ### Scope Identify the release of the library where this vulnerability was introduced. Determine the severity/impact of the issue. - Does the vulnerability expose confidential data, provide potential for denial of service attacks. - Ease of replication: does it require precise timing, a significant number of steps, or a specific configuration. - Is the vulnerability actively being exploited? - Use the [CVSS 4.0](https://www.first.org/cvss/v4.0/specification-document) base score as a guide. Supply-chain and CI/CD incidents are always treated as **Critical** regardless of CVSS. ### Mitigate 1. Use a private repo, or private pull request via GitHub advisory reporting to generate a patch. 2. Ask the reporter to verify patch. 3. Alert important and trusted adopters. 4. Prepare advisory - Title: {description} - Summary: Summary of vulnerability, severity. - Details: More detailed description of vulnerability. - Impact: How does this issue impact users of WendyOS. - Mitigations: Are there ways to mitigate against this issue without updating. - Timeline: When reported, patch written, CVE request, received and published. - Patches: Details of patched versions and patch. - Credit: Add credit to reporter - Select CWE. 5. Request CVE 6. We aim to resolve any issue within 30 days, but this may not always be possible. ### Remediation and Disclosure - Merge pull request and release versions of affected library with fix included. Security fixes are only applied to the **latest** stable releases of WendyOS. End users will be required to upgrade to latest once a release is made. - Release CVE advisory. ## Learn - How was the vulnerability introduced? - What could we have done to prevent it? - Is there anything we can to ensure it doesn't happen again. - What did we do well during the process? - What could be improved in the process? --- # Connect to Wendy Lite through Wendy Cloud Source: https://docs.wendy.dev/latest/advanced/lite-cloud-relay/ --- title: "Connect to Wendy Lite through Wendy Cloud" description: "Use Wendy Cloud to reach an enrolled Lite board when you are away from its local network. The board needs an internet connection and must appear online." --- # Connect to Wendy Lite through Wendy Cloud Use Wendy Cloud to reach an enrolled Lite board when you are away from its local network. The board needs an internet connection and must appear online. ## Find and select your board ```sh wendy cloud login wendy cloud discover ``` Use the Cloud selector shown for the board when a command asks for a device. You can also choose the board in the device picker's Cloud tab. Wendy opens an encrypted connection to the selected board automatically; you do not need to configure a tunnel or open an inbound port on its network. Your account needs permission to connect to the device in its organization. Use a personal sign-in for Lite connections; service-account sessions do not support this connection path. ## If the connection fails 1. Check that the board is powered, connected to Wi-Fi, and online in `wendy cloud discover`. 2. Confirm that your selected sign-in belongs to its organization with `wendy auth status`. Use `wendy auth use` to switch if needed. 3. If authentication has expired, sign in again with `wendy cloud login`. 4. Update the CLI and board firmware if the error reports an unsupported protocol. Connections have idle and maximum-duration limits. Start the command again if a session expires. The connection is also limited by certificate expiry. ## Enrollment and removal [Enroll a board over USB](../../advanced/cli/lite-enrollment/) before using Cloud access. USB is also required to clear the board's local enrollment. You can [remove Cloud access](../../advanced/clients/wendy-cli/commands/device/unenroll/) while the board is offline, but that does not erase its local settings. --- # Certificates and device trust Source: https://docs.wendy.dev/latest/advanced/pki/ --- title: "Certificates and device trust" description: "Wendy uses certificates to check that you are connecting to the intended device and that your account belongs to its organization. The CLI manages certificates when you sign in and" --- # Certificates and device trust Wendy uses certificates to check that you are connecting to the intended device and that your account belongs to its organization. The CLI manages certificates when you sign in and enroll; normal use does not require editing certificate files. ## Common tasks - [Sign in to Wendy Cloud](../../advanced/clients/wendy-cli/commands/cloud/login/). - [Enroll a device](../../advanced/clients/wendy-cli/commands/device/enroll/). - [Renew a sign-in certificate](../../advanced/clients/wendy-cli/commands/auth/refresh-certs/). - [Remove a device's enrollment](../../device/unenrollment/). The sections below are an advanced reference for administrators investigating trust or self-hosted deployments. Mutual TLS (mTLS) means both sides authenticate with certificates. Wendy Cloud or your self-hosted certificate service issues them. ## Certificate roles | Certificate | Issued to | Used for | |-------------|-----------|----------| | Device cert | `wendy-agent` during provisioning | mTLS server identity; stored in `/etc/wendy-agent/provisioning.json` | | CLI cert | Developer machine via `wendy auth login` | mTLS client auth when connecting to provisioned devices | The device's mTLS CA pool is built from the `chainPem` field in `provisioning.json`. CLI clients must present a certificate whose chain terminates at that same CA. If `chainPem` is absent or empty, the agent refuses to build a TLS configuration and returns an error indicating that the device may need to be re-provisioned. ### Credential persistence On WendyOS, the agent keeps enrollment state in `/etc/wendy-agent/`: | File | Contents | Permissions | |------|----------|-------------| | `device-key.pem` | Device private key | `0600` | | `provisioning.json` | Enrollment details, device certificate (`certPem`) and CA chain (`chainPem`); new writes exclude the private key | `0600` | Unchanged saves and agent restarts preserve these files when their contents and permissions are already correct. Required updates and permission repairs still write the affected file; the Go agent uses atomic replacement and filesystem syncing. When migrating an older JSON file containing `keyPem`, the agent saves the separate key before removing the embedded copy. The agent no longer emits `device.pem`, `ca.pem` or `.provisioned`. Unprovisioning still removes copies left by older agents. Agent services, including the embedded container registry, receive credentials in memory; these persistence files are internal agent state, not an app credential API. The Swift agent follows the same layout for software-backed keys. With a Secure Enclave key, there is no `device-key.pem`; the wrapped key is held in the Keychain. ## Certificate identity and URI SAN Every CSR generated by the CLI and agent now carries the authoritative Wendy identity as a **URI Subject Alternative Name** in addition to the legacy Subject CommonName: | Entity | URI SAN format | |--------|---------------| | Operator (certificate service identity endpoint) | `spiffe://wendy.sh/tenant/‹uuid›/operator/‹sub›` | | User (CLI, cloud-relayed) | `spiffe://wendy.sh/tenant/‹uuid›/service/user-‹userID›` | | Device / agent (cloud-relayed) | `spiffe://wendy.sh/tenant/‹uuid›/service/asset-‹assetID›` | | Device (ACME / EST, direct) | `spiffe://wendy.sh/tenant/‹uuid›/device/‹deviceID›` | | User (CLI) — legacy chain | `urn:wendy:org:‹orgID›:user:‹userID›` | | Device / agent — legacy chain | `urn:wendy:org:‹orgID›:asset:‹assetID›` | The **tenant SPIFFE principal is the identity**. It is what `IdentityFromCert` (Go) and `OrgIdentity.identity(fromLeaf:)` (Swift) resolve first, because it is the only identity certificate service carries across a renewal: the renew path replaces a CSR's URI SAN list wholesale with the principal read off the presented certificate, so a renewed leaf comes back with its `urn:wendy:org:…` SAN gone. The `urn:wendy:org:…` SAN is read second and only as a legacy old-chain identity. On a transitional leaf that carries both, the principal decides *who* the caller is and the URN contributes only its organisation, so a peer that can still compare nothing but organisations keeps working. The CommonName (`sh/wendy/‹org›/‹asset›` or `wendy/user/‹uid›`) is the last fallback, used only when no URI SAN is present. Two identities are compared in whichever vocabulary they share: tenant UUIDs when both carry one, organisations when both carry one. A pair with no shared vocabulary — a SPIFFE-only caller against an old-chain device, or the reverse — is **not** a match, because no mapping exists between a tenant UUID and an int32 organisation and answering "close enough" is exactly what a cross-tenant caller would need. Legacy tokens that carry no `org_id` claim (user enrollment only) produce a CSR with a CommonName only — no URI SAN — so existing enrollments continue to work without re-authentication. The cloud certificate service validates the URI SAN against the enrollment-token or mTLS identity at issuance time. When the enrollment token carries a `tenant_uuid` claim, a second URI SAN is added *alongside* the urn:wendy SAN: | Entity | Tenant SPIFFE URI SAN format | |--------|------------------------------| | Device / agent | `spiffe://wendy.sh/tenant/‹uuid›/service/asset-‹assetID›` | | User (CLI) | `spiffe://wendy.sh/tenant/‹uuid›/service/user-‹userID›` | Cloud refuses to sign a relay grant unless the CSR carries exactly this SPIFFE principal. The principal kind is always `service`, never `device`: cloud relays every client leaf through certificate service's `service-identity` profile, which would refuse a profile-kind mismatch. That profile is also the one that consults the tenant domain allow-list, so these CSRs stay URI-SAN-only — any dNSName fails the mint. The urn:wendy SAN is retained rather than replaced. It is what the agent's org-equality gate reads out of a peer certificate, and a SPIFFE principal names a tenant UUID rather than the int32 org that gate compares, so dropping the urn would silently disarm org enforcement across the fleet. Organizations with no pki tenant receive no `tenant_uuid` claim and enroll exactly as before — the absence of the claim is never an error. › **Certificate refresh:** the refresh path derives identity from the › already-stored certificate rather than an enrollment token, so it has no › `tenant_uuid` to read and adds no SPIFFE SAN. Only initial enrollment does. ## Server certificate verification The CLI verifies device server certificates on all mTLS connections (BLE, LAN gRPC, and cloud tunnel). Verification includes: 1. **Chain validation** — The device's server certificate is validated against the CA chain from the CLI's auth session. ML-DSA chain certificates are handled specially since Go's `crypto/x509` cannot parse them. 2. **Organization matching** — The server certificate's Wendy org ID is extracted and compared against the CLI's expected org ID. If the device belongs to a different organization than the CLI session, the connection is rejected with an `OrgMismatchError`. For BLE connections, the CLI automatically retries with a matching certificate from another org if one is available. 3. **Exact device identity (`ServerVerifyOpts.ExpectedIdentity`)** — When the caller has pinned a specific asset (see "Device identity pinning" below), the verifier additionally requires the peer's leaf certificate to carry an `asset` Wendy identity whose org and entity id match exactly. Unlike organization matching there is no grace mode here: a certificate with no Wendy identity at all is a mismatch, not a legacy device to tolerate, because the caller asked for a *specific* device and got something that cannot prove it is that device. This check runs inside `tls.Config.VerifyConnection`, not `VerifyPeerCertificate` — a resumed TLS 1.3 handshake skips `VerifyPeerCertificate` but not `VerifyConnection`, so pinning implemented in the wrong hook would silently stop firing on session resumption. 4. **SPKI pinning** — On first mTLS connection to a device — over BLE, LAN gRPC, or the cloud tunnel — its SPKI (Subject Public Key Info) fingerprint is pinned in `~/.wendy/known_devices.json`, keyed by the certificate's Wendy asset identity. A later connection presenting a different key while the *previously pinned* certificate is still within its validity window is hard-refused (a `PinMismatchError` aborts the handshake): a renewal replaces an expiring certificate, it does not race one that is still live, so an unexplained key change during that window is treated as a MITM signal, not a warning. Once the pinned certificate has expired, a new key is ordinary rotation — accepted silently, and the pin is updated to the new fingerprint. ### Device-to-device peer verification (UUID pin) When one device dials another by its device UUID, the agent runs `VerifyPeerDevice` after the chain check. The peer leaf must carry exactly one tenant SPIFFE device principal (`spiffe://wendy.sh/tenant//device/`), its tenant must equal the calling device's own tenant, and its device UUID must equal the roster entry for the dialed name. There is no grace mode: a calling device with no tenant identity of its own refuses every device peer. The dialed address is a routing hint only and is not part of the check. `NewClientTLSConfigExpectingPeer` selects this strict rule whenever the peer identifier is a UUID. A legacy asset-ID string selects the older asset pin: the certificate must identify the expected asset, and its scope must match the calling device's own scope when that scope is known. Only this legacy path permits an unknown calling scope; it still requires the exact asset identity and a valid certificate chain. Wendy Net verifies the exact enrolled device identity when connecting to a peer. Devices must belong to the same organization. Operators in that organization can also verify a target device when delivering builds. ## Device identity pinning (default device) On the first successful connection to a hostname, the CLI records that hostname's identity in `~/.wendy/config.json` under `devicePins`: the **organisation**, the **cloud host** that issued its certificate, and the **asset id** from the device certificate's identity SAN (the name of its tenant SPIFFE principal, or the legacy `urn:wendy:org::asset:` URN on an old chain), and that **principal** when it has one. Every later connection to that hostname is checked against the pin — this is what feeds `ServerVerifyOpts.ExpectedIdentity` above, so a wrong device is rejected during the TLS handshake itself, not after. The pin is deliberately not a certificate fingerprint — a device legitimately rotates and re-enrolls certificates, and that must not look like an attack. What trips it is a change of *who* is answering: | Observed | Result | | --- | --- | | Same org + cloud + asset (renewed or re-enrolled cert) | Connects normally | | Different org or cloud host | Refused, no prompt | | Same org + cloud, **different asset id** | Refused, no prompt — the hostname now resolves to a different machine, or the device was wiped and re-enrolled as a new asset | | **No mTLS identity at all**, on a hostname that was pinned | Refused, no prompt — the device may have been unenrolled, reflashed, or reset, or another machine may have taken its name | | No mTLS identity, hostname never pinned | Connects normally (ordinary out-of-the-box device) | The asset id is read only from a certificate that passed chain and org verification, so an impostor cannot assert its way past the pin. On the dial ladder, a wrong-device rejection aborts the whole ladder immediately — no further certificate or port is tried — and a hostname that carries any pin at all is never offered the unauthenticated plaintext rung, regardless of what its (attacker-controlled) mDNS TXT records claim about it. **Every refusal above reads identically** whether the CLI is running interactively, under `--json`, or non-interactively — there is deliberately no "trust this identity anyway?" prompt. A MITM warning that can be dismissed gets dismissed, and the person who could actually distinguish a legitimate replacement from an attack is rarely the one staring at a prompt mid-command. The one way past a refusal is a separate, deliberate act: ```sh wendy device unpin wendy device unpin spiffe://wendy.sh/tenant//device/ wendy device unpin urn:wendy:org::asset: ``` This clears the local pin only — it never dials the device, so it works even when the device is offline, wiped, or gone. The next successful connection to that hostname records a fresh pin from scratch. Naming a device explicitly with `wendy device set-default ` has the same clearing effect, since typing the hostname is itself the user asserting "I mean that device." Both forms are accepted because the two stores are keyed differently. The identity-change refusals above name a hostname, and the hostname form clears it (a pin records its device's principal on the first connect after the SPIFFE cutover, which is how the hostname form still reaches the SPKI entry). The **SPKI** refusal (point 4 above) can only name the certificate identity, because that is what `known_devices.json` is keyed by — the tenant SPIFFE principal for a Wendy-issued device, the legacy URN for an old chain and there is often no hostname to offer: `wendy device list` and the device picker dial the device's IP, and an agent that never advertises `orgid` in its mDNS records leaves nothing locally that maps a name to an asset. Copy the identity out of the refusal and pass it back — it clears the SPKI entry and any `devicePins` entry naming the same asset. Unpinning by hostname also clears pins filed under the device's *other* names (the cloud roster's asset name, its wendy net name), because one device is legitimately pinned under several — but only when those pins name the same organisation and asset. Those alternate names come from mDNS, which is unauthenticated, so a pin naming a *different* asset is a different device's pin and is left alone. Unpinning a local VM by its alias, `wendy device unpin vm:`, clears only that VM's pin, never the `127.0.0.1` pin every other loopback address shares. Whatever is cleared is printed, one line per entry, so an unpin never removes trust state silently. ### Re-enrolling after an intentional reset or organization move Unenrolling a device does not clear the identity saved by this CLI. If you intentionally unenrolled, reset, or reflashed it, or know it is moving to another organization, clear the old local pin before retrying enrollment: ```sh wendy device unpin wendyos-ccr1.local wendy device enroll --device wendyos-ccr1.local ``` Use the hostname printed in the refusal. Unpinning does not enroll the device or delete its cloud asset. If unenrollment reported a cloud cleanup failure, finish that cleanup in the Wendy Cloud dashboard separately. If the identity change was unexpected, keep the pin and verify the device first; check `wendy auth login` if this CLI is missing the organization's credentials. ## Pin sources and precedence A pin's `source` field records how the CLI learned it: | Source | Written by | Trust basis | | --- | --- | --- | | `lan` | An ordinary connection to the device (the default) | Only a certificate presented on the local network | | `cloud` | The org's asset roster, fetched over an authenticated cloud session (e.g. `wendy cloud discover` seeds a pin for every named asset the roster returns) | The org's cloud vouching for the (name, org, asset) binding | Pins written before sources were tracked carry an empty `source` and are treated as `lan`. A `cloud` write is authoritative and overwrites whatever was pinned for that name, `lan`-sourced or not — that is how cloud-known devices get their trust-on-first-use window closed before the CLI ever meets them on the network (see below). The reverse never holds: a LAN sighting cannot upgrade or override a `cloud` pin, and specifically: - A `lan`-sourced pin that predates asset ids (no asset recorded) is backfilled with the observed asset id on the first connection whose org and cloud still match — org and cloud already vouch for the connection, so this is a one-time upgrade, not a challenge. A `cloud`-sourced pin with no asset id is **not** backfilled this way: the cloud already spoke for this name, and a LAN sighting is not evidence to the contrary, so it is treated as a plain match instead. - An asset-less `cloud` pin is never displaced by a `lan` pin in a way that would erase an existing exact-identity constraint on that name — cloud authority decides *which* binding to believe, and applying it is worthless if the result is a name constrained to no asset at all. Agents whose certificates carry no asset identity at all are never treated as swapped devices: an empty *observed* asset id can never produce a mismatch on its own, regardless of source. ## Multi-key pin lookup One device can answer to more than one name, and different surfaces record its pin under different ones: an ordinary connection records under the mDNS hostname actually dialed, cloud seeding records under the asset's roster name, and a wendy net dial names the device by its wendy net name. Resolving "does this dial have a pin" therefore checks, in order: 1. The name the caller dialed (`--device` value, saved default, or the device picker's name). 2. The wendy net name from the discovery-cache entry whose hostname matches (1), if the cache has one. 3. That entry's display name. Consulting an alias can only ever *find* a pin, never discard one: an attacker-chosen alias can at most impose a stricter constraint that the real device still satisfies, never a way to bypass one. The trust decision itself always rests on the certificate, never on which name resolved the lookup. ## Cloud pin seeding A hostname the CLI has never connected to has no pin, so its first connection is trust-on-first-use — the one moment this scheme cannot, by construction, distinguish the real device from an impostor answering first. Seeding pins from the cloud closes that window for every device the org's cloud already knows about: whenever the CLI fetches the org's asset roster over an authenticated session, each named asset is recorded as a `cloud`-sourced pin (org, asset id) before the CLI has ever reached it on the network. An impostor answering an mDNS query for that name is then rejected on the very first LAN connection attempt, not just the second. ## CA key rollover The trust bundle may contain multiple CA certificates sharing the same subject DN. This is normal during a CA key rollover, where an old CA and a new CA temporarily coexist in the bundle. The agent's ML-DSA client certificate verifier (`verifyMLDSAClientCert`) tries every CA whose subject DN matches the client certificate's issuer DN. Verification succeeds as soon as any matching CA validates the certificate. If all matching CAs fail, the error from the last attempted CA is returned. If no CA in the pool has a matching subject DN, the verifier returns a "client certificate issuer not found in trusted CA pool" error. ## Clock skew and the NotBefore floor A device that reboots without network connectivity (e.g. a power cycle with no WiFi) may not have synchronised its clock via NTP before the mTLS server starts. With an unsynchronised clock that predates the certificates, every incoming client certificate would be rejected as "not yet valid", silently making the mTLS port unusable. To handle this, `wendy-agent` reads the `NotBefore` timestamp from the device's own provisioning certificate at startup and passes it to the verifier as a **time floor**. During peer-certificate verification the effective time is: ``` effectiveNow = max(time.Now(), provisioningCert.NotBefore) # and, only when the real clock is still behind that floor, advanced far # enough to admit a cert issued moments after provisioning — but never # further than 24h past the floor: if time.Now() < provisioningCert.NotBefore: effectiveNow = max(effectiveNow, min(peerCert.NotBefore, provisioningCert.NotBefore + 24h)) ``` The 24h cap is what keeps the floor from becoming a blank cheque: on a device whose clock never synced, a peer certificate dated further ahead than that is still rejected. `effectiveNow` is used **only for the leaf certificate's `NotBefore` check** — on both the standard (RSA/ECDSA) path, via `x509.VerifyOptions.CurrentTime`, and the ML-DSA path. Certificate expiry (`NotAfter`) and CA-certificate validity are always checked against the real system clock, so the floor cannot mask a genuinely expired certificate or make an immature CA appear valid. Pass a zero `time.Time` to disable the floor. When the device clock is behind the floor at startup, `wendy-agent` logs a `WARN` that includes the device clock, the floor, and how far behind the clock is. If the provisioning certificate cannot be parsed, the floor is zero (disabled) and a `WARN` notes that NTP clock-skew protection is off. Once NTP synchronises the clock the discrepancy disappears on its own. The systemd unit also orders the agent after `time-sync.target` (`After=`/`Wants=`) so NTP synchronisation is attempted as early as possible; the floor is the primary guard and does not depend on that ordering. For additional clock resilience, the agent uses **Roughtime** — a cryptographically-signed time protocol — to obtain a trusted timestamp from multiple public servers. Roughtime sync runs on startup and periodically thereafter, advancing the system clock when the verified time is ahead of the local clock. When connecting to a device, the CLI automatically checks for clock skew. If the device clock lags by more than 2 minutes, the CLI fetches a signed Roughtime proof and relays it to the device, which verifies the signature and advances its clock. The CLI's own clock is never sent as authoritative time — only used to decide whether to relay. The CLI can also broadcast Roughtime time to nearby devices via `wendy device sync-time`. ## Self-hosted certificate service Administrators can run the [Wendy certificate service](https://github.com/wendylabsinc/pki-core) locally for development. This requires separate service setup; hosted Wendy Cloud users can use the CLI flows above. ### Prerequisites 1. **Start the engine and admin API:** ```sh pkicore serve all --dev ``` 2. **Create a CA and configure the Wendy frontend** (`frontend.wendy.device_ca_id` in `config.yaml`). 3. **Start the Wendy gRPC frontend:** ```sh pkicore serve wendy --config config.yaml ``` This exposes `wendycloud.v1.CertificateService` on the configured listen address (default `:50051`). ### Authenticate the CLI Do this first: `wendy device enroll` mints its enrollment token from your stored auth session, so the CLI has to be logged in to the same certificate service before it can enroll anything. `--api-key` selects the local certificate service flow. ```sh wendy auth login \ --api-key \ --cloud-grpc :50051 ``` The same step issues the CLI its own client certificate, which is what lets `wendy device version`, `wendy run`, and the other device commands connect over mTLS afterwards. ### Enroll a device Find your machine's LAN IP (the address the device can reach): ```sh ifconfig | grep "inet " | grep -v 127.0.0.1 ``` Then enroll the target device: ```sh wendy device enroll \ --cloud-grpc :50051 \ --name my-device ``` › **Plaintext dial (local development only):** the agent's enrollment dial is TLS › by default for every address, and a cloud host given without a port resolves › to `:443` (it used to resolve to the plaintext `:50051`, which is what sent › enrollment tokens in cleartext). A local certificate service serving › plaintext gRPC therefore needs two things: its port named explicitly, as › above, and `WENDY_CLOUD_INSECURE=1` set **in the agent's environment on the › device** — the agent performs this dial, so setting the variable in your own › shell has no effect. The agent logs a warning naming the target address › whenever the variable is active, because the enrollment token is a bearer › credential. Never set it on a real device. › › The CLI's own cloud dials follow the same rule: TLS for every address, so › `wendy` commands that talk to a local plaintext cloud need › `WENDY_CLOUD_INSECURE=1` in **your** shell as well. A local or on-prem tunnel › broker is always verified by hostname: its certificate must chain to the › Wendy CA and carry a DNS or IP SAN for the address the CLI dials (for › example `localhost` or the LAN IP). After this, `wendy device version`, `wendy run`, and other device commands automatically use the mTLS port (plaintext port + 1) when the device's Avahi advertisement includes `tls=true`. > **Note:** The end-to-end test helper `go run ./cmd/local-pki-test` passes > `nil` for identity URIs, so the CSR it generates has no URI SAN. This is > intentional — the tool is for CA wiring tests only, not for producing > production-equivalent certificates. ## Avahi advertisement After provisioning, `wendy-agent` updates `/etc/avahi/services/wendyos-mdns.service` to set the `_wendyos._udp` service block's port to the mTLS port and adds a `tls=true` TXT record. The CLI reads this record during device discovery to select the mTLS connection path. ## Artifact signing key (`DefaultVerifier`) Separate from the mTLS PKI, the agent embeds an **ML-DSA65 public key** (`sigverify.DefaultVerifier`) used solely to verify the authenticity of agent update binaries delivered via `UpdateAgent`. This key is a build-time constant and is distinct from provisioning certificates. A companion **per-org publisher key** (currently a disabled placeholder, `sigverify.Disabled()`) is used to verify container images in `RunContainer`. This key is sourced from provisioning/PKI at runtime and is specific to each organisation's image artifacts. Both keys are currently empty stubs; verification becomes active once the cross-repo signing pipeline is deployed and real keys are embedded. --- # Release Process Source: https://docs.wendy.dev/latest/advanced/RELEASES/ --- title: "Release Process" description: "This project uses a dual-track release system: - Pre-releases: Automatically created on every push to main with timestamp-based tags (e.g., 2025.12.04-195108) - Semver releases: Ma" --- # Release Process This project uses a dual-track release system: - **Pre-releases**: Automatically created on every push to `main` with timestamp-based tags (e.g., `2025.12.04-195108`) - **Semver releases**: Manually promoted from pre-releases using the workflow dispatch UI ## Creating a Semver Release ### Quick Start 1. Go to **Actions** → **Create Semver Release** → **Run workflow** 2. Fill in the form: - **version**: Your semver version (e.g., `0.2.0`, `1.0.0-beta.1`) - **promote_prerelease**: The timestamp tag to promote (e.g., `2025.12.04-195108`) 3. Click **Run workflow** ### Finding Pre-release Tags **Via GitHub UI:** - Go to [Releases](https://github.com/wendylabsinc/wendyos/releases) - Look for pre-release entries with timestamp tags **Via CLI:** ```bash gh release list --limit 10 ``` Example output: ``` v0.1.0 Latest v0.0.1 2025-12-05T00:05:07Z 2025.12.04-195108 Pre-release 2025.12.04-195108 2025-12-04T20:08:54Z ← Use this tag 2025.12.04-191348 Pre-release 2025.12.04-191348 2025-12-04T19:33:45Z ``` ### Examples **Promoting a pre-release to a stable release:** - version: `0.2.0` - promote_prerelease: `2025.12.04-195108` **Creating a beta release:** - version: `1.0.0-beta.1` - promote_prerelease: `2025.12.04-195108` **Creating a release candidate:** - version: `1.0.0-rc.1` - promote_prerelease: `2025.12.04-195108` ## Semver Guidelines Follow [Semantic Versioning 2.0.0](https://semver.org/): - **MAJOR.MINOR.PATCH** (e.g., `1.2.3`) - **MAJOR**: Incompatible API changes - **MINOR**: Backward-compatible new features - **PATCH**: Backward-compatible bug fixes - **Pre-release versions** (e.g., `1.0.0-beta.1`, `2.0.0-rc.2`) - Append `-.` to the version - Common identifiers: `alpha`, `beta`, `rc` (release candidate) ## Workflow Details The semver release workflow: 1. ✅ Validates the semver format 2. 📦 Downloads all build artifacts from the specified pre-release 3. 🔄 Renames assets to use the semver tag (critical for download scripts!) 4. 🚀 Creates a new GitHub release with the semver tag 5. 🏷️ Marks it as "latest" (non-pre-release) 6. 📝 Includes the original release notes + promotion note ### Asset Naming **Pre-release assets** (timestamp-based): ``` wendy-agent-linux-arm64-2025.12.04-195108.tar.gz wendy-cli-darwin-arm64-2025.12.04-195108.tar.gz ``` **Semver release assets** (renamed during promotion): ``` wendy-agent-linux-arm64-v0.2.0.tar.gz wendy-cli-darwin-arm64-v0.2.0.tar.gz ``` **Download URL pattern:** ``` https://github.com/wendylabsinc/wendy-agent/releases/download/v0.2.0/wendy-agent-linux-arm64-v0.2.0.tar.gz ``` All platform builds are included: - `wendy-agent-linux-arm64-${VERSION}.tar.gz` - `wendy-agent-linux-amd64-${VERSION}.tar.gz` - `wendy-agent-macos-arm64-${VERSION}.zip` (Developer ID signed and notarized) - `wendy-cli-linux-arm64-${VERSION}.tar.gz` - `wendy-cli-linux-amd64-${VERSION}.tar.gz` - `wendy-cli-darwin-arm64-${VERSION}.tar.gz` - `wendy-cli-windows-amd64-${VERSION}.zip` - `wendy-cli-windows-arm64-${VERSION}.zip` ## Release assets Each release also includes SPDX Software Bill of Materials files (`*.spdx.json`) for each shipped binary, Swift packages, and the whole-repo source tree, along with Sigstore-backed SLSA build provenance and SBOM attestations. See [Verifying release artifacts](../../security/verification/) for how to inspect and verify these files. ## Automation - **Pre-releases**: Fully automated on every `main` push - **Homebrew nightly formula**: Auto-updated (direct push) for every pre-release - **Homebrew nightly cask**: Auto-updated (direct push) for every pre-release - **Homebrew stable formula**: Auto-updated via PR for semver releases - **Homebrew stable cask**: Auto-updated via PR for semver releases - **Winget**: Auto-updated for semver releases - **Semver releases**: Manual promotion via workflow dispatch - **Latest tag**: Auto-updated to point to the most recent pre-release ## FAQ **Q: Can I skip the pre-release step?** A: The workflow is designed to promote existing pre-releases. This ensures the binaries are tested before becoming official releases. **Q: What if I need to rebuild for a semver release?** A: Push to `main` to create a new pre-release, then promote it using the workflow. **Q: Can I delete old pre-releases?** A: Yes, but keep at least the last few in case you need to reference or promote them. **Q: How do I update Homebrew for a stable release?** A: You don't need to update it manually. The release workflow opens a PR in [homebrew-tap](https://github.com/wendylabsinc/homebrew-tap) automatically for the stable CLI formula and macOS agent cask. Review that PR, wait for the tap checks to pass, and merge it. --- # Roadmap Status Source: https://docs.wendy.dev/latest/advanced/roadmap/ --- title: "Roadmap Status" description: "Status reviewed on 19 October 2026. Available products and unfinished integration work have separate entries below. Planned work has no committed release date unless one is explici" --- # Roadmap Status Status reviewed on 19 October 2026. Available products and unfinished integration work have separate entries below. Planned work has no committed release date unless one is explicitly stated. See [The Wendy ecosystem](../../concepts/ecosystem/) to choose a product and find its setup guide. ## In Progress - **MLX Swift support**: MLX Swift integration for unified-memory AI workloads. The containerised MLX LLM demo has landed (see Shipped); the remaining work is first-class MLX Swift integration in the Wendy ecosystem. - **Companion SDK and Cloud v2 integration**: reusable SDK work and mobile support for Cloud v2 continue separately from the released iOS app. Check cameras, app controls, logs, and notifications in the intended mobile build before migrating an existing deployment. See [Moving to Cloud v2](../../cloud-migration/). - **Data, Cloud, and BI integration**: device capture exists; storage, querying, and analytics workflows need their own configuration and validation. See [Wendy Data](../../data/) for supported capture and current limits. ## Available Beta - **Wendy Cloud v2**: remote device access and fleet management. The current CLI signs into v2 by default; v1 remains available with `--legacy`. App-port and UDP tunnels on v2 need updated Cloud, CLI, and Agent versions. Start with the [Cloud guide](../../cloud/). - **Headless Mac** — _beta_: deploy native macOS apps to Apple Silicon Mac targets with [Headless Mac](../../installation/wendy-agent-macos/). Apple Silicon only. Since the initial beta it has gained remote agent self-update, a LAN registry served over mTLS, app supervision and restart parity across agent restarts, and native gamepad entitlements. Beta limitations are documented in the macOS install guide. ## Available Preview - **Jetson AGX Thor** — _preview_: WendyOS support for AGX Thor-class physical AI workloads, including the dedicated USB-recovery flash path that writes QSPI and internal NVMe directly. Install, deploy, GPU, and camera all work; see the [AGX Thor install guide](../../installation/wendyos-nvidia-jetson-agx-thor/). - **Wendy Lite** — _preview_: ESP32 runtime and deployment layer for C5, C6, C61, P4, and S3 targets. Regular native ESP-IDF projects are the recommended app model and deploy with `wendy run`; Swift and other WASM guests remain available as an optional portable runtime. See [ESP32 installation](../../installation/wendy-lite-esp32/) and the [Wendy Lite reference](../../advanced/wendy-lite/). ## Shipped - **Wendy Companion for iOS**: [available in the App Store](https://apps.apple.com/us/app/wendy-companion/id6761771159). Its release does not imply that every v1 workflow has a v2 equivalent. - **Wendy BI 1.0**: analytics and dashboards are available through [Wendy BI](https://wendy.dev/products/wendy-bi), with separate access and data-connection setup. - **TeleCollect**: capture and export pose demonstrations on supported Apple devices. See the [product introduction](https://wendy.dev/blog/telecollect-pose-data-collection). - **Arduino Uno Q OS image**: included in [WendyOS 0.19.5](https://github.com/wendylabsinc/WendyOS-Builder/releases/tag/0.19.5). Image availability and hardware CI coverage are separate milestones. - **Jetson AGX Orin support**: stable — install, deploy, GPU, and camera. - **Jetson Orin Nano support**: stable. - **Raspberry Pi 3, 4, and 5 support** (8GB Pi 5 recommended). - **Windows support** for the Wendy CLI and VS Code deployment flow. - **Zero-trust PKI, deployed to GCP** ([WDY-1226](https://linear.app/wendylabsinc/issue/WDY-1226)): the public-key infrastructure backing device-to-cloud auth is deployed, with CI auto-deploy from main. Every cloud connection is mutually authenticated. See [PKI](../../advanced/pki/). - **Stagefile build descriptors**: `build.stagefile.yaml` is now a first-class build input alongside Dockerfiles and compose files, with parallel compilation, CUDA stages resolved from the device's GPU architecture, pinned source installs, and cache scoping. The bundled example apps have been converted to it. - **ROS 2 support**: standard upstream ROS 2 in stock containers with a selectable middleware (CycloneDDS, Fast DDS, RTI Connext, GurumDDS), plus `wendy device ros2` for remote nodes, topics, services, parameters, actions, lifecycle nodes, and rosbags. See [Robotics](../../integrations/) and the [Foxglove integration](../../integrations/foxglove/). - **Swift on the device**: Swift app templates, tutorials, cross-compilation, and remote Swift debugging, plus native Swift apps on macOS targets. - **Unitree G1 support**: agent install and reflash guides for the G1. - **Network (IP) cameras**: a third camera transport alongside USB and CSI, with camera discovery, a credential store, and a picker. - **Python debugging** from the VS Code extension. - **Local OS updates** without requiring a full system flash. - **MLX LLM in a local container** ([WDY-1229](https://linear.app/wendylabsinc/issue/WDY-1229)): a working containerised MLX LLM demo, backed by an upstream MLX segfault fix. The demo container is not size-optimised — it ships Ubuntu 24, the CUDA toolkit, and the Swift toolchain, because MLX compiles CUDA just-in-time. ## Planned - **RISC-V support**. - **3D geospatial digital twin**: planned; no current release date is committed here. - **Logging and telemetry demo** — _planned, not yet started_ ([WDY-1231](https://linear.app/wendylabsinc/issue/WDY-1231)): a demo showcasing remote hardware debugging through logging and telemetry. It was originally sequenced against the PKI release, which has since shipped; the demo is still in the backlog. - **Wendy Swift initiative** — _separate release track_: the Swift toolchain, templates, and debugging support have shipped (see Shipped). The remaining work is the coordinated launch, which is deliberately kept on its own track so it is not diluted by adjacent releases. The original sequencing behind the Pipecat Agent template refresh no longer applies — that work shipped. --- # Vscode Source: https://docs.wendy.dev/latest/advanced/vscode/ --- title: "Vscode" description: "WendyOS developer documentation." --- --- # Wendy Data Platform demo runbook Source: https://docs.wendy.dev/latest/advanced/wendy-data-platform-demo/ --- title: "Wendy Data Platform demo runbook" description: "How to reproduce the end-to-end Wendy Data Platform demo on a real device from a clean start: an NVIDIA Jetson running a WendyOS agent built from the data platform branch, the refe" --- # Wendy Data Platform demo runbook How to reproduce the end-to-end Wendy Data Platform demo on a real device from a clean start: an NVIDIA Jetson running a WendyOS agent built from the data platform branch, the reference model app (`Examples/WendyDataModelApp`), a flight-recorder campaign, and episode uploads into a deployed cloud ingest service. The path this runbook walks has been executed on real hardware. An episode fires on the app's edge-triggered `person_detected` event, or on demand with `data campaign trigger`, carries the app's YOLOv8n prediction records in `events.jsonl` (including the pre-trigger buffer), and reaches the ingest catalog with upload state `uploaded`, which only flips after `CommitEpisode` verifies every file hash server-side. For agent-managed inference without deploying a model application, use `Examples/WendyDataPeople/campaign.yaml`. Its `inference` block installs a pinned Hugging Face object-detection model and applies it to `camera: "*"`; campaign inspect reports startup and per-camera state. The steps below retain the separate model-app path used for this hardware demo. See the [campaign YAML reference](clients/wendy-cli/commands/data.md#inference) for the managed runtime requirements, event behavior, and notification settings. Placeholders used throughout, to be replaced with your own values: | Placeholder | What it is | |---|---| | `` | The device's mDNS name, for example `wendyos-.local` | | `` | The organization the device is enrolled in | | `` | The device's asset identifier in that organization | | `` | The Google Cloud project hosting the ingest service | | `` | The ingest service URL the device should upload to | | `` | An episode identifier, as printed by `wendy data episodes` | ## 0. Record the device's starting state Before changing anything, write down what you are about to change, so you can put it back. The demo touches the agent binary, one environment file, and the auto-updater timer; it does not touch enrollment. | Fact | Why it matters | |---|---| | Hostname | Every `wendy` command below takes `--device ` | | Hardware and WendyOS version | Determines which agent build to install | | Enrollment (organization and asset) | Stays as-is; the demo redirects uploads only | | Camera device nodes | The app and the campaign compete for these, see step 5 | | Agent version before the demo | The rollback target | | Free space on `/` and on the data partition | Episodes are large; see step 4 | ## 1. Prerequisites on the workstation - Go toolchain. On macOS, cgo builds need `CC=/usr/bin/clang`; if `clang` resolves to a Swift toolchain shim, cgo fails with a silent "exit status 2". - Docker for the app image build. If a pull hangs on macOS, the Docker Desktop credential store is the usual cause: point `DOCKER_CONFIG` at a scratch directory containing a `config.json` of `{}`. - The Google Cloud command-line interface (`gcloud`) if you need to look up the ingest URL. If it crashes under an old system Python, set `CLOUDSDK_PYTHON` to a newer interpreter. ## 2. Recon (read-only) ```sh wendy device list --timeout 8s # no --scan flag; --timeout scans once wendy device info --device ssh root@ 'df -h /; ls /dev/video*' ``` Check `df` before anything else. A device whose root filesystem is full will fail the agent install in confusing ways. The failure mode seen in practice: the daily agent updater accumulates one backup of the agent binary per run in `/opt/wendy/bin` (`wendy-agent.backup.*`), fills the root partition, and then a failing update leaves `/usr/local/bin/wendy-agent` truncated and the agent dead. Recovery: ```sh ssh root@ ' rm -f /opt/wendy/bin/wendy-agent.backup.* cp /opt/wendy/bin/wendy-agent.latest /usr/local/bin/wendy-agent systemctl restart wendyos-agent' ``` ## 3. Build the agent and command-line interface (CLI) ```sh cd /go # Agent: pure Go, cross-compiles from macOS directly (CGO_ENABLED=0). make build-agent-linux-arm64 VERSION=dev-data-platform-demo # CLI: needs cgo (gousb, Bluetooth Low Energy); the darwin Makefile target's # CGO_ENABLED=0 does not link, so build it directly: CC=/usr/bin/clang CGO_ENABLED=1 go build \ -ldflags "-s -w -X github.com/wendylabsinc/wendy/go/internal/shared/version.Version=dev-data-platform" \ -o bin/wendy-demo ./cmd/wendy ./bin/wendy-demo data --help # confirm the data verbs exist in this build ``` ## 4. Install the agent on the device Stage a rollback first, on the device, so recovery does not depend on the workstation: ```sh ssh root@ ' mkdir -p /data/demo-rollback cp /usr/local/bin/wendy-agent /data/demo-rollback/wendy-agent.pre-demo' ``` Then push with the sanctioned update path and verify: ```sh ./bin/wendy-demo device update --binary bin/wendy-agent-linux-arm64 --device ./bin/wendy-demo device info --device # "version" shows the new build ./bin/wendy-demo data sources --device # v2 DataService answers; camera healthy ``` The nightly auto-updater would overwrite this agent shortly after midnight, so disable it for the demo window and re-enable it afterwards: ```sh ssh root@ 'systemctl disable --now wendyos-agent-updater.timer' # undo after the demo: ssh root@ 'systemctl enable --now wendyos-agent-updater.timer' ``` ## 5. Point the data plane at the ingest service Enrollment stays untouched; only the episode transfer worker is redirected. The agent's systemd unit reads `/etc/default/wendy-agent` (`EnvironmentFile=`), so: ```sh INGEST_URL=ingest.data.wendy.sh ssh root@ "cat >> /etc/default/wendy-agent </Examples/WendyDataModelApp /go/bin/wendy-demo run --device --detach --yes /go/bin/wendy-demo data campaign deploy campaign.yaml --device /go/bin/wendy-demo data campaign trigger model-harness-demo --device ``` ### One camera, two consumers `campaign.yaml` declares a `camera: front` source and the app consumes the same camera, on a single-camera device, at the same time. That works because the app never opens the physical device: it reads the agent-fed v4l2loopback node that the `camera` entitlement grants, while the campaign's capture adapter reads the same producer. Video4Linux2 admits one holder of a capture device, and the agent is it. Earlier revisions of this demo needed a `campaign-telemetry-only.yaml` variant because the app opened `/dev/video0` itself and the capture adapter then failed with `VIDIOC_S_FMT: device busy`. That variant is gone; `campaign.yaml` is the only campaign the example needs. Beyond removing the conflict, the subscription is what makes an episode usable as training data: each sample the app receives carries a `sample_id`, the episode records the same identifier for the bytes it kept, and each prediction names the samples it was computed from. See "What to expect in the episode". ### Verify, in order ```sh ./bin/wendy-demo data episodes --device # newest episode "complete" ./bin/wendy-demo data inspect --device # trigger.reason, files [events.jsonl, telemetry.jsonl], upload.state "uploaded" ``` `upload.state: "uploaded"` is the cloud-side proof: the worker only records it after `CommitEpisode` succeeds, and the server verifies every stored object's SHA-256 against the manifest before flipping the catalog row to complete. Read-side inspection of the catalog (`QueryEpisodes` and `GetEpisode`) requires a user bearer token rather than the asset identity, so check the rows directly in the deployment's ClickHouse (`episodes` and `episode_files` tables) or through the business intelligence surface once it is wired. ## What to expect in the episode `person_detected` is the only trigger. The app emits it edge-triggered when a person enters frame, once per appearance rather than once per frame, so walking through produces one episode rather than fifty; with nobody in view, seal one on demand with `data campaign trigger`. Each trigger seals one episode, 10 seconds of pre-trigger buffer and 10 seconds after, which uploads within seconds. The app holds itself to a ceiling of 5 predictions per second, but what it achieves is set by inference, not by the ceiling: measured nearer three per second sustained, so a 20-second episode carries on the order of sixty prediction records. Quote the ceiling as a ceiling. Earlier revisions of this runbook told you to expect an episode to fire by itself on a `model.uncertainty > 0.65` trigger. That trigger is gone from `campaign.yaml`, and the advice was wrong rather than merely stale: the app scores uncertainty as 1 minus its best detection confidence, so a scene the detector has no class for sits at exactly 1.0 and the threshold fires on every prediction. Measured on the device, that produced a fresh episode every 30 seconds with nobody in frame. Collecting on uncertainty is still the right idea; it needs a real uncertainty signal behind it, entropy across classes or the margin between the top two, rather than one minus a confidence. To pair those outcomes with the frames that produced them: - `model_inputs.jsonl` lists every sample the harness handed to the model, as `{source_id, sample_id, episode_nanos, payload_bytes, dropped_before}`. - `cameras//index.jsonl` carries the same `sample_id` on each frame the episode kept, together with the segment file and byte offset holding it. - each `prediction` record in `events.jsonl` carries an `inputs` list of `{source_id, sample_id}`. Join on `(source_id, sample_id)` and every prediction resolves to the exact bytes the model saw. The manifest's `model_io` block states this contract, counts the predictions that named their inputs, and — per source — whether the episode retains payloads for all of the consumed samples or only the subset the capture policy kept. To watch the camera capture rather than join against it, open the `cameras//playable.mp4` the episode already carries: the agent remuxes each camera source into a browser-playable MP4 while sealing, lists it in the manifest as a derived file, and uploads it with everything else. Only an episode sealed by an older agent (or a source whose seal-time remux was refused, see the manifest's `playable_notes`) still needs the laptop-side `episode-playable` conversion; the raw capture is an elementary stream with no container, so players otherwise invent a frame rate and the clip runs at the wrong speed. See "Playing back camera capture" in the `wendy data` command documentation. ## Pitfalls discovered on the way (all hit for real) 1. Full root disk from accumulated updater backups: see the recon step. 2. `Dockerfile` export stage: ultralytics pulls the graphical `opencv-python` wheel, which needs `libxcb1 libgl1 libglib2.0-0`, and torch 2.13 and newer needs `onnxscript` for Open Neural Network Exchange (ONNX) export. Both are handled in the example's Dockerfile. 3. App data socket refused with "peer is not in a wendy app scope": on a device whose containerd uses the cgroupfs driver the `system.slice:edge-agent:` cgroup path is literal, which the attribution parser must recognize. Without that, no record ever reaches the agent. 4. After any agent restart the app's data socket listener is gone and the app gets ECONNREFUSED until the app is redeployed (`device apps remove --force`, then `wendy run`). Cause: containerd's metadata store drops labels with empty values, so type-only entitlements (camera, episode-write) lost their `sh.wendy/entitlement.*` labels and `RestoreAppSystemAPISockets` skipped the container. Fixed: `wendyLabels` now writes a `present=true` marker for type-only entitlements. Containers created by an older agent still carry the broken labels, so after updating the agent redeploy the app once. 5. An episode that exhausts its 5 upload attempts is terminally `failed` and is never retried; trigger a fresh episode after fixing connectivity. 6. `wendy device apps start` streams logs and does not return with `--detach` semantics; script around it or use `wendy run`. ## Rollback ```sh ssh root@ ' cp /data/demo-rollback/wendy-agent.pre-demo /usr/local/bin/wendy-agent chmod 755 /usr/local/bin/wendy-agent sed -i "/WENDY_DATA_INGEST_URL/d;/WENDY_DATA_DIR/d" /etc/default/wendy-agent systemctl restart wendyos-agent systemctl enable --now wendyos-agent-updater.timer' ``` Enrollment is untouched by the demo, so there is nothing to restore there. ## Open gaps - Catalog read verification from the CLI needs a user bearer token; today the proof is the commit-gated `uploaded` state plus a direct ClickHouse read. - The graphics processing unit (GPU) variant of the app, using `onnxruntime-gpu`, has not been exercised; the demo runs the CPU path. - Model apps read camera data through the entitlement-mounted V4L2 loopback nodes. There is no per-app sensor listener in this implementation. Application recording writes use the app data socket, which checks Linux peer credentials and cgroup app identity before accepting records. Camera reads are controlled by the container's device mounts and camera entitlement. - ROS 2 sources report `healthy: true` unconditionally (`data_ros2_adapter.go`, `ros2DataSource`): nothing probes the Data Distribution Service (DDS) graph, so a dead domain enumerates as healthy. Camera and audio sources derive their health; ROS 2 does not yet. - ROS 2 capture treats "rosbag2 has not exited within 750 ms" as "rosbag2 is recording" (`data_ros2_adapter.go`, `startOne`). Nothing confirms the bag directory was created or that a topic was subscribed, so a recorder that fails just after that window reports a successful start and surfaces the failure only at Stop. - The audio capture now backdates a segment stamp past the chunk of pulse-code modulation (PCM) data it read and past a constant for the delay still outstanding behind it, and publishes a 20 ms uncertainty half-width to cover that constant (`mapping_segment` reads `receipt-minus-pipeline-v1`). The constant is an estimate, not a reading: nothing queries the actual ALSA or PipeWire delay per chunk, so a device whose buffering departs markedly from the Jetson Orin Nano the constants were measured on could fall outside the published bound. - `wendy data download` mutates its own `--output` flag variable when the flag is empty, so re-running the same command object in one process writes to the first episode's directory. --- # Wendy Lite Source: https://docs.wendy.dev/latest/advanced/wendy-lite/ --- title: "Wendy Lite" description: "Wendy Lite is Wendy's runtime and deployment layer for ESP32 microcontrollers. It supports native ESP-IDF applications as the recommended project model and can also run portable WA" --- # Wendy Lite Wendy Lite is Wendy's runtime and deployment layer for ESP32 microcontrollers. It supports native ESP-IDF applications as the recommended project model and can also run portable WASM guest applications. ## Role in the Wendy Platform The broader Wendy platform targets Linux/macOS edge devices (Raspberry Pi, Jetson, Mac) via WendyOS and wendy-agent. Wendy Lite covers bare-metal MCUs where containers and a full OS are not viable. Native apps use the complete ESP-IDF API and normal ESP-IDF project layout; `wendy run` detects, builds, and deploys them. WASM remains available when a smaller portable application boundary is preferable. > **Recommendation:** Start new applications as regular native ESP-IDF projects. Use the optional WASM runtime when portability or sandboxing matters more than full ESP-IDF access. Camera and display/framebuffer peripherals are not exposed to WASM guests and should be driven by native ESP-IDF drivers. ## Supported Targets and Boards Wendy Lite supports the following ESP32 targets: - ESP32-C5 - ESP32-C6 - ESP32-C61 - ESP32-P4 - ESP32-S3 For each target, `wendy install` lists the specific boards Wendy Lite supports. Choosing a board selects a Wendy Lite firmware variant pre-configured with that board's flash size, RAM, and peripherals. However, most targets also have a **generic** board, representing any board that exposes the SoC over USB with no further customization. If your specific board isn't listed, you can probably use the generic one instead. Native app capability is firmware-specific; where the installer offers multiple choices, select a board variant labeled **native app support**. Native applications are built with ESP-IDF 5.5.4 through the ESP-IDF Installation Manager (`eim`). ## Documentation ### User Guides - [Getting Started](getting-started.md) - [Native Apps](native-apps.md) - [WASM Apps](wasm-apps.md) - [Host API Reference](host-api.md) ### Implementation Details - [Source Code](repository.md) - [Swift SDK Internals](swift-sdk.md) - [StdIO](stdio.md) - [WendyCom Protocol](wendy-com.md) --- # Getting Started with Wendy Lite Source: https://docs.wendy.dev/latest/advanced/wendy-lite/getting-started/ --- title: "Getting Started with Wendy Lite" description: "Wendy Lite is a runtime for Espressif ESP32 microcontrollers. It runs on your device, handles its connectivity, and lets you build, deploy and run your own native applications on i" --- # Getting Started with Wendy Lite Wendy Lite is a runtime for Espressif ESP32 microcontrollers. It runs on your device, handles its connectivity, and lets you build, deploy and run your own native applications on it — all from a single command-line tool, the Wendy CLI. This guide takes the shortest path from nothing to a running application: three steps, ending with an existing example built and deployed on your device. ## Step 1 — Install the Wendy CLI The Wendy CLI is your entry point to the device: it installs Wendy Lite, configures it, and builds and deploys applications. Run this command to install it: ```sh curl -fsSL https://install.wendy.dev/cli.sh | bash ``` or follow the [CLI installation guide](https://docs.wendy.dev/latest/#1-install-the-wendy-cli). Wendy builds applications with EIM, the ESP-IDF Installation Manager, and picks the right ESP-IDF version for you. If EIM is missing, `wendy run` offers to install it with Homebrew. Without Homebrew, install EIM by following Espressif's [installation instructions](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html#installation). ## Step 2 — Install Wendy Lite on your device Connect your device over USB and run: ```sh wendy install ``` This flashes Wendy Lite onto the device. When prompted, select the board variant labeled **native app support**; variants without that label cannot run the native `blink-rgb-led` example. You only do this once per device. During installation, `wendy install` also prompts you to configure the Wi-Fi connection and enroll the device in Wendy Cloud — feel free to skip these for now. ## Step 3 — Build and deploy an application Rather than writing an application from scratch, start from an existing one. `blink-rgb-led` is the simplest example: it blinks the onboard RGB LED. Scaffold the project and enter its directory: ```sh mkdir blink-rgb-led cd blink-rgb-led wendy init --template blink-rgb-led ``` Then build, deploy and run it: ```sh wendy run ``` That is the whole cycle. `wendy run` compiles the project in the current directory, pushes the application to the device, and starts it. The onboard LED should start blinking. `blink-rgb-led` runs on these development kits: - ESP32-C5-DevKitC-1 - ESP32-C6-DevKitC-1 - ESP32-C6-DevKitM-1 - ESP32-C61-DevKitC-1 More native examples are in the [wendy-lite-native-apps](https://github.com/wendylabsinc/wendy-lite-native-apps) repository. ## Next steps You now have a working setup and a project you can edit. Change something in `blink-rgb-led/main/main.c`, run `wendy run` again, and watch it land on the device. When you want to write your own application, see [Native Apps for Wendy Lite](../../../advanced/wendy-lite/native-apps/) — it covers how a Wendy Lite project is put together, how to integrate the `wendy_core` component, and which parts of the hardware the system owns. To manage the board remotely, follow [enroll Wendy Lite in Wendy Cloud](../../../advanced/cli/lite-enrollment/). USB is required for enrollment; afterward, the board uses Wi-Fi for Cloud access. --- # Host API Reference Source: https://docs.wendy.dev/latest/advanced/wendy-lite/host-api/ --- title: "Host API Reference" description: "All functions are imported from the \"wendy\" WASM module. The canonical declaration source is Sources/CWendyLite/include/wendy.h. This page documents every function by subsystem, in" --- # Host API Reference All functions are imported from the `"wendy"` WASM module. The canonical declaration source is `Sources/CWendyLite/include/wendy.h`. This page documents every function by subsystem, including the Swift and Rust equivalents. > **Scope note:** wendy-lite host imports cover GPIO, I2C, SPI, UART, RMT, NeoPixel, Timer, Storage, System, OTel, BLE, WiFi, Sockets, TLS, and USB. **Camera and display/framebuffer peripherals are not exposed to WASM guests.** Boards with cameras or RGB LCD panels (e.g. ESP32-S31-Korvo-1, ESP32-P4-Function-EV) must drive those peripherals with native ESP-IDF drivers; the wendy-lite runtime can run alongside a native display loop but does not control the panel. ## Callback Model Many APIs are asynchronous. They accept a `handler_id` (an `int`/`Int32` you choose) and fire `wendy_handle_callback` on the guest when the event arrives. Your guest must export this function: ```c void wendy_handle_callback(int handler_id, int arg0, int arg1, int arg2); ``` Swift guests using `WendyLiteApp` get this export automatically (via `CallbackDispatch`). C and Rust guests must export it manually and call `sys_yield()` / `sys::yield_now()` periodically to receive dispatched events. --- ## GPIO | C function | Swift | Rust (`gpio::`) | |------------|-------|----------------| | `gpio_configure(pin, mode, pull)` | `GPIO.configure(pin:mode:pull:)` | `configure(pin, mode, pull)` | | `gpio_read(pin)` | `GPIO.read(pin:)` | `read(pin)` | | `gpio_write(pin, level)` | `GPIO.write(pin:level:)` | `write(pin, level)` | | `gpio_set_pwm(pin, freq_hz, duty_pct)` | `GPIO.setPWM(pin:frequencyHz:dutyPercent:)` | `set_pwm(pin, freq_hz, duty_pct)` | | `gpio_analog_read(pin)` | `GPIO.analogRead(pin:)` | `analog_read(pin)` | | `gpio_set_interrupt(pin, edge, handler_id)` | `GPIO.setInterrupt(pin:edge:handlerId:)` | `set_interrupt(pin, edge, handler_id)` | | `gpio_clear_interrupt(pin)` | `GPIO.clearInterrupt(pin:)` | `clear_interrupt(pin)` | **Mode constants:** `WENDY_GPIO_INPUT=0`, `WENDY_GPIO_OUTPUT=1`, `WENDY_GPIO_INPUT_OUTPUT=2` **Pull constants:** `WENDY_GPIO_PULL_NONE=0`, `WENDY_GPIO_PULL_UP=1`, `WENDY_GPIO_PULL_DOWN=2` **Interrupt edge constants:** `WENDY_GPIO_INTR_RISING=1`, `WENDY_GPIO_INTR_FALLING=2`, `WENDY_GPIO_INTR_ANYEDGE=3` Swift enums: `GPIOMode`, `GPIOPull`, `GPIOInterruptEdge`. --- ## I2C | C function | Swift (`I2C.`) | Rust (`i2c::`) | |------------|----------------|----------------| | `i2c_init(bus, sda, scl, freq_hz)` | `initialize(bus:sdaPin:sclPin:frequencyHz:)` | `init(bus, sda, scl, freq_hz)` | | `i2c_scan(bus, addrs_out, max_addrs)` | `scan(bus:buffer:maxAddresses:)` | `scan(bus, addrs_out)` | | `i2c_write(bus, addr, data, len)` | `write(bus:address:data:length:)` | `write(bus, addr, data)` | | `i2c_read(bus, addr, buf, len)` | `read(bus:address:buffer:length:)` | `read(bus, addr, buf)` | | `i2c_write_read(bus, addr, wr, wr_len, rd, rd_len)` | `writeRead(bus:address:writeData:writeLength:readBuffer:readLength:)` | `write_read(bus, addr, wr, rd)` | --- ## SPI | C function | Swift (`SPI.`) | Rust (`spi::`) | |------------|----------------|----------------| | `spi_open(host, mosi, miso, sclk, cs, clock_hz)` | `open(host:mosiPin:misoPin:sclkPin:csPin:clockHz:)` | `open(host, mosi, miso, sclk, cs, clock_hz)` | | `spi_close(dev_id)` | `close(deviceId:)` | `close(dev_id)` | | `spi_transfer(dev_id, tx_buf, rx_buf, len)` | `transfer(deviceId:txBuffer:rxBuffer:length:)` | `transfer(dev_id, tx, rx)` / `transmit(dev_id, tx)` | The Rust crate adds a `transmit` helper for TX-only transfers (passes `null` as `rx_buf`). --- ## UART | C function | Swift (`UART.`) | Rust (`uart::`) | |------------|-----------------|-----------------| | `uart_open(port, tx_pin, rx_pin, baud)` | `open(port:txPin:rxPin:baud:)` | `open(port, tx_pin, rx_pin, baud)` | | `uart_close(port)` | `close(port:)` | `close(port)` | | `uart_write(port, data, len)` | `write(port:data:length:)` | `write(port, data)` | | `uart_read(port, buf, len)` | `read(port:buffer:length:)` | `read(port, buf)` | | `uart_available(port)` | `available(port:)` | `available(port)` | | `uart_flush(port)` | `flush(port:)` | `flush(port)` | | `uart_set_on_receive(port, handler_id)` | `setOnReceive(port:handlerId:)` | `set_on_receive(port, handler_id)` | `setOnReceive` registers a callback handler ID; the host fires `wendy_handle_callback` when data arrives. --- ## RMT (Remote Control / Timing Buffer) | C function | Swift (`RMT.`) | Rust (`rmt::`) | |------------|----------------|----------------| | `rmt_configure(pin, resolution_hz)` | `configure(pin:resolutionHz:)` | `configure(pin, resolution_hz)` | | `rmt_transmit(channel_id, buf, len)` | `transmit(channelId:buffer:length:)` | `transmit(channel_id, buf)` | | `rmt_release(channel_id)` | `release(channelId:)` | `release(channel_id)` | Used for WS2812 protocols, IR, and other timing-sensitive serial protocols. --- ## NeoPixel (WS2812) High-level API layered on RMT. | C function | Swift (`NeoPixel.`) | Rust (`neopixel::`) | |------------|---------------------|---------------------| | `neopixel_init(pin, num_leds)` | `initialize(pin:numLeds:)` | `init(pin, num_leds)` | | `neopixel_set(index, r, g, b)` | `set(index:r:g:b:)` | `set(index, r, g, b)` | | `neopixel_clear()` | `clear()` | `clear()` | --- ## Timer | C function | Swift (`Timer.`) | Rust (`timer::`) | |------------|-----------------|-----------------| | `timer_delay_ms(ms)` | `delayMs(_:)` | `delay_ms(ms)` | | `timer_millis()` | `millis()` | `millis()` | | `timer_set_timeout(ms, handler_id)` | `setTimeout(ms:handlerId:)` | `set_timeout(ms, handler_id)` | | `timer_set_interval(ms, handler_id)` | `setInterval(ms:handlerId:)` | `set_interval(ms, handler_id)` | | `timer_cancel(timer_id)` | `cancel(timerId:)` | `cancel(timer_id)` | `setTimeout` and `setInterval` return a timer ID. Pass it to `cancel` to stop the timer before it fires. Swift apps should prefer `WendyClock.sleep` over `Timer.delayMs` — the former is async-safe, the latter is a blocking spin. --- ## System | C function | Swift (`System.` / `Console.`) | Rust (`sys::`) | |------------|-------------------------------|----------------| | `sys_uptime_ms()` | `System.uptimeMs()` | `sys::uptime_ms()` | | `sys_reboot()` | `System.reboot()` | `sys::reboot()` | | `sys_firmware_version(buf, len)` | `System.firmwareVersion(buffer:length:)` | `sys::firmware_version(buf)` | | `sys_device_id(buf, len)` | `System.deviceId(buffer:length:)` | `sys::device_id(buf)` | | `sys_sleep_ms(ms)` | `System.sleepMs(_:)` | `sys::sleep_ms(ms)` | | `sys_yield()` | `System.yield()` | `sys::yield_now()` | | `sys_wait_for_event(timeout_ms)` | `System.waitForEvent(timeoutMs:)` | — | | `wendy_print(buf, len)` | `Console.print(_:length:)` | `console::print(buf)` | `sys_wait_for_event` blocks until the host has a pending callback or the timeout elapses. Swift apps call this indirectly via `pumpAsyncRuntimeOnce`. C/Rust apps should call `sys_yield` periodically to allow callbacks to be dispatched. --- ## Storage (NVS) Non-Volatile Storage key-value API. Keys and values are byte buffers. | C function | Swift (`Storage.`) | Rust (`storage::`) | |------------|--------------------|--------------------| | `storage_get(key, key_len, val, val_len)` | `get(key:keyLength:value:valueLength:)` | `get(key, val)` | | `storage_set(key, key_len, val, val_len)` | `set(key:keyLength:value:valueLength:)` | `set(key, val)` | | `storage_delete(key, key_len)` | `delete(key:keyLength:)` | `delete(key)` | | `storage_exists(key, key_len)` | `exists(key:keyLength:)` | `exists(key)` | --- ## BLE ### Core BLE | C function | Swift (`BLE.`) | Rust (`ble::`) | |------------|----------------|----------------| | `ble_init()` | `initialize()` | `init()` | | `ble_advertise_start(name, name_len)` | `startAdvertising(name:nameLength:)` | `advertise_start(name)` | | `ble_advertise_stop()` | `stopAdvertising()` | `advertise_stop()` | | `ble_scan_start(duration_ms, handler_id)` | `startScan(durationMs:handlerId:)` | `scan_start(duration_ms, handler_id)` | | `ble_scan_stop()` | `stopScan()` | `scan_stop()` | | `ble_connect(addr_type, addr, addr_len, handler_id)` | `connect(addressType:address:addressLength:handlerId:)` | `connect(addr_type, addr, handler_id)` | | `ble_disconnect(conn_handle)` | `disconnect(connHandle:)` | `disconnect(conn_handle)` | ### GATT Server (`GATTS` / `ble::gatts`) | C function | Swift (`GATTS.`) | Rust (`ble::gatts::`) | |------------|------------------|-----------------------| | `ble_gatts_add_service(uuid, uuid_len)` | `addService(uuid:uuidLength:)` → service ID | `add_service(uuid)` | | `ble_gatts_add_characteristic(svc_id, uuid, uuid_len, flags)` | `addCharacteristic(serviceId:uuid:uuidLength:flags:)` → char ID | `add_characteristic(svc_id, uuid, flags)` | | `ble_gatts_set_value(chr_id, data, data_len)` | `setValue(characteristicId:data:dataLength:)` | `set_value(chr_id, data)` | | `ble_gatts_notify(chr_id, conn_handle)` | `notify(characteristicId:connHandle:)` | `notify(chr_id, conn_handle)` | | `ble_gatts_on_write(chr_id, handler_id)` | `onWrite(characteristicId:handlerId:)` | `on_write(chr_id, handler_id)` | ### GATT Client (`GATTC` / `ble::gattc`) | C function | Swift (`GATTC.`) | Rust (`ble::gattc::`) | |------------|------------------|-----------------------| | `ble_gattc_discover(conn_handle, handler_id)` | `discover(connHandle:handlerId:)` | `discover(conn_handle, handler_id)` | | `ble_gattc_read(conn_handle, attr_handle, handler_id)` | `read(connHandle:attrHandle:handlerId:)` | `read(conn_handle, attr_handle, handler_id)` | | `ble_gattc_write(conn_handle, attr_handle, data, data_len)` | `write(connHandle:attrHandle:data:dataLength:)` | `write(conn_handle, attr_handle, data)` | --- ## WiFi | C function | Swift (`WiFi.`) | Rust (`wifi::`) | |------------|-----------------|-----------------| | `wifi_connect(ssid, ssid_len, pass, pass_len)` | `connect(ssid:ssidLength:password:passwordLength:)` | `connect(ssid, pass)` | | `wifi_disconnect()` | `disconnect()` | `disconnect()` | | `wifi_status()` | `status()` | `status()` | | `wifi_get_ip(buf, len)` | `getIP(buffer:length:)` | `get_ip(buf)` | | `wifi_rssi()` | `rssi()` | `rssi()` | | `wifi_ap_start(ssid, ssid_len, pass, pass_len, channel)` | `startAP(ssid:ssidLength:password:passwordLength:channel:)` | `ap_start(ssid, pass, channel)` | | `wifi_ap_stop()` | `stopAP()` | `ap_stop()` | --- ## Sockets | C function | Swift (`Net.`) | Rust (`net::`) | |------------|----------------|----------------| | `net_socket(domain, type, protocol)` | `socket(domain:type:protocol:)` | `socket(domain, sock_type, protocol)` | | `net_connect(fd, ip, ip_len, port)` | `connect(fd:ip:ipLength:port:)` | `connect(fd, ip, port)` | | `net_bind(fd, port)` | `bind(fd:port:)` | `bind(fd, port)` | | `net_listen(fd, backlog)` | `listen(fd:backlog:)` | `listen(fd, backlog)` | | `net_accept(fd)` | `accept(fd:)` | `accept(fd)` | | `net_send(fd, data, len)` | `send(fd:data:length:)` | `send(fd, data)` | | `net_recv(fd, buf, len)` | `recv(fd:buffer:length:)` | `recv(fd, buf)` | | `net_close(fd)` | `close(fd:)` | `close(fd)` | Swift enums: `SocketDomain` (`.inet = 2`), `SocketType` (`.stream = 1`, `.dgram = 2`). C constants: `WENDY_AF_INET=2`, `WENDY_SOCK_STREAM=1`, `WENDY_SOCK_DGRAM=2`. --- ## DNS | C function | Swift (`DNS.`) | Rust (`dns::`) | |------------|----------------|----------------| | `dns_resolve(hostname, hostname_len, result_buf, result_len)` | `resolve(hostname:hostnameLength:resultBuffer:resultLength:)` | `resolve(hostname, result_buf)` | --- ## TLS | C function | Swift (`TLS.`) | Rust (`tls::`) | |------------|----------------|----------------| | `tls_connect(host, host_len, port)` | `connect(host:hostLength:port:)` → fd | `connect(host, port)` | | `tls_send(fd, data, len)` | `send(fd:data:length:)` | `send(fd, data)` | | `tls_recv(fd, buf, len)` | `recv(fd:buffer:length:)` | `recv(fd, buf)` | | `tls_close(fd)` | `close(fd:)` | `close(fd)` | --- ## OpenTelemetry | C function | Swift (`OTel.`) | Rust (`otel::`) | |------------|-----------------|-----------------| | `otel_log(level, msg, msg_len)` | `log(level:message:messageLength:)` | `log(level, msg)` | | `otel_metric_counter_add(name, name_len, value)` | `counterAdd(name:nameLength:value:)` | `counter_add(name, value)` | | `otel_metric_gauge_set(name, name_len, value)` | `gaugeSet(name:nameLength:value:)` | `gauge_set(name, value)` | | `otel_metric_histogram_record(name, name_len, value)` | `histogramRecord(name:nameLength:value:)` | `histogram_record(name, value)` | | `otel_span_start(name, name_len)` | `spanStart(name:nameLength:)` → span ID | `span_start(name)` | | `otel_span_set_attribute(span_id, key, key_len, val, val_len)` | `spanSetAttribute(spanId:key:keyLength:value:valueLength:)` | `span_set_attribute(span_id, key, val)` | | `otel_span_set_status(span_id, status)` | `spanSetStatus(spanId:status:)` | `span_set_status(span_id, status)` | | `otel_span_end(span_id)` | `spanEnd(spanId:)` | `span_end(span_id)` | Log level constants: `WENDY_OTEL_ERROR=1`, `WENDY_OTEL_WARN=2`, `WENDY_OTEL_INFO=3`, `WENDY_OTEL_DEBUG=4`. Swift enum: `OTelLogLevel`. --- ## USB | C function | Swift (`USB.`) | Rust (`usb::`) | |------------|----------------|----------------| | `usb_cdc_write(data, len)` | `cdcWrite(_:length:)` | `cdc_write(data)` | | `usb_cdc_read(buf, len)` | `cdcRead(_:length:)` | `cdc_read(buf)` | | `usb_hid_send_report(report_id, data, len)` | `hidSendReport(reportId:data:length:)` | `hid_send_report(report_id, data)` | --- # Native Apps for Wendy Lite Source: https://docs.wendy.dev/latest/advanced/wendy-lite/native-apps/ --- title: "Native Apps for Wendy Lite" description: "Wendy Lite is a runtime for Espressif ESP32 microcontrollers. It handles your device's connectivity and lets you write your own native applications, build them, and deploy them to " --- # Native Apps for Wendy Lite ## Overview Wendy Lite is a runtime for Espressif ESP32 microcontrollers. It handles your device's connectivity and lets you write your own **native applications**, build them, and deploy them to it. Native apps are built with Espressif's tooling, in particular [ESP-IDF](https://docs.espressif.com/projects/esp-idf/), version **5.5**. ESP-IDF lets you write your application in C or C++. Because it is driven by CMake, you can also integrate other compiled languages, such as Rust or Swift. The **Wendy CLI** is your entry point to the device. It lets you: - install Wendy Lite on a device, - configure Wi-Fi and cloud access, - build and deploy your native application. Deployment can happen over three transports: | Transport | Availability | | --- | --- | | USB | available | | Wi-Fi (local network) | available | | Cloud | coming soon | Configuring a device can additionally go over **Bluetooth Low Energy (BLE)**. BLE is there mainly to reach a device in the situations where neither Wi-Fi nor USB is available. ## Prerequisites Before you start, you need: - **Wendy CLI** — installs Wendy Lite on a device, configures it, and builds and deploys your application. - **EIM**, the ESP-IDF Installation Manager — Espressif's tool for managing the ESP-IDF development environment (toolchains, Python environment, SDK versions). Wendy Lite supports **ESP-IDF 5.5**; the Wendy CLI manages this environment for you, so you never need to pin a version yourself. If EIM is missing, `wendy run` offers to install it with Homebrew. - a device already flashed with Wendy Lite. [Getting Started with Wendy Lite](getting-started.md) walks through installing the CLI and flashing a device. When you install Wendy Lite on your device, ensure you pick the variant labeled **native app support**. If you are not sure whether your installed version supports native apps, run `wendy device info` to check. ## Anatomy of a Wendy Lite native app A Wendy Lite native app is an ordinary ESP-IDF project with two additions: - a `wendy.json` manifest in the project root. - the **`wendy_core`** component. `wendy_core` is what makes your application a Wendy app rather than a bare ESP-IDF firmware. It runs your code inside the Wendy environment, so that the device can be managed by the Wendy CLI and reached through Wendy Cloud. Creating an app therefore comes down to three steps: 1. Create an ESP-IDF project. 2. Declare `wendy_core` in the project's dependencies. 3. Call `wendy_core_init()` at the very beginning of `app_main()`. ### Step 1 — Create an ESP-IDF project Use the standard ESP-IDF project layout: a top-level `CMakeLists.txt`, a `main` component, a `sdkconfig.defaults` holding your build configuration, and a `wendy.json` manifest: ```text my-app/ ├── CMakeLists.txt ├── sdkconfig.defaults ├── wendy.json └── main/ ├── CMakeLists.txt ├── idf_component.yml └── main.c ``` `wendy.json` looks like this: ```json { "$schema": "https://wendy.dev/schemas/wendy.json", "appId": "com.example.my-esp32-app", "version": "0.1.0", "platform": "wendy-lite", "entitlements": [] } ``` > **`sdkconfig` is generated, not authored.** > It's created on first build and can be regenerated whenever the target > changes, so changes made directly to it can be lost. Store your > configuration in `sdkconfig.defaults` instead, or, if you really want to > rely on it, keep `sdkconfig` itself in your git repo. ### Step 2 — Declare the `wendy_core` dependency Add `wendy_core` to your component manifest, `main/idf_component.yml`: ```yaml dependencies: idf: version: ">=5.5.0" wendy_core: git: "https://github.com/wendylabsinc/wendy-lite.git" path: "components/wendy_core" ``` Then require it from your component in `main/CMakeLists.txt`: ```cmake idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES wendy_core) ``` ### Step 3 — Initialize Wendy Core `wendy_core_init()` must be the **first** function your application calls: ```c #include "wendy_core.h" void app_main(void) { ESP_ERROR_CHECK(wendy_core_init()); /* Your application starts here. */ } ``` This call brings up the agent that handles all communication with the Wendy environment. Nothing else in your application may run before it. ## Starting from an example The quickest way to start a project is to copy an existing one. The [`blink-rgb`](https://github.com/wendylabsinc/wendy-lite-native-apps/tree/main/blink-rgb) project is a deliberately minimal example: it shows how a project for Wendy Lite is laid out and how `wendy_core` is integrated. Its `main/main.c` calls `wendy_core_init()` first, as in Step 3 above, then adds a simple blink loop: ```c #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "rgb_led.h" #include "wendy_core.h" #define RGB_LED_GPIO 8 void app_main(void) { ESP_ERROR_CHECK(wendy_core_init()); ESP_ERROR_CHECK(rgb_led_init(RGB_LED_GPIO, 1)); bool on = false; while (true) { on = !on; if (on) { ESP_ERROR_CHECK(rgb_led_set(0, 24, 24, 0)); } else { ESP_ERROR_CHECK(rgb_led_clear()); } vTaskDelay(pdMS_TO_TICKS(500)); } } ``` ## What Wendy Lite owns, and what your app must not touch Wendy Lite handles communication for you. It owns and initializes the communication controllers and peripherals: - **USB** - **Wi-Fi** - **Bluetooth Low Energy (BLE)** — the system uses it above all to configure the device when Wi-Fi and USB are not available. Your application is free to *use* these channels, but it must never configure them itself. Do not initialize the Wi-Fi or BLE controller, and do not set up the network stack: use what Wendy Lite has already configured. > **Do not use the USB Serial/JTAG controller as a console.** > Wendy Lite claims that peripheral and drives the console itself. Configuring > it from your application, or using it for your own I/O, will conflict with > the system. ## Building and deploying One command covers the whole cycle. Run it from your project directory: ```sh wendy run ``` `wendy run` compiles the project, pushes the resulting application to the device, and starts it. It selects the right ESP-IDF toolchain version for you, so you do not have to pin one yourself. The application is pushed over USB, over the local network, or (soon) through the cloud. To rebuild the project entirely from scratch, delete `build`, `sdkconfig`, and `managed_components`, then run `wendy run` again. Also delete `dependencies.lock` if you want to move to more recent versions of your dependencies (such as `wendy_core`). ## The Project Remains a Normal ESP-IDF Project Your application can use ESP-IDF components, managed components, Kconfig options, and peripheral drivers, exactly as in any other ESP-IDF project — except for the restrictions listed above. This is especially important for displays, cameras, audio, and other hardware that needs full access to the native ESP-IDF APIs. By including a partition table that matches the one Wendy Lite itself uses for the installed firmware variant, you can still compile and deploy through classic IDF commands: ```bash idf.py set-target esp32c6 idf.py menuconfig idf.py build idf.py -p /dev/cu.usbmodemXXXX flash monitor ``` There is no CLI command yet to fetch this partition table. Extract the `partitions.csv` file that corresponds to your Wendy Lite variant from the [wendy-lite repository](https://github.com/wendylabsinc/wendy-lite), add it to your project, and enable it in `sdkconfig.defaults`: ```ini CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" ``` Without this, `idf.py flash` uses the project's default partition table instead, which can overwrite or omit Wendy Lite's reserved partitions. --- # Wendy Lite Repository Source: https://docs.wendy.dev/latest/advanced/wendy-lite/repository/ --- title: "Wendy Lite Repository" description: "The wendy-lite source code lives in wendylabsinc/wendy-lite — the Swift SDK, Rust crate, C headers, and ESP-IDF firmware component all live in this single repo." --- # Wendy Lite Repository The wendy-lite source code lives in [wendylabsinc/wendy-lite](https://github.com/wendylabsinc/wendy-lite) — the Swift SDK, Rust crate, C headers, and ESP-IDF firmware component all live in this single repo. ## Repository Layout ``` wendy-lite/ Sources/ CWendyLite/ include/wendy.h Host-function declarations (WASM import attributes) shim.c Thin C shim for Swift interop WendyLite/ Swift SDK (SwiftPM library target) WendyLite.swift Re-exports CWendyLite WendyLiteApp.swift @main protocol + async runtime bootstrap WendyClock.swift Embedded-Swift async clock + TimerHub CallbackDispatch.swift Handler registry + wendy_handle_callback export GPIO.swift GPIO types and wrapper enum I2C.swift I2C wrapper SPI.swift SPI wrapper UART.swift UART wrapper RMT.swift RMT (remote control) wrapper NeoPixel.swift NeoPixel/WS2812 wrapper Timer.swift Low-level timer wrapper Network.swift WiFi, Net (sockets), DNS, TLS wrappers BLE.swift BLE, GATTS, GATTC wrappers OTel.swift OpenTelemetry log/metrics/tracing wrapper Storage.swift NVS key-value wrapper System.swift System + Console wrappers USB.swift USB CDC + HID wrapper src/lib.rs Rust crate (no_std FFI + safe wrappers) CMakeLists.txt ESP-IDF component CMake (firmware build) Package.swift SwiftPM package (Swift SDK) Cargo.toml Cargo package (Rust crate) .github/workflows/ build.yml Matrix firmware build + nightly/release publishing ``` ## CI and Releases Wendy publishes installable firmware variants through its stable and nightly release channels. `wendy install` reads the current firmware catalog and only presents board variants that have a published binary for the selected channel. --- # Wendy Lite StdIO Source: https://docs.wendy.dev/latest/advanced/wendy-lite/stdio/ --- title: "Wendy Lite StdIO" description: "On Wendy Lite, a WASM app can print data to the standard output. When developed in Swift, the app typically uses print() to do so." --- # Wendy Lite StdIO On Wendy Lite, a WASM app can print data to the standard output. When developed in Swift, the app typically uses `print()` to do so. A native app can use `printf()` or any C API related to `stdout`. It can also use `write()` on `STDOUT_FILENO`. And finally, it can also use the ESP-IDF log routines, named `ESP_LOGx()`. In all these cases, the output goes to these destinations: * UART: Even if slow and archaic, this is the primary destination. Bootloader messages, and any message related to a runtime fatal error (assertions, crashes, ...), are output only on the UART interface. * USB-Serial-JTAG (USJ): In the modern era, we always use USB to connect the computer to the device. This allows flashing the firmware, running the debugger and displaying logs. * WendyCom connection: when the device is controlled remotely via the WendyCom protocol, the client can attach to the device and collect all outputs. ## Implementation * `ESP_LOGx()` calls are redirected to stdout. * `stdout` is a stdlib FILE opened on `/dev/console`. * `/dev/console` is managed by the `wendy_stdio` component. It implements the VFS which redirects the output to the UART, to the USJ and to any additional subscriber. * `wendy_com_stdio` subscribes to `wendy_stdio` and stores the output in a ring buffer, making the collected data available to the WendyCom client. There is one caveat: when the output data is collected by the WendyCom client, `wendy_com` is in charge of draining the ring buffer. `wendy_com` runs in the com thread. Therefore, the com thread must never block and, in particular, it must never block by writing logs when the ring buffer is full. This would result in a deadlock. To mitigate this problem, we adopted two rules: 1. The com thread must never write to stdout or stderr. Note that these FILEs have their own locks, which makes it impossible to prevent com thread deadlocks other than by avoiding the com thread writing to them in the first place. 2. Logs generated by the com thread by calling `ESP_LOGx()` go through a special path that never blocks. When invoked in the com thread, `ESP_LOGx()` works in this way: 1. `wendy_com_stdio` subscribes to the `ESP_LOG` mechanism and intercepts the logs coming from the com thread. 2. `wendy_com_stdio` puts the data into the ring buffer, in non-blocking mode. 3. `wendy_com_stdio` delivers the same data to any subscriber. 4. `Wendy_USJ` subscribes to `wendy_com_stdio`, gets the com thread logs from it and prints them on the USJ output. --- # Swift SDK Internals Source: https://docs.wendy.dev/latest/advanced/wendy-lite/swift-sdk/ --- title: "Swift SDK Internals" description: "The WendyLite SwiftPM library is an Embedded Swift target compiled to wasm32-unknown-wasip1. This document covers how the async runtime, entry point protocol, timers, and callback " --- # Swift SDK Internals The `WendyLite` SwiftPM library is an Embedded Swift target compiled to `wasm32-unknown-wasip1`. This document covers how the async runtime, entry point protocol, timers, and callback dispatch work — context needed when extending the SDK or debugging unexpected behaviour. ## Requirements - Swift 6.3.2 via `swiftly` - Embedded Swift SDK: `swift-6.3.2-RELEASE_wasm-embedded` - Target triple: `wasm32-unknown-wasip1` Build command: ```bash swiftly run +6.3.2 swift build \ --swift-sdk swift-6.3.2-RELEASE_wasm-embedded \ --triple wasm32-unknown-wasip1 \ -c release ``` ## Entry Point: WendyLiteApp `WendyLiteApp` is a protocol with a static `main()` entry point wired to `@main`. Conform a type to it and annotate with `@main`: ```swift @main struct MyApp: WendyLiteApp { let clock = WendyClock() mutating func setup() async { // One-time init (hardware config, WiFi connect, etc.) } mutating func loop() async { // Called repeatedly for the lifetime of the app } } ``` `setup()` has a default no-op implementation so it is optional. `main()` does the following before entering the loop: 1. Calls `bootstrapAsyncRuntime()` (idempotent, guarded by `RuntimeBootstrapState`). 2. Constructs an instance via `Self()`. 3. Awaits `setup()`. 4. Loops forever awaiting `loop()`. ## Async Runtime Bootstrap Embedded Swift on WASM runs a cooperative single-threaded executor. `bootstrapAsyncRuntime()` (in `WendyLiteApp.swift`) sets it up: 1. Registers the timer callback handler ID with `CallbackDispatch` (handler ID `1`). 2. Drains any events that arrived before user code starts (`pumpAsyncRuntimeOnce(timeoutMs: 0)`). 3. Launches a background `Task` that calls `pumpAsyncRuntimeOnce(timeoutMs: 250)` in a loop, yielding between iterations so Swift tasks can run. `pumpAsyncRuntimeOnce` calls `System.waitForEvent(timeoutMs:)` (which maps to `sys_wait_for_event` on the host and blocks until a callback fires or the timeout elapses), then calls `TimerState.shared.drainReady()` to resume any sleeping tasks whose deadlines have passed. ### Why not Task.sleep? `Task.sleep` requires a conforming `Clock` type, but Embedded Swift cannot satisfy the untyped throwing requirement in the `Clock` protocol. Use `WendyClock` instead. ## WendyClock `WendyClock` (`WendyClock.swift`) is a concrete async-sleep implementation that drives `await clock.sleep(for:)` and `await clock.sleep(until:)`. ```swift let clock = WendyClock() try? await clock.sleep(for: .milliseconds(500)) ``` Internally: - `now` reads `System.uptimeMs()` and wraps it in `WendyClock.Instant`. - `sleep(until:)` delegates to `TimerState.shared.sleep(until:)` in `TimerHub`. ### TimerHub `TimerHub` maintains a sorted array of `Waiter` entries (deadline + checked continuation). When a sleep is requested: 1. A `Waiter` is inserted in deadline order. 2. `rescheduleForEarliestDeadline()` calls `Timer.setTimeout` on the host for the earliest pending deadline (cancelling any previously scheduled timer first). 3. When the host fires the timer, `timerFired()` moves all expired waiters to `readyWaiters`. 4. `drainReady()` (called from `pumpAsyncRuntimeOnce`) resumes those continuations. Cancellation is handled via `withTaskCancellationHandler`: a `SleepRegistration` flag is shared between the operation and the cancel handler so that cancellation arriving before or after the continuation is registered is always handled correctly. **Edge case**: If `Timer.setTimeout` returns a negative ID, the host could not allocate an ESP timer (no free slots or `esp_timer_create`/`esp_timer_start` failure). Pending sleepers will stall in that case — no recovery is attempted. ## Callback Dispatch `CallbackDispatch` (`CallbackDispatch.swift`) is the single routing point for all host-to-guest async events (GPIO interrupts, BLE events, timer callbacks, UART receive callbacks). ```swift // Register a handler for handler_id = 42 CallbackDispatch.register(42) { arg0, arg1, arg2 in // handle event } ``` The exported C function `wendy_handle_callback` (which the host calls to dispatch events into the WASM guest) routes by handler ID through the registered table. Handler IDs are application-defined integers. Timer callbacks internally use ID `1` (a private constant in `WendyClock.swift`). Your application must use IDs that do not collide with internal use. ## SwiftPM Package Structure ``` Package.swift targets: CWendyLite (C target) — wendy.h + shim.c WendyLite (Swift target, depends on CWendyLite) swiftSettings: .enableExperimentalFeature("Embedded"), -wmo ``` The `WendyLite` target uses whole-module optimisation (`-wmo`) and the Embedded experimental feature, both of which are required for the WASM Embedded SDK. App packages must replicate these settings in their own executable target (see the README for the complete template). ## Linker Requirements for App Packages App packages must pass specific linker flags: | Flag | Purpose | |------|---------| | `--allow-undefined` | Host imports are resolved at load time by WAMR | | `--initial-memory=65536` | Minimum WASM linear memory | | `--table-base=1` | Avoid table slot 0 (reserved by some runtimes) | | `--strip-all` | Minimise binary size | | `--export=malloc`, `--export=free` | Host may call these for buffer allocation | | `--export=wendy_handle_callback` | Required for host-to-guest callbacks | | `-z stack-size=8192` | Stack size; increase for deeper call stacks | ## Known Constraints - **No `Task.sleep`** — use `WendyClock.sleep`. - **Single-threaded executor** — concurrent tasks interleave cooperatively; there is no parallelism. - **Timer slots are finite** — the ESP timer subsystem has a limited pool; each outstanding `WendyClock.sleep` occupies one slot for the minimum-deadline timer. - **nonisolated(unsafe)** — `CallbackDispatch` and `TimerState` use `nonisolated(unsafe)` storage because the cooperative single-threaded WASM executor guarantees no true concurrent access. --- # WASM Apps Source: https://docs.wendy.dev/latest/advanced/wendy-lite/wasm-apps/ --- title: "WASM Apps" description: "Wendy Lite firmware variants with WASM support can run Swift, Rust, C/C++, AssemblyScript, or WAT guests as a smaller, portable, sandboxed alternative to native ESP-IDF apps. Guest" --- # WASM Apps Wendy Lite firmware variants with WASM support can run Swift, Rust, C/C++, AssemblyScript, or WAT guests as a smaller, portable, sandboxed alternative to native ESP-IDF apps. Guests call into hardware and platform features through Wendy's host-imported APIs — see [`host-api.md`](host-api.md) for the full function reference. ## Optional WASM Guest Languages | Language | Entry point | Library | |----------|-------------|---------| | Swift | `@main` on `WendyLiteApp` | SwiftPM: `WendyLite` (this repo) | | Rust | `#[no_mangle] pub extern "C" fn _start()` | Cargo: `wendy-lite` (this repo) | | C / C++ | `void _start(void)` | Include `Sources/CWendyLite/include/wendy.h` | | AssemblyScript | `export function _start()` | `@external("wendy", ...)` declarations | | WAT | `(export "_start" ...)` | Direct import from `"wendy"` module | ## Building and Running WASM Apps For Swift projects, `wendy run` builds the package for `wasm32-unknown-wasip1`, uploads the `.wasm` application, and attaches to its console. For other guest languages, or to embed a `.wasm` app directly into a firmware build, see [Manually Embedding a WASM App](#manually-embedding-a-wasm-app) below. ## Manually Embedding a WASM App For low-level firmware development, a WASM guest can still be embedded directly: 1. Build your app to `.wasm`. 2. Convert the binary to a C header array: `./wasm_apps/wasm2header.sh my_app.wasm main/demo_wasm.h` 3. Rebuild the firmware: `idf.py build` 4. Flash: `idf.py flash` See [`host-api.md`](host-api.md) for the full function reference, [`swift-sdk.md`](swift-sdk.md) for Swift-specific internals, [`stdio.md`](stdio.md) for console I/O, [`native-apps.md`](native-apps.md) for the native app build-to-device flow, and [`wendy-com.md`](wendy-com.md) for the WendyCom protocol reference. --- # WendyCom Protocol Source: https://docs.wendy.dev/latest/advanced/wendy-lite/wendy-com/ --- title: "WendyCom Protocol" description: "WendyCom is a lightweight request-response protocol used to control Wendy Lite devices. It runs over two transports: USB (via the ESP32 USB Serial JTAG peripheral) and mTLS (over t" --- # WendyCom Protocol ## Basics WendyCom is a lightweight request-response protocol used to control Wendy Lite devices. It runs over two transports: USB (via the ESP32 USB Serial JTAG peripheral) and mTLS (over the network). The host sends a `WendyComCommand` and the device replies with a `WendyComResponse`. Each command carries a `request_id` that the response echoes back, allowing the host to match replies to outstanding requests. The command type is encoded as a `oneof` field in the protobuf message. In addition to the request-response exchange, either side can send unsolicited `WendyComEvent` messages at any time. Events are not tied to a request: they carry an `event_id` instead of a `request_id` and receive no reply. The device uses them, for example, to stream console output after a `ConsoleAttach` command (the `event_id` given in the attach params tags the resulting events). Each message consists of an 8-byte header followed by a protobuf-encoded body. ### Message Header |Offset|Field|Type|Description| |------|-----|----|-----------| |0|`magic`|`uint8_t`|Fixed sync byte `0xA5`, used to detect framing errors| |1|`version`|`uint8_t`|Protocol version, currently always `2`| |2|`category`|`uint8_t`|Message category; only `0` is accepted (non-zero triggers a receive reset)| |3|`channel`|`uint8_t`|Sub-channel within the link — see [WendyCom via Cloud](#wendycom-via-cloud)| |4–5|`reserved`|`uint16_t`|Unused, set to `0` on send| |6–7|`body_size`|`uint16_t`|Length of the body that follows, in network byte order| ## WendyCom over TLS The device listens on TCP port 5054 and advertises itself via mDNS as `_wendy-lite._tcp`. Every connection is TLS-encrypted. The device operates in one of two modes depending on whether it has been enrolled in Wendy Cloud: - **Unauthenticated TLS** — When not enrolled, the device uses a built-in default certificate; client identity is not verified. This is the factory default. The mDNS TXT record advertises `mtls=false`. - **mTLS** — When enrolled, the device uses a provisioned certificate and requires the client to present a certificate signed by the configured chain of trust. The mDNS TXT record advertises `mtls=true`. ## WendyCom over USB Serial JTAG WendyCom runs over the ESP32 USB Serial JTAG peripheral. This link supports several modes, WendyCom being one of them; the host switches between them by sending escape sequences. ### Modes **`USJ_MODE_OFF` (0)** — USB input is silently discarded. No data is written to the channel. **`USJ_MODE_CONSOLE` (1)** — ESP log output is mirrored to the USB channel (the `vprintf`-hook forwards every log line). `wendy_usj_write()` is active. This is the startup default. **`USJ_MODE_ECHO` (2)** — Every received byte is immediately echoed back. Useful for connectivity checks. **`USJ_MODE_COM` (3)** — The channel is handed to the `wendy_com` stack, enabling WendyCom message exchange over USB as we do over mTLS. ### Escape sequences `DLE` (0x10) characters in the data stream are interpreted as commands. Each `DLE` is followed by a command byte: ```text DLE c → switch to console mode DLE e → switch to echo mode DLE m → switch to com mode DLE o → switch to off mode ``` In `USJ_MODE_COM`, the `wendy_com_uart` layer intercepts escape sequences before data reaches the `wendy_com` stack: ```text DLE _ → pass a literal DLE byte through to the `wendy_com` stack DLE k → keep-alive, sent every 6 seconds DLE → disconnect the `wendy_com` link, then switch to mode ``` Two consecutive `DLE` characters are considered like one. Therefore, you can put as many `DLE` as you want in front of a command byte. ### Establishing a WendyCom connection Before entering `USJ_MODE_COM`, the host must flush any stale data buffered in the USB layer on both sides. The handshake uses echo mode for this: 1. Open the USB channel. 2. Send `DLE e` to switch to echo mode. 3. Verify the link: send a few bytes and confirm that data flows back. 4. Drain the channel: read until no data arrives within a timeout. 5. Send a unique sentinel byte sequence and wait until it is echoed back — this confirms the channel is fully flushed and both sides are in sync. 6. Send `DLE m` to switch to `USJ_MODE_COM`. The channel is now in `USJ_MODE_COM` with no stale data in either buffer. #### Implementation notes A board that has just rebooted is a hostile environment for this sequence: boot logs keep the stream continuously readable so it never goes quiet, bytes can be dropped, and the device is not ready to serve WendyCom for some time after USB comes up. The CLI's implementation (`serialHandshake`, in `go/internal/cli/liteclient/link_direct.go`) is therefore more aggressive than the steps above, collapsing 2-5 into a single retry loop: - **Steps 2 and 5 go out as one write.** Each attempt sends `DLE DLE DLE DLE e` immediately followed by 16 spaces of padding and a fresh 32-character random sentinel, as a single payload. - **The echo mode command is repeated with every attempt.** The device refuses `DLE e` until its WendyCom agent is running, and the window between USB initialization and that point is wide enough to swallow the first attempt. Applying the command is a plain mode assignment, so re-issuing it is idempotent. Escape sequences are consumed by the mode parser and never echoed, so the prefix cannot show up in the echoed stream. - **Every attempt carries a fresh sentinel.** The sentinel is a stream position marker, not a liveness probe: because the echo is FIFO, seeing the *latest* one come back proves nothing the host wrote earlier is still in flight, which is what step 4 is really after. Matching an older sentinel would leave the later echoes queued, and the WendyCom framer reads their leading padding as a frame header. - **The padding is 16 spaces.** A `DLE` whose command byte is lost stays latched and consumes the byte after it, so a run of expendable bytes ahead of the sentinel keeps a dropped byte from corrupting it. - **Matching uses a rolling window.** The host reads one byte at a time, keeping the last 32, and compares that window to the sentinel just sent. Draining the channel (step 4) falls out of this for free, without the stream ever having to go quiet. - **Retries widen.** The first attempt goes out immediately, then the host waits 100 ms before retrying, then 200, 300, 400 and 500 ms, holding at 500 ms, within a total budget of 3 seconds — about eight attempts. Widening keeps probing a device that is still booting without pushing a kilobyte of sentinels at it. ### Switching to program mode In addition to the modes described above, the host can reset the device into program mode via the DTR and RTS signals exposed by the USB Serial JTAG peripheral. The `wendy os install` command uses this to flash the firmware. See [wendy os install](../clients/wendy-cli/commands/os/install.md) ### Caveat USB Serial JTAG currently has no flow control. If the host sends data faster than the device can consume it, bytes may be silently dropped. To avoid this, messages must be kept below 1024 bytes, and the host must wait for a response before sending the next message. ## WendyCom via Cloud An enrolled board can be selected through `wendy cloud discover` without a direct USB or LAN connection. The CLI opens an encrypted connection through Wendy Cloud; WendyCom commands and replies remain encrypted between the CLI and board. See [connect through Cloud](../lite-cloud-relay.md) for setup and troubleshooting. The board maintains an outbound control connection, so its network does not need an inbound port. After handing a client connection over to the relay, it reconnects to Cloud so it can continue reporting presence. ### Time synchronization for enrollment The CLI obtains signed time for the board automatically. Firmware verifies the reply before accepting it; your computer does not simply set the board's clock. The default enrollment flow uses Sigstore's signed-time service. Firmware that supports Roughtime can use that mode in a compatible custom deployment. ### Channel multiplexing The `channel` byte supports multiple logical channels within a WendyCom link. The Cloud relay uses channel 0 inside end-to-end TLS. - **Channel 0 is open by default.** It is created implicitly when a link connects and can never be opened or closed explicitly. Direct connections (USB, local TLS) always use channel 0. - The device currently supports up to 4 channels per link, including channel 0. Data received on a channel that has not been opened is reported with a `channel_state` error (see below). ### Channel open/close service messages A WendyCom host can manage nonzero channels using `WendyComService` messages: ```proto message WendyComOpenChannel {} message WendyComCloseChannel {} message WendyComChannelOpen {} message WendyComChannelClose {} enum WendyComChannelErrorReason { WENDY_COM_CHANNEL_ERROR_REASON_NOT_OPEN = 0; WENDY_COM_CHANNEL_ERROR_REASON_REJECTED = 1; } message WendyComChannelError { WendyComChannelErrorReason reason = 1; } message WendyComChannelState { oneof state { WendyComChannelOpen open = 1; WendyComChannelClose close = 2; WendyComChannelError error = 3; } } message WendyComService { oneof cmd { WendyComOpenChannel open_channel = 1; WendyComCloseChannel close_channel = 2; WendyComChannelState channel_state = 3; } } ``` A host opening a nonzero channel sends `open_channel` and waits for confirmation before sending payload. It sends `close_channel` when finished. The Cloud broker does not perform these operations. The device answers channel management with a `WendyComChannelState` message, sent device → host on the channel concerned: - `open_channel` → the device replies with `channel_state` `open` on success, or `error` with reason `REJECTED` if the channel cannot be opened (all channel slots in use, or the channel is already open — including channel 0). - `close_channel` → the device replies with `channel_state` `close`, or `error` with reason `REJECTED` if the channel cannot be closed (channel 0) or is not open. The device may also send `channel_state` `close` unsolicited when it closes a channel on its own. - Any other (disallowed) service message → the device replies with `channel_state` `error` with reason `REJECTED`. - A command received on a channel that is not open → the device reports `channel_state` `error` with reason `NOT_OPEN`. `channel_state` reports reach the WendyCom host inside the encrypted stream. ## WendyCom Handshake A handshake message is sent automatically when a connection is established. The host sends the protocol version it supports (e.g. `major=1, minor=0`) along with a fresh `handshake_id`; the device replies with the same kind of message, echoing the `handshake_id` and carrying its own protocol version. The major version must match on both sides — the device closes the connection if the host's major version is unsupported. Commands sent before the handshake completes are rejected. ## WendyCom Commands ### Ping Checks that the device is reachable and responsive. The device replies with `OK` and no payload. ### Reboot Asks the device to reboot. The device replies with `OK` before resetting. ### App Download Transfers a WASM application binary to the device in three steps: 1. **AppPushBegin** — opens a transfer session and tells the device the total file size in bytes. 2. **AppPushData** — sends the binary in chunks, each carrying its byte offset within the file. The host sends one command per chunk and waits for `OK` before sending the next. 3. **AppPushEnd** — closes the transfer session, signalling that all chunks have been delivered. ### AppStart Starts the previously downloaded WASM application on the device. ### AppStop Stops the currently running application. ### GetDeviceIdentity Queries the device for its identity. The device responds with a `WendyComDeviceIdentity` message containing three fields: - `id` — a unique identifier for the device (e.g. a serial number or hardware ID). - `name` — a short machine-readable name. - `display_name` — a human-readable display name. If the device cannot provide an identity, it returns an error result and the host discards the device from its list of reachable devices. ### GetDeviceInfo Queries the device for information about its hardware and software. The device responds with a `WendyComDeviceInfo` message containing: - `os` — the operating system name, typically `wendy-lite`. - `os_version` — the operating system version. - `cpu_architecture` — the CPU architecture. - `target` — the SoC name (e.g. `esp32c6` or `esp32s3`). - `wasm_app_support` — whether the device can run WASM applications. - `native_app_support` — whether the device can run native applications. - `board` — the board identifier (e.g. `esp32c6_generic` or `esp32s3_seeed_xiao_native`). Empty when the board is not known. --- # Wendy Os Publisher Source: https://docs.wendy.dev/latest/advanced/wendy-os-publisher/ --- title: "Wendy Os Publisher" description: "A tool that uploads, removes and promotes (nightly - release) builds for WendyOS and Wendy Lite." --- A tool that uploads, removes and promotes (nightly -> release) builds for [WendyOS](../wendyos/) and [Wendy Lite](../wendy-lite/). https://github.com/wendylabsinc/wendy-os-publisher Wendy OS Publisher is integrated into WendyOS CI on Github actions, where it uploads as nightly. You can manually run this tool to promote a build to release. ## CI integration ### Nightly builds The nightly pipeline runs all device builds in parallel. The `publish` job: - Always updates the master manifest (runs even if some builds are cancelled). - Creates a GitHub pre-release and sends a Discord announcement **only when all device builds succeed**. If any build fails, neither the pre-release nor the Discord notification fires. - The GitHub pre-release is tagged `nightly` and also tagged `${VERSION}-nightly`. Release notes are auto-generated from PRs and commits since the last `vX.Y.Z` tag (`--generate-notes`). - The Discord announcement is sent via a single `curl`/`jq` step in the `publish` job, not per-device. ### Release builds The release pipeline triggers on `v*` tags. A dedicated `announce` job sends the Discord notification **only after all of the following jobs succeed**: `release`, `publish-linux-repos`, `publish-aur`, and `publish-winget`. If any of those jobs fails, the Discord announcement does not fire. The `announce` job also only runs for stable (non-prerelease) builds. - A GitHub release is created by the `release` job (idempotent — skipped if the tag already has a release). - The Discord announcement uses `jq` to build the webhook payload (preventing JSON injection) and validates that the webhook URL matches the Discord API domain before use. It retries up to three times on failure; if all attempts fail the release is still considered successful and the notification is silently skipped. - The Discord webhook is invoked once from the `announce` job, not per-device. --- # Config Partition Source: https://docs.wendy.dev/latest/advanced/wendyos/config-partition/ --- title: "Config Partition" description: "Every WendyOS image includes a small FAT32 partition labelled config, mounted at /config on the device. It is readable and writable on Linux, macOS, and Windows without any additio" --- # Config Partition Every WendyOS image includes a small FAT32 partition labelled `config`, mounted at `/config` on the device. It is readable and writable on Linux, macOS, and Windows without any additional drivers or tools — including immediately after `dd`-ing an image onto an SD card or NVMe drive. ## Why it exists The root filesystem on a WendyOS device is replaced wholesale during OTA updates. Anything written to `/` will be overwritten when an update is applied. The `config` partition is outside the update boundary. Files placed there survive OS updates and are available from the very first boot. This makes it the right place for anything that needs to be pre-staged before a device has ever booted, or preserved across updates. ## Flashing an enrolled device Valid first-boot provisioning after a flash replaces prior pending or completed local unenrollment recovery. Flashing does **not** revoke certificates or delete the old Cloud asset. Clean up that old Cloud identity separately; do not use a newly provisioned device's identity as proof that the prior one was unenrolled. ## Accessing it from a host computer Because the partition is FAT32, your computer mounts it automatically when you plug in the storage. - **macOS / Linux:** appears as a volume labelled `config`. On macOS, `wendy install` writes provisioning through this auto-mount directly. - **Windows:** appears as a drive labelled `config` Write files there, eject, and they are available at `/config` on the device on next boot. This applies to the boards that install from a disk image (Raspberry Pi, x86). The boards flashed over USB never present their storage to the host, so `wendy install` writes a FAT32 image instead and programs it like any other partition: | Board | How the partition is filled at install time | | --- | --- | | Raspberry Pi, x86 | Mounted on the host after the image is written | | Jetson Thor, Orin | The flashpack's `config-partition.fat32.img` is injected, then flashed | | Dragonwing IQ-8275, IQ-9075 | The bundle ships no config image, so wendy builds one on the host and programs it over EDL | | Arduino UNO Q | As on Dragonwing: wendy builds the image on the host and programs it over EDL | On Dragonwing the partition is created by a first-boot service rather than by the image build, so it stands down when it already finds a FAT32 filesystem labelled `config` — otherwise it would reformat the seed away. ## Accessing it on the device The partition is mounted at `/config` on every boot (fstab `nofail`, so a missing or unformatted partition is not fatal): ```sh ls /config ``` ## wendy-agent self-update If a file named `wendy-agent` is present in `/config` on boot, the agent validates it (must be a 64-bit ELF binary for the device's architecture) and, if valid, installs it over **the currently running agent binary** — `/opt/wendyos/bin/wendy-agent` on a stock image — then exits so systemd restarts it with the new binary. The file is deleted from `/config` regardless of outcome, so it is only applied once. The install is refused if the target directory sits on a merged systemd-sysext overlay. A binary written there is discarded when the overlay is rebuilt on the next boot, so the seed would appear to succeed, silently do nothing, and retry forever. `wendy install` writes the latest stable arm64 agent binary to the config partition automatically after flashing. If this write fails, `wendy install` warns rather than aborting (unless WiFi, device-name, or pre-enroll provisioning was explicitly requested — then the install exits non-zero, see [wendy.conf](#wendyconf)) — the device still boots using the agent baked into the image and fetches updates after first boot. ## Pre-provisioning (`provisioning.json`) Enrollment prepared during installation can be handed to the agent through `/config/provisioning.json`. At boot, the agent validates the handoff, then persists its private key as `/etc/wendy-agent/device-key.pem` before writing `/etc/wendy-agent/provisioning.json` with the enrollment details and certificates. Both destination files use mode `0600`; the destination JSON excludes the handoff's embedded `keyPem` field. On the same boot, the agent replaces `/etc/wendyos/device-uuid` with the device UUID in the handed-off certificate and rewrites the mDNS `id=` TXT record to match; see [Configuration files](services/configuration-files.md). After both writes succeed, the agent removes the handoff file. A persistence failure leaves it available for retry on a later boot, and retrying preserves destination files whose contents and permissions already match. Malformed or incomplete handoffs are logged and removed. See [Credential persistence](../pki/README.md#credential-persistence) for the stored file layout and legacy-file handling. This UUID/mDNS alignment also runs after live re-enrollment. The `_wendyos._udp` service's `id=` TXT record is updated before Avahi restarts. If neither a non-empty explicit hostname nor a device name exists, the agent first saves its current hostname in `/etc/wendy-agent/hostname` so changing the UUID does not rename the device at the next boot. A legacy asset certificate has no device UUID and leaves these records unchanged. ## Driver add-ons (`extensions.json`) A file named `extensions.json` in `/config` seeds kernel driver add-ons at first boot, so a device can come up with an out-of-tree driver it needs to reach the network at all. It is a JSON array: ```json [ { "name": "your-driver", "artifact_url": "https://example.com/your-driver.raw", "kernel_version": "6.18.33-v8-16k", "sha256": "9668b870...", "signature": "", "modules_load": ["your_driver"] } ] ``` Only `name` and `artifact_url` are required. `modules_load` overrides the module list the image declares for itself, and is normally omitted. The file is **deleted before any entry is applied**, not after. A bad or unreachable URL therefore cannot wedge every subsequent boot, and a power cut mid-fetch leaves nothing to retry. The whole batch is bounded at 90 seconds, of which up to 15 are spent waiting for the network link, so a slow registry cannot stall startup. Anything dropped is recoverable afterwards with `wendy device drivers install`. > **Seeding is inert until a signing key ships.** Unlike a CLI install, the seed path > requires a verified signature and has no operator to accept the risk. The embedded > key is currently an empty placeholder, so every entry is refused with > `cannot be authenticated (no signing key embedded)`. See > [Managing drivers](../../../device/managing-drivers/). ## wendy.conf `wendy.conf` is an INI-format file the agent reads on first boot to configure the device. If present, the agent applies its contents and then **deletes the file** so settings are not re-applied on subsequent boots. ### Format ```ini [wifi] ssid = MyNetwork password = hunter2 ``` The `[wifi]` section connects the device to a WiFi network. `password` may be omitted for open networks. ### How it is applied On every boot, `wendy-agent` checks for `/config/wendy.conf`. If found: 1. If `[wifi]` contains a non-empty `ssid`, runs `nmcli device wifi connect [password ]` to connect and create a persistent NetworkManager profile. 2. Deletes `/config/wendy.conf` — even on failure, so bad credentials are not retried on every boot. `wendy install` writes this file automatically when you supply WiFi credentials (via `--wifi-ssid` / `--wifi-password` flags or the interactive prompt). If writing fails, `wendy install` warns and (on an interactive terminal) offers to retry. If the write never succeeds and you explicitly requested WiFi or device-name configuration, the install exits non-zero — the OS image itself is already on the drive, and the error message says so, so you can re-run `wendy install` or configure the device after it boots. ## Size The partition is **64 MB** by default. This is intentionally small — it is for configuration, not application data or logs. Large or frequently-written data belongs on the data partition (`/data`). The size can be adjusted at build time via `WENDYOS_CONFIG_PART_SIZE_MB` in your distro or `local.conf`. --- # Flashing WendyOS Source: https://docs.wendy.dev/latest/advanced/wendyos/flashing/ --- title: "Flashing WendyOS" description: "The wendy install command writes a WendyOS image to a drive (SD card, USB, NVMe enclosure, etc.)." --- # Flashing WendyOS The `wendy install` command writes a WendyOS image to a drive (SD card, USB, NVMe enclosure, etc.). ## Usage ```bash wendy install --device-type --drive ``` Use `wendy os list-drives` to enumerate available drives. ## Migrating from WendyOS < 0.17.0 WendyOS 0.17.0 introduces a new OTA update system that is not backward-compatible with pre-0.17.0 images. Devices running an older image cannot receive OTA updates; `wendy os update` and `wendy device update` refuse the OS step and print guidance to reflash. Perform a one-time reflash with `wendy install` (or `wendy os install`) to migrate the device to the new update system. Once reflashed, OTA updates work normally. ## How it works `wendy install` downloads the WendyOS release zip (~5.5 GB) for the selected device type and writes it directly to the target drive. The compressed zip is never fully extracted to disk — the image entry is streamed from the zip to the drive in a single pass, so the peak temporary disk usage is the zip file itself. ### Block map acceleration When a block map (`.bmap`) and seekable-zstd image (`.img.zst`) are available for the target storage variant, `wendy install` uses them to flash significantly faster: - Only mapped (non-zero) ranges are written; holes are skipped entirely - The seekable zstd format allows random access, so zero regions are never decompressed - Typical Jetson images (~19 GB uncompressed, ~4 GB mapped data) flash in a fraction of the time Use `--no-bmap` to disable this optimization and flash the full image. Use `--storage nvme` or `--storage sd` to force a specific storage variant when the auto-detection is incorrect (e.g., an NVMe drive in a USB enclosure). ### Block map error handling If a bmap-accelerated write fails — for example due to a checksum mismatch or a stale/incorrect published bmap — `wendy install` automatically falls back to a full sequential write using the already-cached `.img.zst` or `.zip`. No user action and no re-download are required. A failure that occurs *during* the fallback write (short write, decode error, or a non-zero helper exit) is fatal: the helper's stderr is surfaced as the error. ### Drive-size check `wendy install` refuses a target drive that cannot hold the image: ``` sdb (/dev/sdb) is too small for this image: it holds 7.9 GB but the image needs 18.2 GB; use a larger SD card or drive ``` For an image with a GPT partition table, such as the Jetson NVMe images, the drive must hold the image's partitions plus the backup partition table, not the whole image. These images declare a disk far larger than their partitions use (the Jetson Orin Nano NVMe image declares 251 GB, but its partitions end at 27.6 GB, and `/data` grows to fill the drive on first boot). On a smaller drive, `wendy install` skips the empty tail of the image and then rewrites the partition table for the drive's real size, as `sgdisk -e` would. It does the same rewrite on a larger drive, so the partition table always matches the drive it is on. It refuses the drive if any partition would be cut off. Images without a GPT, such as the Raspberry Pi images, still need a drive at least as large as the whole image. When a block map is published, it is checked before the Wi-Fi, device-name and pre-enrollment prompts and before the download, so an undersized drive is rejected up front. For a GPT image this early check can only tell where the image's data ends ("needs at least …"). The exact check runs before writing, against the image's partition table, or against its size when it has none. It is skipped when the drive or image size cannot be determined. ### Image formats Both raw (`.img`) and gzip-compressed (`.img.gz`) images are supported, in addition to zip archives. Gzip content is detected by inspecting the file's magic bytes rather than its extension, so a cached or renamed image without a `.gz` suffix is still handled correctly. Gzip images are decompressed on the fly and streamed straight to the drive — the full decompressed image is never written to a temporary file. ### Caching The downloaded zip is cached locally. On subsequent installs for the same device type the cached zip is used directly, avoiding a repeat download. Legacy `.img` cache entries from older versions of the tool are still recognised as a fallback. Use `wendy os download` to pre-populate the cache without performing an install. ### Progress While writing, the tool reports bytes written to the drive. There is no separate "Extracting image…" step. ## Disk usage | Scenario | Peak temporary disk usage | Cache at rest | |---|---|---| | First install | ~5.5 GB (zip only) | ~5.5 GB `.zip` | | Cached install | negligible | ~5.5 GB `.zip` | ## Platform notes ### macOS The image is written via `dd` to the raw disk device (`/dev/rdiskN`), bypassing the filesystem buffer cache. ### Linux Before writing, all mounted partitions on the target disk are unmounted automatically. If any partition cannot be unmounted, the error is reported and the write does not proceed. The image is written via `dd` with `conv=fdatasync` to ensure the device is flushed before the command exits. ### Windows The image is written directly to the raw disk device. After writing, any auto-assigned drive letters are removed from all partitions to prevent phantom drives from appearing in Explorer. For fixed (non-removable) disks, the disk is then taken offline. For removable media (USB, SD, MMC), the offline step is skipped — Windows does not support setting removable media offline, and physically removing the media serves as the eject action. ## Errors On macOS and Linux, if `dd` exits with a non-zero status its stderr output (the underlying cause, e.g. "Operation not permitted" or "No space left on device") is captured and included in the error reported to you, rather than just the exit code. Continuous `status=progress` output is filtered out and the captured diagnostics are bounded, so the message stays focused on the actual failure. --- # Jetson Source: https://docs.wendy.dev/latest/advanced/wendyos/jetson/ --- title: "Jetson" description: "WendyOS supports the following NVIDIA Jetson targets:" --- # Jetson WendyOS supports the following NVIDIA Jetson targets: | Board | Machine config | SoC | JetPack / L4T | OTA | Yocto series | |---|---|---|---|---|---| | Jetson AGX Orin DevKit (NVMe) | `jetson-agx-orin-devkit-nvme-wendyos` | tegra234 | JetPack 7.2 / L4T 39.2.0 | wendyos-update | blacksail | | Jetson AGX Orin DevKit (eMMC) | `jetson-agx-orin-devkit-emmc-wendyos` | tegra234 | JetPack 7.2 / L4T 39.2.0 | wendyos-update | blacksail | | Jetson Orin Nano DevKit (NVMe) | `jetson-orin-nano-devkit-nvme-wendyos` | tegra234 | JetPack 7.2 / L4T 39.2.0 | wendyos-update | blacksail | | Jetson Orin Nano DevKit (SD) | `jetson-orin-nano-devkit-wendyos` | tegra234 | JetPack 7.2 / L4T 39.2.0 | wendyos-update | blacksail | | Jetson AGX Thor DevKit (NVMe) | `jetson-agx-thor-devkit-nvme-wendyos` | tegra264 | JetPack 7.2 / L4T 39.2.0 | wendyos-update | blacksail | --- ## AGX Thor (tegra264) All Jetson boards — Orin (tegra234) and Thor (tegra264) — now build from the **blacksail** Yocto series in a split-poky composition, unified on JetPack 7.2 / L4T R39.2.0. Mender has been removed; all boards use the WendyOS `wendyos-update` OTA stack (`WENDYOS_OTA = "wendy"`). The notes below cover the tegra264-specific (Thor) differences that remain: - **Flash format** — produces `tegraflash-tar` (a compressed tar of the tegraflash package) rather than a separate `tegraflash` artefact. - **Bootloader** — uses NVIDIA prebuilt UEFI firmware (`tegra-uefi-prebuilt`). - **Image name suffix** — oe-core master defaults `IMAGE_NAME_SUFFIX` to `.rootfs`, which would produce deployed symlinks such as `wendyos-image-${MACHINE}.rootfs.tegraflash-tar`. The Thor machine config explicitly sets `IMAGE_NAME_SUFFIX = ""` so the deployed symlink matches the expected name `wendyos-image-${MACHINE}.tegraflash-tar`, consistent with all other boards. To bootstrap a Thor build: ```bash ./bootstrap.sh --board jetson-agx-thor ``` See `conf/template/boards/jetson-agx-thor/` in the `meta-wendyos` repo for the board template files. --- ## Secure boot policy > The notes below describe the **current internal policy**, not a shipped, user-facing secure-boot flow. End-user secure-boot enablement is still planned work; see [WDY-1237](https://linear.app/wendylabsinc/issue/WDY-1237) and [WDY-1238](https://linear.app/wendylabsinc/issue/WDY-1238) for the surrounding security tickets. Burning device fuses to enforce secure boot is an irreversible hardware change. WendyOS will **not** burn fuses on user devices as part of normal flashing. Fuse burning is reserved for sensitive, real-world production deployments where the customer explicitly opts in. Until a full secure-boot flow is shipped, the hardening priorities are: - **Kernel hardening** — _planned, not yet shipped_. - **Removal of SUID binaries from production images** — _planned, not yet shipped_ ([WDY-1238](https://linear.app/wendylabsinc/issue/WDY-1238)). Recent CVEs frequently leverage SUID binaries, so trimming them from production images is a near-term focus. - **Image / binary signing** — _planned, not yet shipped_. Approach differs between Jetson (tegra fuse / NVIDIA tooling) and Raspberry Pi; coordination across boards is open. SSH is already removed from production images. Whether a general-purpose shell can also be removed is being investigated; see [WDY-1238](https://linear.app/wendylabsinc/issue/WDY-1238). --- # WendyOS OTA update protocol Source: https://docs.wendy.dev/latest/advanced/wendyos/mender-update/ --- title: "WendyOS OTA update protocol" description: "GetOSUpdateStatus (agent gRPC API) returns the persisted update record plus, when requested, a live snapshot of the wendyos-update engine (OSUpdateEngineStatus, both v1 and v2 of t" --- # WendyOS OTA update protocol `GetOSUpdateStatus` (agent gRPC API) returns the persisted update record plus, when requested, a live snapshot of the `wendyos-update` engine (`OSUpdateEngineStatus`, both v1 and v2 of the agent proto). ### `diagnostics` (field 6) A `map` of raw, display-only connector diagnostics emitted by `wendyos-update status --json --verbose`. Keys and values are connector-specific and additive: | Connector | Example keys | |---|---| | `tegrauefi` | `RootfsStatusSlotA`, `RootfsStatusSlotB`, EFI boot-chain and capsule variable names | | `ubootenv` | uboot environment variable names | The field is absent when the caller did not request engine status (`include_engine_status: false`). Consumers must treat unknown keys as informational and must not rely on key presence for correctness. --- # WendyOS Networking Overview Source: https://docs.wendy.dev/latest/advanced/wendyos/networking/ --- title: "WendyOS Networking Overview" description: "WendyOS provides a self-contained networking stack designed for USB-tethered connectivity between a device (Raspberry Pi, Jetson, or other supported hardware) and a host computer. " --- # WendyOS Networking Overview WendyOS provides a self-contained networking stack designed for USB-tethered connectivity between a device (Raspberry Pi, Jetson, or other supported hardware) and a host computer. When you plug a WendyOS device into a host via USB-C, the device presents a composite USB gadget that includes both a network interface and a serial console. No drivers, no pairing — the device advertises itself over mDNS and the wendy-agent can discover and connect to it immediately. On Linux hosts, `wendy discover` offers to configure the USB-C link the first time it finds the device. ## Architecture ``` WendyOS device Host computer ─────────────────────────────── ─────────────────────────────── usb0 (NCM gadget interface) ←USB→ enxXXXXXXXXXXXX (CDC NCM) IPv4: DHCP client (or shared) IPv4: 10.42.0.1/24 (NM shared) IPv6: link-local (auto) IPv6: link-local (auto) mDNS: _wendyos._udp (Avahi) mDNS discovery (avahi-browse/dns-sd) /dev/ttyGS0 (ACM serial) ←USB→ /dev/ttyACM0 (serial console) ``` ## Components | Component | Role | Source | |---|---|---| | `gadget-setup.sh` | Configures the USB gadget in configfs at boot | `recipes-core/gadget-setup/` | | systemd-networkd | Manages `usb0` IP configuration | `recipes-core/systemd/` | | dnsmasq | Optional on-device DHCP server for `usb0` | `recipes-support/gadget-network-config/` | | radvd | IPv6 Router Advertisement daemon on `usb0` | `recipes-connectivity/radvd/` | | Avahi | mDNS service broadcasting (`_wendyos._udp`) | `recipes-connectivity/avahi/` | | `generate-hostname.sh` | Sets a stable device hostname before Avahi starts | `recipes-connectivity/avahi/` | | `update-mdns-uuid.sh` | Injects device UUID and name into the Avahi service file | `recipes-core/wendyos-identity/` | ## Topics - [USB NCM Gadget](./usb-ncm.md) — How the USB gadget is constructed and initialized - [MAC Address Generation](./mac-addresses.md) — Deterministic MAC generation and IPv6 link-local formation - [IPv4 DHCP](./ipv4-dhcp.md) — DHCP client and server modes for `usb0`, and host-side setup - [IPv6 Router Advertisements](./ipv6-ra.md) — radvd configuration for IPv6 link-local on `usb0` - [mDNS / Avahi](./mdns.md) — Service broadcasting, hostname generation, and host-side discovery - [Troubleshooting](./troubleshooting.md) — Diagnosing common networking problems ## Key Addresses | What | Value | |---|---| | Device `usb0` IPv4 (DHCP client mode) | Assigned by host (typically `10.42.0.2`–`10.42.0.5`) | | Host IPv4 when using NetworkManager shared | `10.42.0.1/24` | | dnsmasq pool (on-device DHCP server mode) | `10.42.0.2`–`10.42.0.5` | | IPv6 | Link-local only (`fe80::/64`, derived from MAC via EUI-64) | | mDNS service | `_wendyos._udp`, port `50051` pre-provisioning / `50052` post-provisioning | | mDNS hostname | `wendyos-.local` or `wendyos-.local` | ## Platform Notes WendyOS runs on multiple hardware platforms. Networking behaviour is identical across them, but the underlying USB controller differs: | Platform | USB Controller | USB Version | |---|---|---| | Raspberry Pi 5 | `dwc2` | USB 2.0 | | Jetson AGX Orin / Orin Nano | `tegra-xudc` | USB 3.2 | | Other Linux (generic) | Detected at runtime | USB 2.0 fallback | The virtual machine boards (`vm-arm64-wendyos`, `vm-x86-64-wendyos`) do not use USB NCM gadget networking. They emulate a standard virtio network device instead. ## Planned changes (not yet shipped) > The items below describe behaviour that is **not implemented today**. Current builds still rely exclusively on NetworkManager via its `nmcli` CLI for WiFi configuration, and have no built-in mTLS termination on the device side. - **Network Manager and IWD coexistence via auto-detect** ([WDY-1236](https://linear.app/wendylabsinc/issue/WDY-1236)) — _planned, not yet implemented_. Today the agent talks to NetworkManager (`nmcli`) only. The planned change adds IWD (Internet Wireless Daemon) as an alternative. The agent will detect which CLI is present on the running OS and select the matching backend, so devices on older OS builds that ship NetworkManager keep working unchanged. The OS-side change to ship `iwd` lands first; the agent-side switchover is gated on the OS PR. - **Unified TCP-based socket protocol for CLI and cloud** ([WDY-1246](https://linear.app/wendylabsinc/issue/WDY-1246)) — _planned, not yet implemented_. The same low-level TCP socket protocol — messages standardised by size, type, and payload — will back both cloud-to-device and CLI-to-device sessions, with the `wendy` CLI behaving as a local cloud instance. The existing flow (CLI emits a UDP broadcast, the device opens an HTTP connection back) becomes one of the supported transports under that unified surface. - **Mutual TLS for device communication** ([WDY-1245](https://linear.app/wendylabsinc/issue/WDY-1245) covers the Wendy Light provisioning slice) — _planned, not yet implemented_. Sample mTLS socket plumbing exists in a development branch and will be merged once the unified protocol above is in place. The accompanying provisioning improvements (generating private keys and CSRs directly on-device, mDNS-based discovery) follow. --- # IPv4 DHCP Source: https://docs.wendy.dev/latest/advanced/wendyos/networking/ipv4-dhcp/ --- title: "IPv4 DHCP" description: "WendyOS assigns an IPv4 address to usb0 through DHCP. There are two modes depending on how the host is set up." --- # IPv4 DHCP WendyOS assigns an IPv4 address to `usb0` through DHCP. There are two modes depending on how the host is set up. ## Modes ### Mode 1 — DHCP Client (Default) In the default build configuration (`WENDYOS_USB_NET_MODE=dhcp`), WendyOS acts as a DHCP client on `usb0`. The host is expected to run a DHCP server on its NCM adapter. The most common way to do this on Linux is to activate a NetworkManager "shared" connection on the host-side interface, which starts a dnsmasq instance automatically and assigns `10.42.0.1/24` to the host. The NetworkManager connection profile on the device (`/etc/NetworkManager/system-connections/usb-gadget.nmconnection`) sets: ```ini [ipv4] method=@USB_NET_MODE@ # replaced at build time with "dhcp" route-metric=700 # high metric, usb0 is the fallback route ``` The high route metric (`700`) means that Ethernet (`100`) and WiFi (`300`) are always preferred as the default route when available. `usb0` only becomes the default route if no other interface is up. DHCP timeout is set to 300 seconds (`ipv4.dhcp-timeout=300`) to handle hosts that activate sharing after the device has already booted. ### Mode 2 — On-Device DHCP Server (Optional) An alternative configuration ships an on-device dnsmasq instance that serves DHCP directly on `usb0`. This is provided by the `gadget-network-config` Yocto recipe. Configuration file: `/etc/dnsmasq.d/usb-gadget-dnsmasq.conf` ``` interface=usb0 bind-interfaces dhcp-range=10.42.0.2,10.42.0.5,12h dhcp-option=3,10.42.0.1 # gateway = device itself dhcp-option=6,8.8.8.8 # DNS quiet-dhcp quiet-dhcp6 quiet-ra ``` The systemd service `gadget-dnsmasq.service` starts after `gadget-setup.service` and runs dnsmasq with this config. It is type `simple` and restarts on failure. In this mode the host receives an address from `10.42.0.2`–`10.42.0.5` and the device is the gateway at `10.42.0.1`. This requires the device to be configured with a static `10.42.0.1/24` address on `usb0`, which is done by setting `WENDYOS_USB_NET_MODE=shared` at build time. ## NetworkManager Configuration Files | File | Purpose | |---|---| | `/etc/NetworkManager/system-connections/usb-gadget.nmconnection` | Per-interface connection profile for `usb0` | | `/etc/NetworkManager/conf.d/10-usb-gadget.conf` | Ensures `usb0` is managed; sets `ipv6.method=link-local` | | `/etc/NetworkManager/conf.d/00-manage-usb0.conf` | Explicitly clears `unmanaged-devices` so NM manages all interfaces | | `/etc/NetworkManager/conf.d/99-interface-metrics.conf` | Sets route metrics: ethernet=100, wifi=300, usb=700 | ## Host-Side Setup ### Easiest — let `wendy discover` set it up (Linux) On a Linux host, `wendy discover` auto-detects a USB-C-tethered Wendy device whose host link isn't configured and offers to set it up for you. Accept the prompt and enter your sudo password; the CLI brings the gadget interface up via a link-local NetworkManager profile (IPv4 `169.254.0.0/16` and IPv6 `fe80::/64`, no DHCP, matching the device's default link-local mode) and installs a udev rule so ModemManager stops grabbing the gadget's serial console. The profile is named `wendy-usb-` and bound to the gadget's MAC address rather than an interface name, so it keeps working when you plug the device into another USB port. Each device gets its own profile. To undo it later, remove the profile and rule manually: ```sh nmcli -f NAME connection show | grep wendy-usb # find the profile sudo nmcli connection delete wendy-usb- sudo rm -f /etc/udev/rules.d/99-wendy-usb.rules ``` The CLI setup does not share the host's internet with the device. For that, use the manual shared-mode steps below. ### Mode 1 host setup — NetworkManager shared Run on the host once the NCM interface (`enxXXXXXXXXXXXX`) appears: ```sh nmcli connection add type ethernet ifname ipv4.method shared ``` This activates a built-in dnsmasq instance, assigns `10.42.0.1/24` to the host, and enables NAT so the device can reach the internet through the host. ### Alternative — link-local addressing The `setup-host-usb-link-local.sh` script in the `wendyos` repository configures the host for link-local addressing instead. This is useful when internet sharing is not needed. It supports both NetworkManager and systemd-networkd: **NetworkManager** — installs `/etc/NetworkManager/system-connections/wendyos-usb.nmconnection`: ```ini [connection] type=ethernet autoconnect-priority=100 match.driver=cdc_ncm;cdc_ether [ipv4] method=link-local ``` **systemd-networkd** — installs `/etc/systemd/network/80-wendyos-usb.network`: ```ini [Match] Driver=cdc_ncm cdc_ether [Network] LinkLocalAddressing=ipv4 ``` ## Verifying DHCP ```bash # On the device — check address assigned to usb0 ip addr show usb0 # On the device — check NetworkManager state nmcli connection show usb-gadget | grep -E "ipv4|IP4" # On the device — watch DHCP exchange in journal journalctl -u NetworkManager | grep -i "usb0\|dhcp" # On the host — capture DHCP packets sudo tcpdump -i enxXXXXXXXXXXXX port 67 or port 68 -vn ``` A successful DHCP exchange looks like: ``` DISCOVER → (device to host, broadcast) ← OFFER (host to device) REQUEST → (device to host) ← ACK (host to device) ``` --- # IPv6 Router Advertisements (radvd) Source: https://docs.wendy.dev/latest/advanced/wendyos/networking/ipv6-ra/ --- title: "IPv6 Router Advertisements (radvd)" description: "WendyOS runs radvd (Router Advertisement Daemon) on usb0 to signal IPv6 availability on the USB NCM interface. No DHCPv6 server is used." --- # IPv6 Router Advertisements (radvd) WendyOS runs `radvd` (Router Advertisement Daemon) on `usb0` to signal IPv6 availability on the USB NCM interface. No DHCPv6 server is used. ## What radvd Does Here Hosts self-assign a link-local address on any interface as soon as it comes up (RFC 4862 §5.3) — no Router Advertisement is required for that. In WendyOS, radvd's role is narrower: it sends periodic RAs with `AdvDefaultLifetime 0` (the device is not a default router) to signal to the host that IPv6 is active on the NCM interface. This ensures platforms that delay IPv6 processing until an RA is received (notably macOS) bring up their `fe80::` address promptly. The `fe80::/64` prefix entry in the config with `AdvAutonomous on` is a no-op for SLAAC — RFC 4861 §5.5.3 excludes link-local prefixes from prefix information processing — but is kept for compatibility with strict radvd configurations. ## Configuration File: `/etc/radvd.conf` (source: `recipes-connectivity/radvd/files/radvd.conf`) ``` interface usb0 { AdvSendAdvert on; MinRtrAdvInterval 3; MaxRtrAdvInterval 10; AdvDefaultPreference low; # Do not advertise as default router AdvDefaultLifetime 0; # SLAAC, not DHCPv6 AdvManagedFlag off; AdvOtherConfigFlag off; prefix fe80::/64 { AdvOnLink on; AdvAutonomous on; AdvRouterAddr on; }; }; ``` Key settings explained: | Setting | Value | Reason | |---|---|---| | `AdvSendAdvert on` | — | Enable RA sending | | `MinRtrAdvInterval` | 3 s | Frequent RAs for fast host configuration after cable insert | | `MaxRtrAdvInterval` | 10 s | Upper bound; low to keep the host synchronized | | `AdvDefaultLifetime` | 0 | The device is not an IPv6 default router | | `AdvDefaultPreference` | low | Deprioritise in multi-router environments (e.g. macOS ICS) | | `AdvManagedFlag off` | — | SLAAC, no DHCPv6 | | `AdvOtherConfigFlag off` | — | No stateless DHCPv6 for DNS etc. | | prefix `fe80::/64` | `AdvAutonomous on` | No-op for SLAAC (link-local prefixes are excluded by RFC 4861 §5.5.3); kept for radvd config compatibility | ## systemd Service The `radvd.service` generated by the `radvd_%.bbappend` recipe: ```ini [Unit] Description=IPv6 Router Advertisement Daemon After=network-online.target gadget-setup.service Wants=network-online.target Requires=gadget-setup.service [Service] Type=forking ExecStartPre=/bin/sh -c 'test -e /sys/class/net/usb0 || sleep 2' ExecStart=/usr/sbin/radvd -C /etc/radvd.conf ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure ``` The `ExecStartPre` guard waits up to 2 additional seconds if `usb0` does not yet exist; radvd may still fail if the interface is absent after that window. The service is auto-enabled in the WendyOS image. The Yocto recipe adds `kernel-module-ipv6` as a runtime dependency to ensure the IPv6 kernel module is available. ## Kernel IPv6 Forwarding The USB network performance tuning package (`usb-network-tuning`) sets IPv6 forwarding via sysctl: ``` net.ipv6.conf.all.forwarding = 1 net.ipv6.conf.default.forwarding = 1 net.ipv6.conf.usb0.forwarding = 1 ``` This is required for radvd to send RAs (radvd will not send RAs on an interface unless IPv6 forwarding is enabled on it). ## Verifying IPv6 ```bash # On the device — check usb0 link-local address ip -6 addr show usb0 # Check radvd is running systemctl status radvd # On the host — check that a link-local address appeared on the NCM interface ip -6 addr show enxXXXXXXXXXXXX # Ping the device from the host using link-local (zone ID required) ping6 fe80::xxxx:xxxx:xxxx:xxxx%enxXXXXXXXXXXXX # Ping the host from the device ping6 fe80::xxxx:xxxx:xxxx:xxxx%usb0 ``` ## Relationship to mDNS Avahi uses both IPv4 and IPv6 (`use-ipv4=yes`, `use-ipv6=yes`). Once the host has a link-local address on the NCM interface (self-assigned at interface-up, confirmed promptly by the RA), Avahi on the device can resolve the device hostname via its `fe80::` address as well as its IPv4 address. This provides a fallback if DHCP has not yet completed. --- # MAC Address Generation Source: https://docs.wendy.dev/latest/advanced/wendyos/networking/mac-addresses/ --- title: "MAC Address Generation" description: "WendyOS generates stable, deterministic MAC addresses for the USB NCM gadget rather than using random or hardware-burned addresses. This means the host always sees the same MAC for" --- # MAC Address Generation WendyOS generates stable, deterministic MAC addresses for the USB NCM gadget rather than using random or hardware-burned addresses. This means the host always sees the same MAC for a given device, which keeps NetworkManager connection profiles and ARP caches valid across reboots and cable disconnects. ## Why Dynamic Generation? Most embedded hardware does not ship with a burned-in MAC address for the USB gadget interface. Using a random MAC at each boot would cause the host to create a new NetworkManager connection profile on every boot. Deriving the MAC from the device serial ensures stability without requiring provisioning infrastructure. ## Algorithm The function `generate_mac()` in `gadget-setup.sh`: ```bash generate_mac() { local hash hash=$(echo -n "$1" | sha256sum | awk '{print $1}') local mac_base=${hash:0:12} # take first 12 hex chars (6 bytes) local first_byte=${mac_base:0:2} local second_char second_char=$(printf '%x' $((0x$first_byte & 0xfe | 0x02))) printf "%02x:%02x:%02x:%02x:%02x:%02x" \ 0x$second_char \ 0x${mac_base:2:2} \ 0x${mac_base:4:2} \ 0x${mac_base:6:2} \ 0x${mac_base:8:2} \ 0x${mac_base:10:2} } ``` Steps: 1. SHA-256 hash the input string. 2. Take the first 12 hex characters of the digest as the 6-byte MAC base. 3. Modify the first byte: `first_byte & 0xfe | 0x02` - `& 0xfe` — clear bit 0 (unicast, not multicast) - `| 0x02` — set bit 1 (locally administered address) The result is a locally administered unicast MAC, which is the correct form for software-assigned addresses (IEEE 802 bit convention: bit 0 = multicast, bit 1 = locally administered). ## Two MACs Per Device The gadget requires two addresses — one for each end of the link: ```bash mac_host=$(generate_mac "${DEVICE_SERIAL}-host") # assigned to host adapter mac_self=$(generate_mac "${DEVICE_SERIAL}-dev") # assigned to device usb0 ``` The suffixes `-host` and `-dev` ensure the two addresses are always distinct even when the SHA-256 output would otherwise be similar. Both are written into the configfs NCM function: ```bash echo "$mac_host" > functions/ncm.usb0/host_addr echo "$mac_self" > functions/ncm.usb0/dev_addr ``` On the host, the adapter name is derived from `host_addr` by the kernel — typically `enx` followed by the 12 hex digits without colons (e.g., `enxaabbccddeeff`). ## IPv6 Link-Local Address Formation IPv6 link-local addresses on `usb0` are derived automatically from the MAC address. NetworkManager configures `usb0` with `ipv6.method=link-local` and `addr-gen-mode=stable-privacy`. With standard EUI-64 (RFC 4291), the process is: 1. Split the 6-byte MAC at the middle: `AA:BB:CC | DD:EE:FF` 2. Insert `FF:FE` in the middle: `AA:BB:CC:FF:FE:DD:EE:FF` 3. Flip the Universal/Local bit of the first byte: `AA XOR 0x02` (RFC 4291 calls this "bit 6" using MSB-first numbering; IEEE 802 calls it "bit 1" using LSB-first numbering — both refer to the `0x02` mask) 4. Prefix with `fe80::`: `fe80::aabb:ccff:fedd:eeff/64` With `stable-privacy` (RFC 7217), the address is derived from a hash of the interface name, prefix, and a secret — this provides better privacy than EUI-64 while remaining stable for the same interface on the same machine. The exact address differs from raw EUI-64 but is always in the `fe80::/64` range. You can inspect the link-local address on the device: ```bash ip -6 addr show usb0 # Example: inet6 fe80::a4b2:e1ff:fe9d:3c1a/64 scope link ``` WendyOS runs radvd on `usb0` to signal IPv6 availability; the host self-assigns its link-local address independently. See [IPv6 Router Advertisements](./ipv6-ra.md). ## Serial Number Sources The MAC derivation input is the device serial number, resolved through this fallback chain: | Priority | Source | Notes | |---|---|---| | 1 | `/proc/device-tree/serial-number` | RPi device tree serial | | 2 | `/sys/devices/platform/tegra-fuse/uid` | Jetson eFuse UID | | 3 | `awk '/Serial/' /proc/cpuinfo` | Legacy RPi CPU info | | 4 | `eth0` MAC (colons stripped) | Present on all hardware | | 5 | `date +%s` | Last-resort, not stable across reboots | On all production hardware (RPi, Jetson), sources 1–4 are stable across reboots, so the generated MACs are stable. --- # mDNS / Avahi Service Discovery Source: https://docs.wendy.dev/latest/advanced/wendyos/networking/mdns/ --- title: "mDNS / Avahi Service Discovery" description: "WendyOS uses Avahi to broadcast its presence over mDNS (Multicast DNS, RFC 6762) so that the wendy-agent on the host can discover the device without knowing its IP address in advan" --- # mDNS / Avahi Service Discovery WendyOS uses Avahi to broadcast its presence over mDNS (Multicast DNS, RFC 6762) so that the wendy-agent on the host can discover the device without knowing its IP address in advance. The device announces a `_wendyos._udp` service that carries the device UUID, name, and display name as DNS-SD TXT records. ## Service Advertisement The Avahi service definition is installed at `/etc/avahi/services/wendyos-mdns.service`: ```xml %h _wendyos._udp 50051 id=WENDY_DEVICE_ID name=WENDY_DEVICE_NAME displayname=WENDY_DISPLAY_NAME ``` The placeholder tokens (`WENDY_DEVICE_ID` etc.) are replaced during boot. On later boots, the same updater replaces the current TXT record values again; it is not limited to the original placeholders (see below). - **Service type**: `_wendyos._udp` - **Port**: `50051` pre-provisioning; updated to `50052` (mTLS) automatically once the device enrolls - **Instance name**: The device hostname (`%h` expands to the current hostname as set by Avahi) - **TXT records**: `id`, `name`, `displayname`, plus provisioning records such as `tls`, `assetid`, and `orgid` when applicable Provisioning updates the port and may add `tls`, `assetid`, and `orgid`. Some service templates also contain an optional `fqdn` record. The agent preserves these records when it updates the hostname-derived TXT values. The agent also keeps a `wired` record listing the routable IPv4 addresses of its connected Ethernet ports, other than the address it takes on a camera port. It updates the record once a new list has held for two checks 10 seconds apart, or for about a minute when the new list drops an advertised address, and restarts Avahi only when the record changes. ## Boot-time mDNS Identity Update On every boot, `wendyos-identity.service` runs `update-mdns-uuid.sh` from `recipes-core/wendyos-identity/`. The script reads `/etc/wendyos/device-uuid` and `/etc/wendyos/device-name`, derives the title-case display name, and replaces the current `id`, `name`, and `displayname` values in the Avahi service file. Matching records by key makes this update idempotent; it is not a one-time placeholder replacement. The equivalent data flow is: ```bash UUID=$(cat /etc/wendyos/device-uuid) DEVICE_NAME=$(cat /etc/wendyos/device-name 2>/dev/null || echo "unknown-device") DISPLAY_NAME=$(echo "$DEVICE_NAME" | sed 's/-/ /g' | \ awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1') # Replace the value of each matching key=... entry. set_txt id "$UUID" set_txt name "$DEVICE_NAME" set_txt displayname "$DISPLAY_NAME" ``` The `displayname` field converts the hyphen-separated device name to Title Case (e.g. `my-device` becomes `My Device`). If UUID or device-name files are not yet present, the script retries for up to 10 seconds to handle ordering races. After updating the file it calls `avahi-daemon --reload` if the daemon is already running. Enrollment also updates identity before the next boot: when loading pre-provisioned credentials or completing live re-enrollment, the agent writes the certificate's device UUID to `/etc/wendyos/device-uuid` and the service's `id=` TXT record before restarting Avahi. A legacy asset certificate carries no device UUID and leaves these records unchanged. See [Pre-provisioning](../config-partition.md#pre-provisioning-provisioningjson) for hostname preservation. `wendy device rename` persists the operator-selected hostname in `/etc/wendy-agent/hostname` and updates the Avahi records at rename time; it does not overwrite the generated `/etc/wendyos/device-name`. Consequently, the boot-time identity update above can replace `name` and `displayname` with the generated device name. At agent startup, after `wendyos-identity.service` and `avahi-daemon.service`, `ReassertHostnameAdvertisement` compares the service file with the persisted explicit hostname and restores the hostname-derived TXT records when needed. It restarts Avahi only when it changes the service file. The same re-assertion repairs a fresh service file after an A/B OTA slot switch. ## Hostname Generation Avahi broadcasts the device hostname as `.local`. The hostname is set by `generate-hostname.sh` via `wendyos-hostname.service`, which runs before `avahi-daemon.service`: **Resolution order:** 1. `/etc/wendy-agent/hostname` — literal hostname set via `wendy device rename` (no prefix) 2. `/etc/wendyos/device-name` — use as-is, lowercased, prefixed with `wendyos-` → `wendyos-.local` 3. `/etc/wendyos/device-uuid` — take the last 8 hex characters (without dashes) → `wendyos-<8-char-uuid-suffix>.local` 4. Legacy fallbacks: RPi serial from `/proc/cpuinfo`, first 16 chars of `/etc/machine-id`, first MAC address, random hex The hostname is written to `/etc/hostname` using a direct write (not `hostnamectl`, to avoid EBUSY issues with bind mounts) and also added to `/etc/hosts` as `127.0.1.1 .local`. > **Rename persistence:** The hostname itself remains stable across reboots and > OTA updates because `/etc/wendy-agent/hostname` is backed by `/data` and has > highest precedence above. The Avahi `name` and `displayname` TXT records are > separate rootfs state and can be re-derived from `/etc/wendyos/device-name` > during boot. The agent re-asserts those records from the persistent explicit > hostname at startup and does nothing on devices that have never been renamed > or whose records are already current. A hostname override can be applied by creating `/etc/wendyos-hostname-override`. ## Avahi Configuration The `avahi_%.bbappend` recipe configures `/etc/avahi/avahi-daemon.conf` at build time: ``` enable-dbus=yes enable-reflector=no # reflector wedges startup in avahi 0.8 (bug WDY-755) use-ipv4=yes use-ipv6=yes publish-addresses=yes publish-hinfo=yes publish-workstation=yes publish-domain=yes ``` The reflector is explicitly disabled. A bug in avahi 0.8 causes the daemon to hang during startup when the reflector is enabled, regardless of the number of interfaces — services never get published. NSS is configured to resolve `.local` names via mDNS by appending to `nsswitch.conf`: ``` hosts: files mdns4_minimal [NOTFOUND=return] mdns4 dns ``` ## Host-Side Discovery ### Linux (avahi-browse) ```bash # One-time scan avahi-browse -t -p _wendyos._udp # Continuous with full resolution avahi-browse -r _wendyos._udp # Example output: # =;enx...;IPv4;wendyos-my-device;_wendyos._udp;local;wendyos-my-device.local;10.42.0.2;50051;"id=xxxxxxxx-..." ``` ### macOS (dns-sd) ```bash # Browse dns-sd -B _wendyos._udp local. # Resolve an instance dns-sd -L "wendyos-my-device" _wendyos._udp local. ``` ### wendy-agent Discovery Code The wendy-agent Go code (`internal/shared/discovery/`) uses `_wendyos._udp` as the service type constant. On Linux it browses through the Avahi daemon's D-Bus API (`org.freedesktop.Avahi` on the system bus) — no `avahi-browse` child process is spawned; it falls back to the `hashicorp/mdns` library, which queries each multicast-capable interface individually, only when the system bus or the Avahi service itself cannot be reached. On macOS it browses and resolves in-process through ``, the same mDNSResponder daemon that the `dns-sd` tool wraps, so no helper processes are spawned. **CLI-side note:** The shipped CLI binary is built with `CGO_ENABLED=0`, so it cannot use nss-mdns to resolve `.local` names. Instead, it performs its own mDNS browse for `.local` hostnames when connecting. Set `WENDY_MDNS_DEBUG=1` to log browse failures, or `WENDY_MDNS_TIMEOUT` (1s–30s) to adjust the timeout. Both the primary Avahi D-Bus path and the `hashicorp/mdns` fallback path parse all TXT records into a key→value map, including the `tls` record. TXT records are **unauthenticated**: they are advertised by whatever host answers on the network, and anyone who can reach the multicast segment can advertise `_wendyos._udp` with any TXT values it likes. They are used for **routing and display only**, never for trust. A device that advertises `tls=true` has `IsMTLS` set to `true`, which tells the CLI to dial the mTLS port (50052) instead of the plaintext one — that arithmetic is real and still applies — but it is not what decides whether the CLI trusts the host it ends up talking to. Trust is established afterward, from the certificate that host presents during the TLS handshake, checked against the identity pin recorded for the name being dialed (see the "Device identity pinning" section of `pki/README.md`). A spoofed TXT record can get a connection attempt redirected to the wrong port on the wrong host; it cannot get that host trusted — the certificate check and pin still have to pass. The fallback path also resolves `wendyosdevice` and `displayname` TXT records, matching the behaviour of the primary path. IPv6 link-local addresses returned by mDNS are annotated with the zone ID (`%`) so that the caller can use them directly in `net.Dial()` calls. The `MDNSService` struct captures: ```go type MDNSService struct { InstanceName string Hostname string IPAddress string // link-local IPv6 includes %zone suffix Port int TXTRecords map[string]string // all advertised TXT records } ``` ## TXT Record Reference | Key | Value | Example | |---|---|---| | `id` | Device UUID (from `/etc/wendyos/device-uuid`; once enrolled, the UUID in the device certificate) | `550e8400-e29b-41d4-a716-446655440000` | | `name` | Slug device name (from `/etc/wendyos/device-name`) | `my-device` | | `displayname` | Title-cased device name | `My Device` | | `fqdn` | Optional reverse-DNS name derived from the hostname when present in the service template | `sh.wendy.my-device` | | `wendyosdevice` | Device UUID (preferred over `id` when present) | `769dc651-4eb2-49f3-b9f6-3e473f15694a` | | `tls` | `"true"` when the device is provisioned and requires mTLS (port 50052) | `true` | | `wired` | Comma-separated IPv4 addresses of the device's connected Ethernet ports (at most 8); absent when none. Used only to choose a route, never to trust a device | `192.168.2.50` | ## Connect Wendy Net devices over Ethernet or Wi-Fi Devices enrolled in the same organization can find one another on a local Ethernet or ordinary Wi-Fi network. Run this on each device you want to connect: ```sh wendy device wendy-net configure --participate=true --ethernet=true --infrastructure-wifi=true wendy device wendy-net status wendy device wendy-net topology ``` Add `--device ` to select a particular device. Check topology for connected peers; saving settings alone does not confirm connectivity. Omitted flags preserve saved choices. Set a flag to `false` to disable that connection type. Changing saved settings restarts local routing and can interrupt traffic. Ethernet is preferred over ordinary Wi-Fi, followed by the radio connections. If a connection disappears, another enabled connection can take over. Existing TCP sessions may survive a short outage; new connections wait for a valid route. ### Network requirements and privacy Allow local multicast discovery on UDP port 5353 and encrypted peer connections on UDP port 43023. Devices need usable IPv4 addresses; isolated Wi-Fi clients and multicast filtering can prevent discovery. Container bridges, access-point interfaces, and IPv4 link-local addresses are not used. Discovery advertises device and organization identifiers in cleartext on the local network. Enable it only where that disclosure is acceptable. A discovered address is a hint: each peer must still authenticate before carrying traffic. The discovery service is `_wendy-net._udp`; peer traffic uses QUIC over UDP. There is no TCP fallback on these LAN connections. --- # Networking Troubleshooting Source: https://docs.wendy.dev/latest/advanced/wendyos/networking/troubleshooting/ --- title: "Networking Troubleshooting" description: "This guide covers common WendyOS USB networking problems, their symptoms, and how to diagnose and fix them." --- # Networking Troubleshooting This guide covers common WendyOS USB networking problems, their symptoms, and how to diagnose and fix them. ## Connectivity Checklist Run through this before diving into specific scenarios: ``` [ ] USB-C data cable connected (not a charge-only cable) [ ] Board booted — serial console available: screen /dev/ttyACM0 [ ] Host sees gadget interface: ip link show | grep enx [ ] Host sharing / link-local active: ip addr show enxXXXXXXXXXXXX [ ] Device has address on usb0: ip addr show usb0 (run on device) [ ] Gadget service healthy: systemctl status gadget-setup [ ] ARP resolved: ip neigh show dev usb0 (on device) [ ] Ping host from device: ping -c 3 10.42.0.1 [ ] Ping device from host: ping -c 3 [ ] mDNS visible from host: avahi-browse -t -p _wendyos._udp ``` --- ## Scenario 1 — Host does not see the gadget interface (`enx` interface missing) **Symptom:** `ip link show | grep enx` produces no output after plugging in. **Step 1 — check if the USB device is enumerated at all:** ```bash # On host lsusb | grep "1d6b:0104" # Expected: Bus 00X Device 00Y: ID 1d6b:0104 Linux Foundation Multifunction Composite Gadget ``` If the device is not listed: - Try a different cable (data cable, not charge-only) - Try a different USB port - Check if the device has booted — connect via a USB-C power supply and watch the serial console **Step 2 — if lsusb shows the device but no network interface:** ```bash # On host dmesg | grep -i "cdc_ncm\|cdc_ether\|usb" ``` Look for driver binding messages. If you see `cdc_ncm` being loaded, the interface should appear. If not, the module may not be available or another driver grabbed the device. **Step 3 — check gadget-setup status on the device (via serial console):** ```bash # On device (/dev/ttyACM0) systemctl status gadget-setup journalctl -u gadget-setup --no-pager ``` Common gadget-setup failures: - "UDC timeout after 60 s" — the UDC module has not loaded; check `lsmod | grep -E "tegra_xudc|dwc2|dwc3"` - "Neither NCM nor ECM is available" — gadget function modules failed to load; check `dmesg | grep usb_f_ncm` --- ## Scenario 2 — Device gets no IPv4 address **Symptom:** `ip addr show usb0` shows only an IPv6 link-local address, no IPv4. **Step 1 — verify the host is providing DHCP:** ```bash # On host ip addr show enxXXXXXXXXXXXX # should show 10.42.0.1/24 if NM sharing is active nmcli connection show | grep -i usb ``` If the host is not set up for sharing, the simplest fix on Linux is to run `wendy discover` and accept the offered USB setup — it configures a link-local NetworkManager profile bound to the gadget's MAC (so it survives a change of USB port) and installs a udev rule so ModemManager leaves the gadget alone. It does not share internet; use the NM shared profile below for that. The equivalent manual steps: ```bash sudo ./scripts/setup-host-usb-link-local.sh install # Or for full internet sharing (NM): nmcli connection add type ethernet ifname enxXXXXXXXXXXXX con-name usb-gadget-sharing ipv4.method shared nmcli connection up usb-gadget-sharing ``` To remove what the CLI installed: `nmcli connection delete wendy-usb-` and `sudo rm -f /etc/udev/rules.d/99-wendy-usb.rules`. **Step 2 — check NetworkManager on the device:** ```bash # On device journalctl -u NetworkManager | grep -i "usb0\|dhcp" nmcli connection show usb-gadget | grep -E "ipv4|IP4|STATE" ``` **Step 3 — force a new DHCP attempt:** ```bash # On device nmcli connection down usb-gadget && nmcli connection up usb-gadget ``` **Step 4 — capture DHCP traffic:** ```bash # On host sudo tcpdump -i enxXXXXXXXXXXXX port 67 or port 68 -vn ``` Expected: `DISCOVER → OFFER ← REQUEST → ACK ←` If you see repeated DISCOVERs with no OFFER: the host DHCP server is not active or bound to a different interface. If you see OFFER but no REQUEST: the device is not receiving the OFFER — this is often a receive-path problem (see Scenario 3). --- ## Scenario 3 — IP assigned but packets lost (usb0 has address, ping fails) **Symptom:** `usb0` has an IP address but `ping 10.42.0.1` gets 100% packet loss. **Step 1 — check RX/TX counters:** ```bash # On device ip -s link show usb0 ``` If TX is in the hundreds but RX is near zero, the USB receive path is broken. This is characteristic of the DWC2 DMA stall bug on RPi5 (see below). **Step 2 — check ARP resolution:** ```bash # On device ip neigh show dev usb0 # REACHABLE = ARP is working # INCOMPLETE = ARP is stuck; the device sent a request but got no reply back # On host — watch ARP sudo tcpdump -i enxXXXXXXXXXXXX arp -n ``` If the host sends ARP Replies but the device still shows INCOMPLETE, packets are being dropped between the USB layer and the network stack. **Step 3 — check packet filters:** ```bash # On device nft list ruleset iptables -L -n -v sysctl net.ipv4.conf.usb0.rp_filter ``` A non-zero `rp_filter` can drop packets if the route is asymmetric. **Step 4 — inspect the USB endpoint (RPi5/DWC2 specific):** ```bash # On device cat /sys/kernel/debug/usb/udc/1000480000.usb/ep1out ``` Healthy output has `NAKSts=0` and requests completing with non-zero `total_data`. Broken output (DMA stall): ``` NAKSts=1 # hardware is NAKing all OUT tokens from host DOEPDMA=0xXXXXXXXX # DMA address programmed but never completing req: ... EINPROGRESS total_data=0 # all requests stuck ``` **Root cause (RPi5, kernel 6.6, BCM2712):** BCM2712 reports `arch=2` (DMA capable) but DMA completion interrupts never fire for OUT transfers in peripheral mode. Fix: `g_dma=false` in `dwc2_set_bcm_params()` — this patch is applied in `meta-rpi-extensions/`. **Do not write `""` to the UDC sysfs file on an unpatched board** — this triggers `ep_stop_xfr` timeouts and leaves Global OUT NAK permanently asserted. Reboot instead. --- ## Scenario 4 — mDNS device not visible on host **Symptom:** `avahi-browse -t -p _wendyos._udp` returns no results. **Step 1 — check Avahi is running on the device:** ```bash # On device systemctl status avahi-daemon journalctl -u avahi-daemon --no-pager | tail -30 ``` **Step 2 — check that the service file was populated:** ```bash # On device cat /etc/avahi/services/wendyos-mdns.service ``` The file should contain the actual UUID, not the placeholder string `WENDY_DEVICE_ID`. If placeholders are present, `update-mdns-uuid.sh` did not run or failed: ```bash journalctl -t wendyos-identity --no-pager ls /etc/wendyos/device-uuid /etc/wendyos/device-name ``` **Step 3 — check UUID and hostname services:** ```bash # On device systemctl status wendyos-hostname.service systemctl status wendyos-uuid-generate.service # if it exists on this build ``` **Step 4 — check mDNS multicast is reaching the host:** ```bash # On host sudo tcpdump -i enxXXXXXXXXXXXX udp port 5353 -vn ``` You should see mDNS queries and responses. If you see nothing, multicast may be blocked on the link. **Step 5 — test with avahi-browse on the device itself:** ```bash # On device avahi-browse -t _wendyos._udp ``` If the device sees its own service, the problem is on the host side. Check that `avahi-daemon` or `dns-sd` is running on the host and that the NCM interface is not isolated from multicast. --- ## Scenario 5 — NM stale connection profile on host **Symptom:** ``` Warning: There is another connection with the name 'usb-gadget-sharing'. Error: Connection activation failed: No suitable device found for this connection (device docker0 not available ...) ``` A previous NetworkManager profile is left over and matches the wrong interface. **Fix:** ```bash # Delete all stale profiles with that name nmcli -g UUID,NAME connection show | grep ':usb-gadget-sharing$' | \ cut -d: -f1 | xargs -I{} sudo nmcli connection delete {} # Then re-enable sudo nmcli connection add type ethernet ifname enxXXXXXXXXXXXX \ con-name usb-gadget-sharing ipv4.method shared sudo nmcli connection up usb-gadget-sharing ``` --- ## Scenario 6 — gadget-setup fails on Jetson (UDC not found / stays in default state) **Symptom:** `journalctl -u gadget-setup` shows the UDC was found but the gadget does not appear on the host, or the role-switch warning is logged. Jetson relies on the FUSB301 CC chip and the `tegra_xudc` driver. If the padctl role-switch notifier chain has not fired before `gadget-setup.sh` runs, the UDC stays in "default" state and never enters device mode. The script attempts to force device mode: ```bash echo "device" > /sys/class/usb_role/usb2-0-role-switch/role ``` Check whether this succeeded: ```bash # On device journalctl -u gadget-setup | grep -i "role" cat /sys/class/usb_role/usb2-0-role-switch/role # should show "device" # Check UDC state cat /sys/kernel/config/usb_gadget/wendyos_device/UDC ``` If the UDC path differs (AGX-style topology): ```bash ls /sys/class/usb_role/ # Look for usb2-*-role-switch ``` --- ## Scenario 7 — host-network container cannot resolve names after a network change **Symptom:** A container using network mode `host` can reach IP addresses but cannot resolve hostnames, even though the host can. Restarting or redeploying the container temporarily fixes it. Host-network containers created while the systemd-resolved stub is available use a Wendy-managed `/etc/resolv.conf` that points to `127.0.0.53`. Because the container shares the host network namespace, its queries reach the live host resolver and automatically follow later upstream-DNS server changes from DHCP, VPN, or WiFi reconfiguration. Search domains known to systemd-resolved are copied into the managed file when the container is deployed and whenever it starts after a reboot. Restart the app to pick up a search-domain change made while it is already running; fully qualified names continue to follow upstream changes without a restart. Check the host and container: ```bash # On the device systemctl is-active systemd-resolved ss -lnt 'sport = :53' # Inside the host-network container cat /etc/resolv.conf # Expected primary path: nameserver 127.0.0.53 ``` When systemd-resolved is absent or its stub listener is disabled, wendy-agent keeps compatibility with other Linux hosts by bind-mounting the host resolver file. The agent logs `systemd-resolved stub is unavailable` when it takes this fallback. A running fallback container cannot observe an atomic replacement of that file; restart the app after the host resolver changes, or enable systemd-resolved for live updates. Containers created by an older agent retain their existing OCI mount. Redeploy them once after upgrading the agent so they receive the managed stub configuration. --- ## Reference — All Diagnostic Commands ### On the device ```bash # Gadget setup systemctl status gadget-setup journalctl -u gadget-setup --no-pager # Network interface ip addr show usb0 ip -s link show usb0 ip neigh show dev usb0 ip neigh flush dev usb0 # NetworkManager systemctl status NetworkManager journalctl -u NetworkManager | grep -i usb0 nmcli connection show usb-gadget nmcli connection down usb-gadget && nmcli connection up usb-gadget # Avahi / mDNS systemctl status avahi-daemon cat /etc/avahi/services/wendyos-mdns.service avahi-browse -t _wendyos._udp avahi-daemon --check # radvd systemctl status radvd ip -6 addr show usb0 # USB hardware (RPi5/DWC2) cat /sys/kernel/debug/usb/udc/1000480000.usb/ep1out ls /sys/class/udc/ cat /sys/kernel/config/usb_gadget/wendyos_device/UDC ls /sys/kernel/config/usb_gadget/wendyos_device/functions/ dmesg | grep dwc2 dmesg | grep -iE "usb|dwc|ncm|udc" | tail -40 # USB hardware (Jetson) cat /sys/class/usb_role/usb2-0-role-switch/role ls /sys/class/usb_role/ # Packet filters nft list ruleset iptables -L -n -v sysctl net.ipv4.conf.usb0.rp_filter ``` ### On the host ```bash # Gadget detection lsusb | grep "1d6b:0104" ip link show | grep enx dmesg | grep -i "cdc_ncm\|ncm\|ether" # Interface state ip addr show enxXXXXXXXXXXXX ip -s link show enxXXXXXXXXXXXX ip -6 addr show enxXXXXXXXXXXXX # ARP ip neigh show dev enxXXXXXXXXXXXX sudo tcpdump -i enxXXXXXXXXXXXX arp -n # DHCP sudo tcpdump -i enxXXXXXXXXXXXX port 67 or port 68 -vn # mDNS avahi-browse -t -p _wendyos._udp # Linux dns-sd -B _wendyos._udp local. # macOS sudo tcpdump -i enxXXXXXXXXXXXX udp port 5353 -vn # NetworkManager nmcli connection show | grep -i usb nmcli -g UUID,NAME connection show | grep usb-gadget-sharing # Serial console (fallback) screen /dev/ttyACM0 picocom /dev/ttyACM0 # SSH ssh root@ ssh root@wendyos-.local ``` --- # USB NCM Gadget Source: https://docs.wendy.dev/latest/advanced/wendyos/networking/usb-ncm/ --- title: "USB NCM Gadget" description: "WendyOS configures the device as a composite USB gadget — a single USB device that exposes both a network interface (NCM) and a serial console (ACM) to the host. This is done at bo" --- # USB NCM Gadget WendyOS configures the device as a composite USB gadget — a single USB device that exposes both a network interface (NCM) and a serial console (ACM) to the host. This is done at boot time using the Linux USB gadget framework via configfs. ## What Is USB NCM? NCM (Network Control Model) is a USB class protocol that carries Ethernet frames over USB. It is part of the CDC (Communications Device Class) family. Compared to the older ECM protocol, NCM offers better throughput and is the preferred mode. WendyOS falls back to ECM only if the kernel does not have NCM support compiled in. On the host, the gadget appears as a standard Ethernet adapter. Linux hosts load the `cdc_ncm` driver automatically; macOS supports NCM natively; Windows requires a driver for older OS versions. ## Composite Gadget Layout The gadget exposes two functions in a single configuration: ``` USB Configuration c.1 (CDC+ACM) ├── functions/ncm.usb0 → network interface (usb0 on device, enxXXXXXXXXXXXX on host) └── functions/acm.usb0 → serial console (/dev/ttyGS0 on device, /dev/ttyACM0 on host) ``` USB descriptor strings set on the gadget: - **Manufacturer**: `Wendy Labs Inc` - **Product**: `WendyOS Device ` - **Serial number**: device serial (from device tree, tegra fuse, `/proc/cpuinfo`, or `eth0` MAC) USB IDs: - **VID**: `0x1d6b` (Linux Foundation) - **PID**: `0x0104` (Multifunction Composite Gadget) ## Kernel Configuration The gadget framework requires these kernel options. On RPi5 they are built-in; on Jetson they are loaded as modules. **RPi5 (`meta-rpi-extensions/recipes-kernel/linux/linux-raspberrypi/usb-gadget.cfg`):** ``` CONFIG_CONFIGFS_FS=y CONFIG_USB_GADGET=y CONFIG_USB_LIBCOMPOSITE=y CONFIG_USB_CONFIGFS=y CONFIG_USB_CONFIGFS_ACM=y CONFIG_USB_CONFIGFS_NCM=y CONFIG_USB_CONFIGFS_ECM=y CONFIG_USB_F_ACM=y CONFIG_USB_F_NCM=y CONFIG_USB_F_ECM=y ``` **Jetson (`meta-tegra-extensions/...usb-gadget.cfg`):** Same options but as modules (`=m`). The module load order matters — `tegra_xudc` must load before `libcomposite` so that the UDC is registered before `gadget-setup.sh` runs. ## Initialization Flow `gadget-setup.service` runs `gadget-setup.sh` once at boot, before NetworkManager starts. ### Step 1 — Resolve device serial number The script tries these sources in order, stopping at the first non-empty result: 1. `/proc/device-tree/serial-number` (RPi device tree) 2. `/sys/devices/platform/tegra-fuse/uid` (Jetson eFuse UID) 3. `awk '/Serial/ {print $2}' /proc/cpuinfo` (legacy RPi CPU info) 4. `eth0` MAC address (stripped of colons) 5. Unix timestamp (last-resort fallback) The last 8 characters are extracted as `SHORT_SERIAL` and used for the human-readable device name when no `/etc/wendyos/device-name` file is present. ### Step 2 — Detect USB controller ```bash if lsmod | grep -q "^tegra_xudc"; then USB_VERSION=0x0320 # USB 3.2 elif lsmod | grep -q "^dwc3"; then USB_VERSION=0x0300 # USB 3.0 elif lsmod | grep -q "^dwc2"; then USB_VERSION=0x0200 # USB 2.0 fi ``` The bcdUSB descriptor is set accordingly. The bus power attribute also changes: `dwc2` devices set `bmAttributes=0x80` (bus-powered, 250 mA), while other controllers use `0xC0` (self-powered). ### Step 3 — Generate stable MAC addresses Two MAC addresses are derived deterministically from the device serial. See [MAC Address Generation](./mac-addresses.md) for the algorithm. ### Step 4 — Build the configfs tree ```bash GADGET_DIR=/sys/kernel/config/usb_gadget/wendyos_device mkdir -p $GADGET_DIR echo 0x1d6b > idVendor echo 0x0104 > idProduct echo $USB_VERSION > bcdUSB echo 0xEF > bDeviceClass # Miscellaneous (required for IAD/composite) echo 0x02 > bDeviceSubClass echo 0x01 > bDeviceProtocol # ... string descriptors ... mkdir -p functions/ncm.usb0 echo $mac_host > functions/ncm.usb0/host_addr echo $mac_self > functions/ncm.usb0/dev_addr echo 10 > functions/ncm.usb0/qmult # queue multiplier for throughput ln -sf functions/ncm.usb0 configs/c.1/ mkdir -p functions/acm.usb0 ln -sf functions/acm.usb0 configs/c.1/ ``` NCM falls back to ECM if `functions/ncm.usb0` cannot be created (kernel without `usb_f_ncm`). ### Step 5 — Activate the UDC The script polls `/sys/class/udc` for up to 60 seconds until a UDC appears, then writes its name to `$GADGET_DIR/UDC` to bind the gadget to the hardware controller. ### Step 6 — Tegra role-switch workaround On Jetson, the USB-C port role is controlled by a CC chip (FUSB301). If the padctl role-switch notifier chain has not fired by the time the gadget is activated, the UDC stays in "default" mode and never enters device mode. The script forces the role explicitly: ```bash echo "device" > /sys/class/usb_role/usb2-0-role-switch/role ``` It falls back to a glob (`usb2-*-role-switch`) on AGX-style topologies where the index differs. ### Step 7 — Post-activation tuning After the UDC binds: - `usb0-force-up` brings `usb0` administratively up (retries for up to 10 seconds to avoid races). - `ip link set usb0 txqueuelen 2000` increases the TX queue length. - The USB interrupt is pinned to CPUs 0–3 via `/proc/irq//smp_affinity`. ## systemd Services | Service | Purpose | |---|---| | `gadget-setup.service` | Runs `gadget-setup.sh` + `usb0-force-up`; type `oneshot`, `RemainAfterExit=yes` | | `wendyos-usbgadget-unbind.service` | Triggered by udev `udc remove` event; writes `""` to the UDC file to cleanly unbind | **Service ordering:** `gadget-setup.service` runs `After=sysinit.target local-fs.target systemd-udevd.service` and `Before=NetworkManager.service network.target`. ## udev Rules | Rule file | Effect | |---|---| | `90-usb0-up.rules` | Runs `usb0-force-up` whenever `usb0` is added or changes state | | `99-usb-gadget-udc.rules` | Triggers `wendyos-usbgadget-unbind.service` on UDC removal | ## Teardown (Rebinding) The script handles re-runs gracefully. If `$GADGET_DIR` already exists it performs a strict reverse-order teardown: 1. Clear UDC binding 2. Remove function symlinks from configs 3. Remove function directories 4. Remove config string and config directories 5. Remove gadget string directories and the gadget root This avoids the EBUSY errors that occur when configfs entries are removed out of order. ## Known Issues **RPi5 / DWC2 DMA stall (kernel 6.6, BCM2712):** BCM2712 reports DMA capability but DMA completion interrupts never fire for Out (host to device) transfers in peripheral mode. The symptom is that the device can send data but receives nothing from the host. The fix is `g_dma=false` in `dwc2_set_bcm_params()`. WendyOS applies this in a kernel patch under `meta-rpi-extensions/`. Do not write `""` to the UDC sysfs file on an affected board without this patch — doing so triggers `ep_stop_xfr` timeouts that leave Global OUT NAK permanently asserted until reboot. --- # WendyOS on Raspberry Pi Source: https://docs.wendy.dev/latest/advanced/wendyos/pi/ --- title: "WendyOS on Raspberry Pi" description: "WendyOS supports Raspberry Pi 5, Raspberry Pi 4, and Raspberry Pi 3, with one image per board. It boots from an SD card or a USB disk on the Pi 3 and Pi 4, and from an SD card or N" --- # WendyOS on Raspberry Pi WendyOS supports **Raspberry Pi 5**, **Raspberry Pi 4**, and **Raspberry Pi 3**, with one image per board. It boots from an SD card or a USB disk on the Pi 3 and Pi 4, and from an SD card or NVMe on the Pi 5. ## Partition Layout Raspberry Pi images use WIC (OpenEmbedded Image Creator). The partition table format depends on the board: - **Raspberry Pi 4 and 5** use a **GPT** partition table (`rpi-partuuid.wks`). Partitions are referenced by PARTUUID in `cmdline.txt` and `fstab`. - **Raspberry Pi 3** uses an **MBR** (msdos) partition table (`rpi-mbr.wks`). The BCM2837 GPU bootrom can only read MBR-partitioned FAT32 and cannot locate `bootcode.bin` on a GPT disk, so the board never boots from a GPT image. Partitions are referenced by filesystem label instead of PARTUUID. ### SD card layout | Partition | Label | Size | FS | Mount | |---|---|---|---|---| | boot | boot | 128 MB | FAT32 | `/boot` | | config | config | 64 MB* | FAT32 | `/config` | | root | root | 8 GB | ext4 | `/` | ### Other boot disks The same image and partition layout also boot from a USB disk (Pi 3 and Pi 4) or an NVMe drive (Pi 5). U-Boot tries the SD card first, so remove a WendyOS SD card to boot the other disk. \* Default size. See [Build configuration](#build-configuration). ## Config partition See [Config Partition](../config-partition.md) for full details and usage guidance. ## Build configuration | Variable | Default | Description | |---|---|---| | `WENDYOS_CONFIG_PART_SIZE_MB` | `64` | Size of the FAT32 config partition in MB | Set this in your `local.conf` or distro config to override the default. --- # WendyOS Hardware Requirements Source: https://docs.wendy.dev/latest/advanced/wendyos/requirements/ --- title: "WendyOS Hardware Requirements" description: "WendyOS requires at least 64 GB of storage. This covers the OS partitions (root A/B for wendyos-update OTA), the config partition, and enough headroom for container images and pers" --- # WendyOS Hardware Requirements ## Minimum requirements | Component | Minimum | Notes | |-----------|---------|-------| | CPU architecture | arm64 or x86_64 | | | RAM | 1 GB | 4 GB recommended for ML workloads | | Storage | 64 GB | NVME or SD card. See [Storage](#storage) below. | | Network | Any | Required for cloud connectivity and OCI image pulls | ## Storage WendyOS requires at least **64 GB** of storage. This covers the OS partitions (root A/B for `wendyos-update` OTA), the config partition, and enough headroom for container images and persistent data. A larger drive is not required by WendyOS itself, but running large container images (for example, ML inference containers with bundled model weights) can consume significant space. If your workload pulls images above a few gigabytes, a 128 GB or larger drive is recommended. The 64 GB minimum applies to removable NVME and SD card installations. On Jetson Orin Nano and Jetson AGX Orin, the data partition auto-expands on first boot to fill any remaining space, so a larger drive is automatically used. Jetson AGX Thor is flashed over USB recovery to its internal storage instead of to a removable target drive. ### Partition sizes at a glance | Device | Root FS (A+B) | Config | Other | |--------|--------------|--------|-------| | Raspberry Pi (SD / NVME) | 8 GB | 64 MB | — | | Jetson Orin Nano / AGX Orin (SD / NVME) | 8 GB + 8 GB | 64 MB | 512 MB data partition (auto-expands) | | Jetson AGX Thor | Flashpack-managed internal partitions | N/A | Flashed over USB recovery; no external SD/NVME target | See the device-specific pages for full partition layouts: - [WendyOS on Raspberry Pi](pi/README.md) - [WendyOS on Jetson](jetson/README.md) ## Supported devices ### Raspberry Pi | Device | Boot media | |--------|-----------| | Raspberry Pi 5 | SD card, NVME | | Raspberry Pi 4 | SD card, USB | | Raspberry Pi 3 | SD card, USB | A Pi 3B boots from USB only after USB boot mode has been enabled once; a Pi 3B+ does so out of the box. ### NVIDIA Jetson | Device | Boot media | |--------|-----------| | Jetson Orin Nano | SD card, NVME | | Jetson AGX Orin | eMMC, NVME | | Jetson AGX Thor | Internal flash + NVME via USB recovery | | DGX Spark | Install `wendy-agent` on the existing Linux system (no image flash needed) | ### x86-64 machines (Intel & AMD) Any 64-bit Intel or AMD PC, workstation, or server runs Wendy by installing `wendy-agent` on your existing Linux distribution (Ubuntu, Fedora, Arch, etc.) — no WendyOS image flash required. This is the same install path used for DGX Spark. | Machine | Setup | |---------|-------| | Intel x86-64 (Core, Xeon) | Install `wendy-agent` on existing Linux | | AMD x86-64 (Ryzen, EPYC) | Install `wendy-agent` on existing Linux | See [Install wendy-agent on Linux](../../../installation/linux/). ## GPU acceleration | GPU | Status | |-----|--------| | NVIDIA (CUDA) | Supported — Jetson and x86 systems with NVIDIA GPUs, via the `gpu` entitlement | | AMD (ROCm) | Supported on compatible x86 Linux hosts with `amdgpu`, via the `gpu` entitlement | See [GPU Access](../../../hardware/gpu/). --- # WendyOS systemd Services Source: https://docs.wendy.dev/latest/advanced/wendyos/services/ --- title: "WendyOS systemd Services" description: "WendyOS is a Yocto-based Linux distribution managed entirely by systemd. This document covers what runs on a WendyOS device, the startup order, and how services depend on one anoth" --- # WendyOS systemd Services WendyOS is a Yocto-based Linux distribution managed entirely by systemd. This document covers what runs on a WendyOS device, the startup order, and how services depend on one another. For the Agent's Cloud-owned UUID unenrollment, guarded local erasure and restart recovery, see [Cloud-owned unenrollment](../../../device/unenrollment/). This requires compatible Cloud and Agent builds; it is not an assertion of deployed acceptance. ## Service inventory | Service | Unit file | Purpose | |---------|-----------|---------| | `wendyos-etc-binds.service` | `recipes-core/wendyos-etc-binds` | Bind-mounts `/data/etc/wendyos` and `/data/etc/NetworkManager/system-connections` over their `/etc` counterparts so identity and WiFi credentials survive OTA updates | | `wendyos-uuid-generate.service` | `recipes-core/wendyos-identity` | Generates `/etc/wendyos/device-uuid` on first boot using `uuidgen` | | `wendyos-device-name-generate.service` | `recipes-core/wendyos-identity` | Generates `/etc/wendyos/device-name` (e.g. `brave-dolphin`) from word lists | | `wendyos-identity.service` | `recipes-core/wendyos-identity` | Updates the mDNS Avahi service record with the device UUID and name | | `wendyos-hostname.service` | `recipes-connectivity/avahi` | Derives `hostname` from the persistent device name and sets it before Avahi starts | | `gadget-setup.service` | `recipes-core/gadget-setup` | Configures the USB gadget composite device (NCM or ECM network + ACM serial console) using Linux configfs | | `wendyos-usbgadget-unbind.service` | `recipes-core/gadget-setup` | Tears down the USB gadget on shutdown | | `wendyos-agent.service` | `recipes-core/wendyos-agent` | Runs the `wendy-agent` binary; provides device management, gRPC API, WiFi control, and container orchestration. Ordered after the boot-time identity publisher and Avahi so an explicit rename can be re-asserted without a race. | | `wendyos-agent-updater.service` | `recipes-core/wendyos-agent` | One-shot agent self-update (downloads from GitHub releases). Also stopped during `UpdateOS` to prevent cgroup SIGTERM propagation to the in-flight OTA (wendyos-update) process | | `wendyos-agent-updater.timer` | `recipes-core/wendyos-agent` | Triggers the updater 5 min after boot, then daily at 03:00 with a random 30-min jitter. Stopped for the duration of any `UpdateOS` call so the updater cannot interrupt an in-flight OTA (wendyos-update) operation; re-started when the call returns | | `containerd.service` | upstream (meta-virtualization) | Container runtime; `wendy-agent` requires it before starting | | `var-lib-containerd.mount` | `recipes-core/systemd-mount-containerd` | Bind-mounts `/data/containerd` to `/var/lib/containerd` so container images persist on the data partition | | `home.mount` | `recipes-core/systemd-mount-home` | Bind-mounts `/data/home` to `/home` | | `var-log.mount` | `recipes-core/systemd-conf` | Bind-mounts `/data/log` to `/var/log` (only when `WENDYOS_PERSIST_JOURNAL_LOGS=1`) | | `swapfile-setup.service` | `recipes-core/swapfile-setup` | Creates and activates a swap file on `/data` before `swap.target` | | `rpi5-eeprom-config.service` | `meta-rpi-extensions/recipes-bsp/rpi-eeprom` | One-shot: sets `PSU_MAX_CURRENT=3000` in the RPi 5 EEPROM for reliable USB gadget operation; reboots to apply, then never runs again | | `rpi5-eeprom-nvme-config.service` | `meta-rpi-extensions/recipes-bsp/rpi-eeprom` | One-shot: sets `PCIE_PROBE=1` and `BOOT_ORDER=0xf461` in the RPi 5 EEPROM for NVMe boot | | `wendyos-mdns.service` | `recipes-connectivity/avahi` | Writes the Avahi service definition for `_wendyos._udp` discovery on port 50051 | | `systemd-resolved.service` | upstream (systemd) | Optional stub DNS resolver; enabled by adding `resolved` to `DISTRO_FEATURES` at build time. When not enabled, NetworkManager manages `/etc/resolv.conf` directly. | | systemd-networkd | upstream (meta-networking) | Manages all network interfaces (WiFi, USB gadget `usb0`) | | avahi-daemon | upstream (meta-connectivity) | Publishes and resolves mDNS/DNS-SD records | | serial-getty@ttyAMA0.service / serial-getty@ttyS0.service | `recipes-core/systemd` | Provides a login shell on the debug UART | ## Startup order The diagram below shows the major ordering relationships. Solid arrows indicate `After=`/`Requires=`; dashed arrows indicate `Before=`/`Wants=`. ``` local-fs.target | +-- data.mount | | | +-- mender-systemd-growfs-data.service (Tegra only) | | +-- wendyos-etc-binds.service ────────────────────────────────────+ | | +-- wendyos-uuid-generate.service | | | | +-- wendyos-device-name-generate.service | | | +-- (sysinit.target) ──────────────────────────────+ | var-lib-containerd.mount | home.mount gadget-setup.service [var-log.mount] | | | containerd.service network.target | | +------------------------------+ | wendyos-agent.service (multi-user.target) wendyos-uuid-generate.service wendyos-device-name-generate.service | +-- wendyos-hostname.service --> avahi-daemon.service --+ +-- wendyos-identity.service ---------------------------+ | wendyos-agent.service ``` ## systemd target dependencies | Target | WendyOS additions | |--------|-------------------| | `sysinit.target` | `wendyos-etc-binds.service`, `wendyos-uuid-generate.service`, `wendyos-device-name-generate.service` | | `local-fs.target` | `var-lib-containerd.mount`, `home.mount`, `[var-log.mount]` | | `swap.target` | `swapfile-setup.service` | | `multi-user.target` | `wendyos-agent.service`, `wendyos-agent-updater.service`, `gadget-setup.service`, `wendyos-hostname.service`, `wendyos-identity.service`, `rpi5-eeprom-config.service`, `rpi5-eeprom-nvme-config.service` | | `timers.target` | `wendyos-agent-updater.timer` | ## Key ordering rules 1. `/data` must be mounted before any bind mount or identity service runs. 2. `wendyos-etc-binds.service` must complete before UUID and device-name generation so the generated files land on the persistent `/data` volume. 3. `gadget-setup.service` runs before `systemd-networkd` so the `usb0` interface exists when networkd starts. 4. `containerd.service` must be running and `/var/lib/containerd` must be bind-mounted before `wendyos-agent.service` starts. 5. EEPROM services (RPi 5 only) run after `multi-user.target` and use a flag file (`/var/lib/wendyos/eeprom-updated`, `/var/lib/wendyos/eeprom-nvme-updated`) to guarantee they execute at most once. 6. `wendyos-agent.service` runs after `wendyos-identity.service` and `avahi-daemon.service`. This ensures `ReassertHostnameAdvertisement`, which restores an explicit `wendy device rename` hostname over boot-time TXT records, runs after the publisher instead of racing it. These are ordering-only dependencies; packaged Linux installs that do not provide the WendyOS units continue to start normally. ## Checking service status ```sh # Overview of all WendyOS-specific services systemctl status wendyos-*.service gadget-setup.service rpi5-eeprom-config.service # Live journal for the agent journalctl -fu wendyos-agent.service # Check mount targets systemctl status var-lib-containerd.mount home.mount ``` ## Wendy Data storage `WENDY_DATA_INGEST_URL` is the cloud DataIngestService endpoint sealed episodes are uploaded to (`ingest.data.wendy.sh` for the dev data platform). It is required for uploads: the enrolled cloud host does not serve this service, so there is no fallback. Without it the agent logs once at Info level that uploads are disabled and sealed episodes stay queued in the local store, bounded by its quota. The device authenticates with the certificate it enrolled with, presented in the TLS handshake; no request header carries identity. `WENDY_TELEMETRY_URL` enables OpenTelemetry exports to an explicitly configured OTLP gRPC collector that accepts the enrolled device's mTLS identity. Cloud's enrollment and relay endpoints no longer provide OTLP ingestion. Without this setting, the agent logs once at Info level that exports are disabled. Telemetry remains available locally within the buffer's retention limits. Set a collector host, host:port, or HTTPS URL without a path, then restart the agent. `WENDY_DATA_DIR` sets the root directory for the device-local Episode store. The production default is `/var/lib/wendy-agent/data/episodes`. Set it in the agent service environment when Episodes should live on a larger data volume: ```sh WENDY_DATA_DIR=/data/wendy-agent/data/episodes wendy-agent ``` The agent creates the directory on startup. Unprivileged developer agents that cannot create the production default fall back to their platform user configuration directory. WendyOS services should set an explicit path on the persistent `/data` partition when the root filesystem is small. ## Agent shutdown The agent handles `SIGINT` and `SIGTERM` before starting background workers, so termination during startup also requests resource cleanup. Its shutdown log entry is `Termination requested, shutting down`; it replaces `Received signal, shutting down` and no longer includes a signal-name field. Use the current entry when filtering logs or configuring alerts. --- # Key Configuration Files Source: https://docs.wendy.dev/latest/advanced/wendyos/services/configuration-files/ --- title: "Key Configuration Files" description: "This document describes the configuration files that control WendyOS service behaviour, where they live on the device, and what they do." --- # Key Configuration Files This document describes the configuration files that control WendyOS service behaviour, where they live on the device, and what they do. ## Identity and device files | Path | Persistence | Description | |------|-------------|-------------| | `/etc/wendyos/device-uuid` | Persistent (`/data/etc/wendyos/`) | RFC 4122 UUID generated once on first boot by `wendyos-uuid-generate.service`; at enrollment (both startup pre-provisioning and live re-enrollment) the agent replaces it with the device UUID in its certificate, the device's one id, and rewrites the `id=` TXT record of the `_wendyos._udp` Avahi service to match (a device with neither a non-empty explicit hostname nor a device name first pins its current hostname to `/etc/wendy-agent/hostname`). A legacy asset certificate carries no device UUID and leaves both untouched. Survives OTA updates via bind mount. | | `/etc/wendyos/device-name` | Persistent (`/data/etc/wendyos/`) | Human-readable name in `adjective-noun` format (e.g. `brave-dolphin`), generated by `wendyos-device-name-generate.service`. Used as the mDNS hostname and USB gadget product string. | | `/etc/wendyos/device-type` | Persistent (`/data/etc/wendyos/`) | Board identifier string (e.g. `raspberry-pi-5`, `raspberry-pi-4`). Seeded from the rootfs on first boot and kept on `/data` thereafter. | | `/etc/wendyos/version.txt` | Symlink to `/usr/lib/wendyos/version.txt`, refreshed via bind mount | OS version string (e.g. `WendyOS-RPi-0.13.2`). Updated on every OTA. | All files under `/etc/wendyos/` are bind-mounted from `/data/etc/wendyos/` by `wendyos-etc-binds.service` early in boot. ## Agent configuration | Path | Description | |------|-------------| | `/etc/wendy-agent/device-key.pem` | Device enrollment private key, mode `0600`. Internal agent state; see [Credential persistence](../../pki/README.md#credential-persistence). | | `/etc/wendy-agent/provisioning.json` | Enrollment details, certificate (`certPem`) and CA chain (`chainPem`), mode `0600`. New writes exclude the private key. | | `/etc/wendy-agent/hostname` | Literal hostname set by `wendy device rename`. Backed by `/data`, it survives reboots and OTA updates and takes precedence over `/etc/wendyos/device-name` in hostname generation. At agent startup, `ReassertHostnameAdvertisement` also uses it to restore hostname-derived Avahi TXT records that were replaced during boot or by a fresh OTA slot. | | `/etc/default/wendy-agent` | Shell-format environment file sourced by both `wendyos-agent.service` and `wendyos-agent-updater.sh`. Supports `WENDYOS_AGENT_GITHUB_REPO`, `WENDYOS_AGENT_VERSION`. Not created by default; create it to override release channel. | | `/var/lib/wendy-agent/` | Runtime state directory for the agent (current-version file, working data). | | `/opt/wendyos/bin/wendy-agent` | Agent binary. Installed at build time; overwritten in-place by `wendyos-agent-updater.service`. Outside `/usr` on purpose: a merged sysext replaces that hierarchy, which would hide an updated binary. | | `/opt/wendyos/bin/wendyos-agent-updater.sh` | Updater logic: checks GitHub releases API, downloads, replaces binary, restarts service. The timer and service are temporarily stopped during `wendy os update` to prevent interference with the in-flight OTA operation (wendyos-update on current images). | | `/opt/wendyos/bin/download-wendyos-agent.sh` | Low-level downloader used by the updater. Fetches `wendy-agent-linux-arm64-*.tar.gz` from the latest stable GitHub release. | ## Network configuration All files under `/etc/NetworkManager/system-connections/` are bind-mounted from `/data/etc/NetworkManager/system-connections/` by `wendyos-etc-binds.service`. | Path | Description | |------|-------------| | `/usr/lib/NetworkManager/system-connections/usb-gadget.nmconnection` | Distro-managed profile for the `usb0` interface. Read-only on the rootfs; updated on OTA. IPv4 mode is set at build time by `WENDYOS_USB_NET_MODE` (`link-local`, `dhcp-client`, or `dhcp-server`). High route-metric (700) keeps WiFi/Ethernet preferred as the default route. | | `/etc/NetworkManager/system-connections/` | User-added connections (WiFi, VPN). Persisted on `/data`. NM gives `/etc` priority over `/usr/lib` for same-named files. | | `/etc/NetworkManager/NetworkManager.conf` | Main NM config: keyfile plugin, DNS managed by NM (not systemd-resolved), random MAC scan disabled, INFO-level logging to journal. | | `/etc/NetworkManager/conf.d/00-manage-usb0.conf` | Ensures NM manages the `usb0` interface. | | `/etc/NetworkManager/conf.d/10-usb-gadget.conf` | Additional per-interface settings for the USB gadget connection. | | `/etc/NetworkManager/conf.d/99-interface-metrics.conf` | Sets route metrics so wired/WiFi interfaces are preferred over USB gadget (`usb0`). | ## systemd journal configuration | Path | When present | Description | |------|-------------|-------------| | `/usr/lib/systemd/journald.conf.d/10-wendyos-persistent.conf` | `WENDYOS_PERSIST_JOURNAL_LOGS=1` | Sets `Storage=persistent` so journald writes to `/var/log/journal` instead of `/run/log/journal`. | | `/etc/systemd/journald.conf.d/10-persistent.conf` | RPi5, `journal_persist-on` feature | Same persistence setting, installed by the RPi-specific `rpi-systemd.inc` bbclass. | See [logging.md](logging.md) for the full journald configuration and how to access logs. ## USB gadget configuration `gadget-setup.service` runs `/usr/sbin/gadget-setup.sh` which uses Linux configfs to build the gadget. Environment variables that control it can be set system-wide (e.g. via `/etc/default/wendy-agent` or a drop-in for `gadget-setup.service`): | Variable | Default | Description | |----------|---------|-------------| | `USB_VID` | `0x1d6b` | USB Vendor ID (Linux Foundation) | | `USB_PID` | `0x0104` | USB Product ID (Multifunction Composite Gadget) | | `USB_MFR` | `Wendy Labs Inc` | Manufacturer string | | `USB_PROD` | `WendyOS Device ` | Product string shown on host | | `USB_SERIAL` | device serial number | Serial number string | | `GADGET_FUNC_ORDER` | `ncm ecm` | Network function preference; `ncm` is tried first, `ecm` is the fallback | The gadget always includes an ACM serial function (`/dev/ttyGS0` on device, `ttyACMx` on host). ## Boot configuration (RPi) | Path | Description | |------|-------------| | `/boot/config.txt` | RPi firmware configuration. WendyOS adds `enable_uart=1` and either `dtoverlay=uart0-pi5` (RPi 5) or `dtoverlay=uart0` (RPi 4). Also adds `dtoverlay=dwc2` when `ENABLE_DWC2_PERIPHERAL=1`. | | `/boot/cmdline.txt` | Kernel command line. WendyOS appends `console=tty1` to keep a HDMI console alongside the serial console. | ## fstab `/etc/fstab` is populated from a board-specific template at build time: - **RPi 4/5** (`rpi-fstab`): mounts partitions by PARTUUID. - **RPi 3** (`rpi-mbr-fstab`): mounts partitions by filesystem label. Under MBR the kernel exposes PARTUUIDs as `-` rather than UUIDv4, so label-based references are used instead. RPi 4/5 fstab mounts: | Partition | Mount point | Options | |-----------|-------------|---------| | `PARTUUID=` | `/boot` | vfat, defaults | | `PARTUUID=` | `/` | ext4, noatime | | `LABEL=config` | `/config` | vfat, nofail | RPi 3 fstab mounts: | Partition | Mount point | Options | |-----------|-------------|---------| | `LABEL=boot` | `/boot` | vfat, defaults | | `LABEL=root` | `/` | ext4, noatime | | `LABEL=config` | `/config` | vfat, nofail | The `nofail` option on `/config` means a missing or unformatted config partition does not block boot. See [Config Partition](../config-partition.md) for details. ## Disk layout The partition table format depends on the board: ### RPi 4/5 — GPT (`rpi-partuuid.wks`) | Partition | Label | Size | Filesystem | Mount | |-----------|-------|------|------------|-------| | 1 | `boot` | 128 MB | FAT32 | `/boot` | | 2 | `config` | 64 MB (default) | FAT32 | `/config` | | 3 | `root` | 8 GB | ext4 | `/` | ### RPi 3 — MBR (`rpi-mbr.wks`) The BCM2837 GPU bootrom on the Pi 3 can only read MBR-partitioned FAT32. The GPT layout used on Pi 4/5 leaves the board unable to find `bootcode.bin` and the system never boots. | Partition | Label | Size | Filesystem | Mount | |-----------|-------|------|------------|-------| | 1 | `boot` | 128 MB | FAT32 | `/boot` | | 2 | `config` | 64 MB (default) | FAT32 | `/config` | | 3 | `root` | 8 GB | ext4 | `/` | The config partition size can be adjusted at build time with `WENDYOS_CONFIG_PART_SIZE_MB` in `local.conf` or the distro conf. There is no pre-provisioned `/data` partition in the SD image. Machines that need persistent data (Tegra/Jetson) add a `/data` partition via their own WKS files. --- # EEPROM Support (Raspberry Pi 5) Source: https://docs.wendy.dev/latest/advanced/wendyos/services/eeprom/ --- title: "EEPROM Support (Raspberry Pi 5)" description: "The Raspberry Pi 5 stores its bootloader configuration in an SPI EEPROM on the board. WendyOS manages two EEPROM settings automatically on first boot via one-shot systemd services." --- # EEPROM Support (Raspberry Pi 5) The Raspberry Pi 5 stores its bootloader configuration in an SPI EEPROM on the board. WendyOS manages two EEPROM settings automatically on first boot via one-shot systemd services. This document describes what is stored, how the automatic update works, and how to inspect or change the EEPROM manually. This page is RPi 5 specific. RPi 4 and Jetson hardware do not use this mechanism. ## What is stored in the EEPROM The RPi 5 EEPROM contains the bootloader firmware and a text-format configuration block. WendyOS cares about three keys: | Key | WendyOS target value | Purpose | |-----|---------------------|---------| | `PSU_MAX_CURRENT` | `3000` (mA) | Raises the USB current limit so the USB gadget composite device can supply enough current. Without this, `usb0` may brownout or fail to enumerate on some hosts. | | `PCIE_PROBE` | `1` | Tells the bootloader to probe PCIe at startup, required for NVMe drives connected via non-HAT+ PCIe adapters. | | `BOOT_ORDER` | `0xf461` | SD card first, NVMe second, Network third, restart on failure. Nibbles are evaluated right-to-left: 1=SD, 6=NVMe, 4=Network, f=restart. | ## Automatic EEPROM management Two one-shot services run after `multi-user.target` and each execute at most once per device lifetime, guarded by flag files in `/var/lib/wendyos/`. ### `rpi5-eeprom-config.service` Source: `meta-rpi-extensions/recipes-bsp/rpi-eeprom/` Script: `/usr/sbin/rpi5-eeprom-update.sh` Flow: 1. Checks `/var/lib/wendyos/eeprom-updated`. If it exists, exits immediately. 2. Reads `/proc/device-tree/model`. If not RPi 5, creates the flag file and exits. 3. Calls `rpi-eeprom-config` to read the current EEPROM configuration. 4. If `PSU_MAX_CURRENT` is missing or not `3000`, stages an updated EEPROM image with `rpi-eeprom-update -d -f `, creates the flag file, and reboots. 5. If already correct, creates the flag file and exits without a reboot. ### `rpi5-eeprom-nvme-config.service` Source: `meta-rpi-extensions/recipes-bsp/rpi-eeprom/` Script: `/usr/libexec/rpi5-eeprom-nvme-update.sh` Runs after `rpi5-eeprom-config.service`. Same flow as above, but sets `PCIE_PROBE=1` and `BOOT_ORDER=0xf461`. When staging the NVMe update, this service reads the **live EEPROM** (not the firmware binary) so that any `PSU_MAX_CURRENT` change from the previous service is preserved in the combined update. ### Flag files | Flag file | Service | |-----------|---------| | `/var/lib/wendyos/eeprom-updated` | `rpi5-eeprom-config.service` | | `/var/lib/wendyos/eeprom-nvme-updated` | `rpi5-eeprom-nvme-config.service` | Deleting a flag file causes the corresponding service to re-run on the next boot. This lets you force a re-check after manually changing EEPROM settings. ## Reading the current EEPROM configuration SSH into the device and run: ```sh rpi-eeprom-config ``` Example output: ``` PSU_MAX_CURRENT=3000 PCIE_PROBE=1 BOOT_ORDER=0xf461 ``` ## Changing EEPROM settings manually To change a setting, extract the current config, edit it, and stage the update: ```sh # Extract current config to a file rpi-eeprom-config --out /tmp/bootconf.txt # Edit the file nano /tmp/bootconf.txt # Stage the update (takes effect on next reboot) FIRMWARE=$(find /lib/firmware/raspberrypi/bootloader-2712/default -name 'pieeprom-*.bin' | head -1) rpi-eeprom-config "${FIRMWARE}" --config /tmp/bootconf.txt --out /tmp/pieeprom-new.bin rpi-eeprom-update -d -f /tmp/pieeprom-new.bin # Reboot to apply reboot ``` > If you change `PSU_MAX_CURRENT` or `PCIE_PROBE` manually, delete the corresponding flag file before rebooting so the WendyOS service does not overwrite your change. ## Firmware binary location The EEPROM firmware images are shipped with the OS and located at: ``` /lib/firmware/raspberrypi/bootloader-2712/ ├── default/ # Stable release firmware ├── stable/ └── latest/ ``` The update scripts search `default/`, then `stable/`, then `latest/` for a `pieeprom-*.bin` file. ## Log files Both services log to the journal and to flat log files: | Service | Log file | |---------|----------| | `rpi5-eeprom-config.service` | `/var/log/rpi5-eeprom-update.log` | | `rpi5-eeprom-nvme-config.service` | `/var/log/rpi5-eeprom-nvme-update.log` | ```sh # View service output in the journal journalctl -u rpi5-eeprom-config.service journalctl -u rpi5-eeprom-nvme-config.service # Or view the flat log cat /var/log/rpi5-eeprom-update.log cat /var/log/rpi5-eeprom-nvme-update.log ``` --- # Logging and Debugging Source: https://docs.wendy.dev/latest/advanced/wendyos/services/logging/ --- title: "Logging and Debugging" description: "WendyOS uses systemd-journald for log management. All services write to the journal via stdout/stderr or logger. This document covers how journald is configured, how to enable pers" --- # Logging and Debugging WendyOS uses systemd-journald for log management. All services write to the journal via stdout/stderr or `logger`. This document covers how journald is configured, how to enable persistent storage, and how to access logs. ## Default behaviour By default, journal logs are stored in volatile memory (`/run/log/journal`) and lost on reboot. The default for a given board tracks `WENDYOS_MENDER`: targets without a Mender-managed `/data` partition (QEMU, RPi, Thor Phase 1) use volatile logs; Orin (tegra234) uses persistent logs. ## Persistent journal option To persist logs across reboots, set `WENDYOS_PERSIST_JOURNAL_LOGS=1` in your build configuration (`local.conf` or the distro conf). This causes two extra files to be installed: **`/usr/lib/systemd/journald.conf.d/10-wendyos-persistent.conf`** ```ini [Journal] Storage=persistent SystemMaxUse=512M SystemKeepFree=1G RuntimeMaxUse=64M MaxRetentionSec=1month Compress=yes ForwardToSyslog=yes ``` **`/lib/systemd/system/var-log.mount`** Bind-mounts `/data/log` onto `/var/log` before `systemd-journald.service` starts: ```ini [Mount] What=/data/log Where=/var/log Type=none Options=bind,x-systemd.mkdir ``` The `x-systemd.mkdir` option automatically creates `/data/log` if it does not exist. The unit runs after `data.mount` and `mender-systemd-growfs-data.service` (if present) to ensure the data partition is fully expanded before the bind mount is established. When `Storage=persistent` is set, journald creates `/var/log/journal//` and writes binary journal files there. These survive reboots and OTA updates (the `/data` partition is outside the update boundary on Tegra machines). ### Enabling persistent logging at runtime (without a rebuild) On a running device you can force persistence without a full image rebuild: ```sh mkdir -p /var/log/journal systemd-tmpfiles --create --prefix /var/log/journal systemctl restart systemd-journald ``` This only persists until the next OTA update that does not include the persistent-logging configuration. ## Accessing logs ### Follow a service in real time ```sh journalctl -fu wendyos-agent.service journalctl -fu gadget-setup.service journalctl -fu wendyos-etc-binds.service ``` ### View logs since last boot ```sh journalctl -b ``` ### View logs from a previous boot (requires persistent storage) ```sh # List available boots journalctl --list-boots # Show logs from boot index -1 (previous boot) journalctl -b -1 ``` ### Filter by systemd identifier (syslog tag) ```sh journalctl -t wendy-agent-updater journalctl -t rpi5-eeprom-update journalctl -t gadget-setup.sh ``` ### View all WendyOS service logs together ```sh journalctl -u "wendyos-*" -u gadget-setup.service ``` ### Check journal disk usage ```sh journalctl --disk-usage ``` ### Vacuum old logs ```sh # Keep only the last 7 days journalctl --vacuum-time=7d # Keep only 200 MB journalctl --vacuum-size=200M ``` ## Log storage locations | Configuration | Location | Survives reboot | |---------------|----------|-----------------| | Default (volatile) | `/run/log/journal/` | No | | `WENDYOS_PERSIST_JOURNAL_LOGS=1` | `/var/log/journal/` (→ `/data/log/journal/`) | Yes | ## Individual service log identifiers | Service | syslog identifier / unit | |---------|--------------------------| | wendy-agent | `wendyos-agent.service` | | agent updater | `wendy-agent-updater` | | agent downloader | `wendy-agent-download` | | USB gadget setup | `gadget-setup.sh` | | UUID generation | `wendyos-uuid-generate.service` | | Device name | `wendyos-device-name` | | etc bind mounts | `wendyos-etc-binds` | | EEPROM update | `rpi5-eeprom-update` | | NVMe EEPROM | `rpi5-eeprom-nvme-update` | | NetworkManager | `NetworkManager` | ## Kernel command line and console By default, WendyOS configures two kernel consoles: - `console=serial0,115200` — hardware UART (see [uart.md](uart.md)) - `console=tty1` — HDMI framebuffer Kernel messages and early boot output appear on both. Systemd redirects unit output to the journal after early boot. To see kernel messages in the journal: ```sh journalctl -k # current boot journalctl -k -b -1 # previous boot ``` --- # Debug UART Source: https://docs.wendy.dev/latest/advanced/wendyos/services/uart/ --- title: "Debug UART" description: "WendyOS exposes a serial console on the primary UART for debugging, recovery, and interactive login. The UART is enabled by default on all supported hardware." --- # Debug UART WendyOS exposes a serial console on the primary UART for debugging, recovery, and interactive login. The UART is enabled by default on all supported hardware. ## Hardware connections Connect a 3.3 V USB-to-serial adapter to GPIO pins 14 (TXD) and 15 (RXD) on the 40-pin header, plus a GND pin. | RPi model | UART device | Notes | |-----------|------------|-------| | RPi 4 | `ttyS0` | Mini UART, derived from VPU clock. Baud rate may drift slightly under heavy CPU load. Bluetooth uses the full PL011 UART (`ttyAMA0`) on RPi 4. | | RPi 5 | `ttyAMA0` (alias `ttyAMA10`) | Full PL011 UART; `uart0-pi5` overlay. RPi 5 has a dedicated UART for the serial console, separate from Bluetooth. | The kernel and serial-getty use the `serial0` alias, which the RPi firmware maps to the correct device for the model: | Model | `serial0` → | |-------|-----------| | RPi 4 | `ttyS0` | | RPi 5 | `ttyAMA0` | ## Baud rate **115200 baud, 8N1** (8 data bits, no parity, 1 stop bit). This is set in two places: 1. Kernel command line: `console=serial0,115200` (assembled by meta-raspberrypi) 2. systemd-serialgetty: `SERIAL_CONSOLES = "115200;ttyS0"` (RPi 4) or `"115200;ttyAMA0"` (RPi 5) ## Enabling the UART The UART is enabled at build time. In `raspberrypi5-wendyos.conf` / `raspberrypi4-64-wendyos.conf`: ```ini ENABLE_UART = "1" ``` This causes `rpi-config_%.bbappend` to append the following to `config.txt`: ```ini enable_uart=1 # RPi 4 dtoverlay=uart0 # RPi 5 dtoverlay=uart0-pi5 ``` You can also enable it at runtime by editing `/boot/config.txt`: ```ini enable_uart=1 dtoverlay=uart0 # RPi 4 # dtoverlay=uart0-pi5 # RPi 5 ``` Then reboot. ## systemd serial-getty A login shell is provided on the serial console by `serial-getty@.service`. The unit is installed via `recipes-core/systemd/systemd-serialgetty.bbappend` and configured for **auto-login as root**: ```ini [Service] ExecStart=-/sbin/agetty -a root -8 -L %I 115200 $TERM ``` The `-a root` flag means the UART console drops straight to a root shell without requiring a password. This is intentional for a development/embedded platform — disable or change it for production deployments by overriding the unit: ```sh # Create a drop-in to require a password mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d/ cat > /etc/systemd/system/serial-getty@ttyS0.service.d/require-password.conf << 'EOF' [Service] ExecStart= ExecStart=-/sbin/agetty -8 -L %I 115200 $TERM EOF systemctl daemon-reload systemctl restart serial-getty@ttyS0.service ``` ## Connecting from a host computer ```sh # macOS (check /dev/tty.usbserial-* for your adapter) screen /dev/tty.usbserial-XXXX 115200 # Linux screen /dev/ttyUSB0 115200 # or minicom -D /dev/ttyUSB0 -b 115200 # Using picocom picocom -b 115200 /dev/ttyUSB0 ``` Press `Ctrl-A K` (screen) or `Ctrl-A X` (minicom/picocom) to exit. ## ACM serial console over USB The USB gadget also exposes an ACM serial function alongside the network. This creates: - `/dev/ttyGS0` on the WendyOS device - `ttyACMx` on the connected host (usually `ttyACM0`) This is separate from the hardware UART and does not require any GPIO wiring. It is available whenever the USB gadget is active: ```sh # On the host screen /dev/ttyACM0 115200 ``` On Linux, ModemManager may claim `ttyACM0` and block the connection. If `screen` fails to open the device, stop ModemManager. If the USB-C link hasn't been configured yet, `wendy discover` offers to set it up, and that setup also installs a udev rule that keeps ModemManager off the gadget. The ACM device provides a shell session (via getty) on the device side at `/dev/ttyGS0`. ## Kernel boot messages Early kernel boot messages appear on the serial console before systemd starts. To capture them, connect the serial adapter before powering on the device. To disable the quiet kernel splash and see all boot messages, remove `quiet` and `splash` from `cmdline.txt` if present. --- # U-Boot and Boot Configuration Source: https://docs.wendy.dev/latest/advanced/wendyos/services/uboot/ --- title: "U-Boot and Boot Configuration" description: "WendyOS on Raspberry Pi does not use U-Boot. The RPi firmware chain (bootcode.bin / EEPROM on RPi 5, bootcode.bin on RPi 4) loads the kernel directly from the FAT32 /boot partition" --- # U-Boot and Boot Configuration WendyOS on Raspberry Pi does not use U-Boot. The RPi firmware chain (`bootcode.bin` / EEPROM on RPi 5, `bootcode.bin` on RPi 4) loads the kernel directly from the FAT32 `/boot` partition using `config.txt` and `cmdline.txt`. This document covers that boot configuration: where the files live, what WendyOS sets, and how to customise them. ## Boot files on the `/boot` partition | File | Purpose | |------|---------| | `config.txt` | RPi firmware configuration (hardware init, overlays, UART, PCIe, GPU) | | `cmdline.txt` | Linux kernel command line (root device, consoles, init parameters) | | `bcm*.dtb` | Device Tree Blob for the SoC | | `overlays/` | Device Tree overlays referenced by `config.txt` | | `kernel8.img` (or `kernel_2712.img`) | Kernel image | | `initrd.img` | Initramfs (if present) | ## `config.txt` WendyOS sets the following in `config.txt` via the `rpi-config_%.bbappend` recipe: ```ini # UART enable_uart=1 # RPi 4: standard uart0 overlay dtoverlay=uart0 # RPi 5: Pi5-specific overlay (uart0-pi5) dtoverlay=uart0-pi5 # USB gadget mode (added when ENABLE_DWC2_PERIPHERAL=1) dtoverlay=dwc2 # PCIe x1 lane (RPi 5 only, for NVMe access) dtparam=pciex1 ``` Additional machine-level settings (from `raspberrypi5-wendyos.conf` / `raspberrypi4-64-wendyos.conf`): | Setting | Value | Description | |---------|-------|-------------| | `DISABLE_RPI_BOOT_LOGO` | `1` | Suppresses the rainbow splash | | `DISABLE_SPLASH` | `1` | No boot splash | | `DISABLE_OVERSCAN` | `1` | No overscan compensation | | `ENABLE_UART` | `1` | Enables primary UART | | GPU memory | `16 MB` | Headless mode (set via meta-raspberrypi defaults) | You can add your own entries to `config.txt` in two ways: 1. **At build time** — set `RPI_EXTRA_CONFIG` in `local.conf`: ``` RPI_EXTRA_CONFIG = "dtparam=i2c_arm=on\n" ``` 2. **At runtime** — mount the `/boot` partition and edit `config.txt` directly, then reboot. ## `cmdline.txt` The kernel command line is assembled by meta-raspberrypi from several sources. WendyOS appends: ``` console=tty1 ``` The root partition is identified differently depending on the board: - **RPi 4 and 5** (GPT layout): `root=PARTUUID=` — a stable UUIDv4 assigned at build time. - **RPi 3** (MBR layout): `root=LABEL=root` — the filesystem label set in `rpi-mbr.wks`. Under MBR, the kernel exposes PARTUUIDs as `-`, not as UUIDv4, so PARTUUID references would not resolve. The full command line on a typical RPi 5 WendyOS device looks like: ``` console=serial0,115200 console=tty1 root=PARTUUID= rootfstype=ext4 rootwait ``` On RPi 3: ``` console=serial0,115200 console=tty1 root=LABEL=root rootfstype=ext4 rootwait ``` | Parameter | Description | |-----------|-------------| | `console=serial0,115200` | Hardware UART console (alias for the model-appropriate UART device). See [uart.md](uart.md). | | `console=tty1` | HDMI framebuffer console added by WendyOS | | `root=PARTUUID=` | Root partition identified by GPT PARTUUID (RPi 4/5) | | `root=LABEL=root` | Root partition identified by filesystem label (RPi 3) | | `rootfstype=ext4` | Root filesystem type | | `rootwait` | Wait for the block device to become available | To add additional kernel parameters at build time, set `CMDLINE:append` in your layer or `local.conf`: ```ini CMDLINE:append = " your.param=value" ``` ## Boot order (RPi 5 EEPROM) On RPi 5, boot order and PCIe probe are stored in the EEPROM, not in `config.txt`. WendyOS manages two EEPROM settings automatically via one-shot systemd services: | Setting | Value | Service | |---------|-------|---------| | `PSU_MAX_CURRENT` | `3000` | `rpi5-eeprom-config.service` | | `PCIE_PROBE` | `1` | `rpi5-eeprom-nvme-config.service` | | `BOOT_ORDER` | `0xf461` | `rpi5-eeprom-nvme-config.service` | `BOOT_ORDER=0xf461` means (right-to-left nibbles): SD card (1) → NVMe (6) → Network (4) → restart (f). Both services are guarded by flag files (`/var/lib/wendyos/eeprom-updated`, `/var/lib/wendyos/eeprom-nvme-updated`) so they run at most once per device. If an update is needed, the service stages it with `rpi-eeprom-update` and reboots. See [eeprom.md](eeprom.md) for full details on reading and writing EEPROM settings. ## Reading current boot configuration ```sh # View current config.txt cat /boot/config.txt # View kernel command line that was used for this boot cat /proc/cmdline # RPi 5: read current EEPROM configuration rpi-eeprom-config ``` ## Customising at runtime The `/boot` partition is writable. After editing, reboot to pick up changes. ```sh # Remount read-write if necessary mount -o remount,rw /boot # Edit nano /boot/config.txt # Sync and reboot sync && reboot ``` > Note: OTA updates on Jetson/Tegra machines use Mender and replace the entire rootfs, but the `/boot` partition and `/config` partition sit outside the update boundary and are not affected by updates. --- # WiFi Configuration Source: https://docs.wendy.dev/latest/advanced/wendyos/services/wifi/ --- title: "WiFi Configuration" description: "WendyOS manages WiFi through NetworkManager. There are three ways to configure WiFi: pre-staging credentials on the config partition before first boot, using the wendy device wifi " --- # WiFi Configuration WendyOS manages WiFi through NetworkManager. There are three ways to configure WiFi: pre-staging credentials on the config partition before first boot, using the `wendy device wifi` CLI from a connected host, or running `nmcli` directly on the device. ## How WiFi connections are stored NetworkManager stores connection profiles as `.nmconnection` files. WendyOS splits these into two locations: | Location | Who writes it | Persistence | |----------|---------------|-------------| | `/usr/lib/NetworkManager/system-connections/` | OTA image (distro-managed) | Replaced on OTA update | | `/etc/NetworkManager/system-connections/` | User / `wendy-agent` | Bind-mounted from `/data/etc/NetworkManager/system-connections/`; survives OTA updates | `wendyos-etc-binds.service` establishes the bind mount early in boot so that NetworkManager always sees user-added connections at `/etc`. A hash-based migration step removes stale distro-managed copies from `/data` while preserving connections the user has modified. ## Pre-staging credentials via `wendy.conf` Place a `wendy.conf` file on the `config` partition before first boot. The agent reads it on startup, applies the WiFi configuration, then **deletes the file**: ```ini [wifi] ssid = MyNetwork password = hunter2 ``` The agent calls `nmcli device wifi connect password `, which creates a persistent NetworkManager profile in `/etc/NetworkManager/system-connections/`. Omit `password` for open networks. `wendy install` writes this file automatically when you supply `--wifi-ssid` and `--wifi-password` flags or respond to the interactive prompt. See [Config Partition](../config-partition.md) for the full `wendy.conf` specification. ## Using the `wendy device wifi` CLI The `wendy` CLI (running on a host machine) communicates with `wendy-agent` over the network or Bluetooth to manage WiFi remotely. ### Interactive TUI Running `wendy device wifi` opens a full-screen table listing nearby networks: ```sh wendy device wifi ``` ### Subcommands ```sh # List visible networks (shows SSID, security, signal, known/connected status) wendy device wifi list # List in JSON format (includes priority and RSSI) wendy device wifi list --json # Connect to a network (prompts for SSID and password interactively) wendy device wifi connect # Connect non-interactively wendy device wifi connect --ssid "MyNetwork" --password "hunter2" # Connect to a hidden network wendy device wifi connect --ssid "HiddenNet" --password "secret" # Show current connection status wendy device wifi status # Disconnect from the current network wendy device wifi disconnect # Remove a saved network wendy device wifi forget --ssid "OldNetwork" # Set autoconnect priority of a known network wendy device wifi rank --ssid "Home" --priority 10 # Bulk-reorder known networks (highest priority first) wendy device wifi rank --order "Home,Office,Cafe" ``` ## Using `nmcli` directly on the device SSH into the device and run `nmcli`: ```sh # Scan for available networks nmcli device wifi list # Connect (creates a persistent profile) nmcli device wifi connect "SSID" password "password" # Show all saved connections nmcli connection show # Disconnect from the current WiFi nmcli device disconnect wlan0 # Delete a saved profile nmcli connection delete "SSID" # Show current connection status nmcli device status ``` ## NetworkManager configuration The main NM configuration is at `/etc/NetworkManager/NetworkManager.conf`: ```ini [main] dns=default rc-manager=file plugins=keyfile no-auto-default=lo connectivity.enabled=false [logging] level=INFO backend=journal [device] wifi.scan-rand-mac-address=no ``` Key decisions: - By default, DNS is managed by NM writing `/etc/resolv.conf` directly (`rc-manager=file`). Optionally, `systemd-resolved` can be enabled instead by adding `resolved` to `DISTRO_FEATURES` at build time. When its `127.0.0.53:53` stub is active, newly deployed host-network containers use it so upstream DNS changes propagate without a container restart; their managed resolver file also copies the currently known search domains. Otherwise, the agent retains an inode-pinning host-file fallback for compatibility. - Random MAC address scanning is disabled so device identity remains stable during scans. - `connectivity.enabled=false` saves bandwidth by not sending periodic HTTP connectivity probes. ## USB gadget and WiFi coexistence The USB gadget interface (`usb0`) is configured with route metric 700, higher than typical WiFi/Ethernet auto-metrics (~100–600). When WiFi is connected, it becomes the preferred default route. The USB gadget remains available as a fallback and for direct host-to-device access. The `WENDYOS_USB_NET_MODE` build variable controls the IPv4 method for `usb0`: | Value | Behaviour | |-------|-----------| | `link-local` (default) | RFC 3927 self-assigned 169.254.x.x address; discovery via mDNS/Avahi | | `dhcp-client` | Device requests an address from the host's DHCP server | | `dhcp-server` | Device runs a DHCP server (requires `dnsmasq`; NM shared mode) | ## mDNS discovery WendyOS advertises itself via Avahi using the service type `_wendyos._udp` on port 50051. The `WENDYOS_MDNS_INTERFACES` variable controls which interfaces Avahi uses: - Default (`""`): all interfaces (both `usb0` and `wlan0`) - Set to `"usb0"` to restrict discovery to the USB gadget only On a host with Avahi or mDNS, find devices with: ```sh dns-sd -B _wendyos._udp local. ``` ## Check regulatory restrictions Use `wendy device wifi regulatory` to inspect the country and permitted channels. Use `wendy device wifi regulatory set-country ` to request the device's actual two-letter country code. Requests are temporary hints; driver and firmware restrictions still apply. See the [Wi-Fi command guide](../../clients/wendy-cli/commands/device/wifi.md#check-wi-fi-country-and-available-channels) for examples and detailed output. The Linux agent requires `iw`. It reads the kernel-wide country and each radio's restrictions within a ten-second operation limit. The service uses the same access controls as other Wi-Fi settings. --- # Wendy Cloud Source: https://docs.wendy.dev/latest/cloud/ --- title: Wendy Cloud description: Enroll devices, deploy apps, and operate remotely with Cloud v2. Existing Cloud v1 users can follow the Legacy tab. --- Wendy Cloud connects your CLI to devices outside your local network. Enrolled devices connect outbound, so you do not need to open inbound ports or assign public IP addresses. You can still build and operate local devices without a Cloud account. Use **Cloud v2** for new setups. Choose **Legacy** if your devices still belong to Cloud v1. Signing into v2 does not move existing devices or applications. Follow [Moving to Cloud v2](../cloud-migration/) before migrating a deployment. ## Start with Cloud v2 These examples use CLI [2026.10.17-123916](https://github.com/wendylabsinc/WendyOS/releases/tag/2026.10.17-123916) or newer. You need the [current Wendy CLI](https://github.com/wendylabsinc/WendyOS/releases), access to your organization in [Wendy Cloud](https://cloud.wendy.dev/login), and a supported device running a compatible Wendy Agent. Keep local access to the device during its first enrollment. Use an owner or admin account to enroll it; access to other operations depends on your organization's permissions. ### Sign in and check your organization ```sh wendy cloud login wendy cloud status ``` The current CLI opens the production Cloud v2 sign-in page. Select your organization in the browser. The CLI saves a named context containing your Cloud session and operator certificate. An older CLI may default to Cloud v1; update it before following this guide. If you have multiple saved contexts, sign-in can leave the previous context active. Use `wendy cloud status` to inspect their names and endpoints, then select the intended one: ```sh wendy auth use ``` For v2, check that the selected context points to `cloud.wendy.dev` and reports an OIDC login. [Login options](../advanced/clients/wendy-cli/commands/cloud/login/) cover email-based sign-in and service accounts. ### Enroll a new device Connect the device locally by USB networking or LAN, discover it, and enroll it using the selector returned by discovery: ```sh wendy discover wendy cloud enroll-device --device --name shop-floor-01 wendy cloud discover --all ``` The device receives its own identity and Cloud binding. Wait for it to appear online before trying remote commands. A successful sign-in alone does not enroll a device. For an already enrolled v1 device, use the [migration guide](../cloud-migration/) instead of repeating enrollment. Wendy Lite has a separate firmware workflow and requires physical USB access for enrollment. Start with [ESP32 setup](../installation/wendy-lite-esp32/). Mac targets also have their own [setup and limitations](../installation/wendy-agent-macos/). ### Discover, deploy, and verify ```sh wendy cloud discover wendy cloud discover --all wendy cloud discover --json ``` Discovery shows online devices by default. `--all` includes offline devices; `--json` supports scripts. Cloud v2 identifies devices with UUIDs. Use a name or UUID returned by discovery, rather than a legacy numeric asset ID. Run `wendy device set-default`, open its **Cloud** tab, and press **d** on the intended device. Press **d** again on the default to clear it; the same key works in the live `wendy cloud discover` table. The saved choice keeps its Cloud endpoint, organization, and device identity, so renaming the device or switching context does not retarget it. Use `--device` to override it for one command. From your app's project directory: ```sh wendy run wendy cloud device apps list --device shop-floor-01 wendy cloud device logs --device shop-floor-01 ``` The saved Cloud target makes `wendy run` deploy through Cloud. Check the app's state, logs, and expected output after deployment. A completed upload does not prove the application is ready. To reach an app's own web or API port remotely, use a tunnel as described below. ### Reach services through a tunnel Cloud v2 forwards TCP or UDP to any port from 1 through 65535 on the device's loopback interface. Arbitrary ports need an updated Cloud service and CLI and Wendy 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. ```sh # An HTTP service listening on device port 3000 wendy cloud tunnel 8080:3000 --device shop-floor-01 # A UDP service listening on device port 9000 wendy cloud tunnel 9000:9000/udp --device shop-floor-01 # The Wendy Agent service wendy cloud tunnel 15052:50052 --device shop-floor-01 ``` The Agent port is a transport for an authenticated client, not a web page. Forwarding it does not remove the Agent's authentication requirements. Keep the command running while using a tunnel, then press Ctrl+C to close it. A tunnel reaches only a service that is already listening on the device. Port 22 works only when the target actually runs SSH; release Jetson and Raspberry Pi images do not include an SSH server. Cloud selects the v2 relay. Do not pass `--broker-url` for a v2 session. See the [tunnel reference](../advanced/cloud/tunnel/) for UDP limits, selection rules, and errors. ### Use service accounts for automation A tenant admin can create a service account and enroll its key. Then sign in without an interactive browser: ```sh wendy cloud login --service-account ./wendy-service-account.json ``` Follow [service-account setup](../advanced/clients/wendy-cli/commands/auth/login/#service-account-login) to create the key and configure CI. Service-account login does not issue an operator certificate. Successful sign-in does not imply that device deployment, compute-device tunnels, or certificate-signed operations are available to that account. Check the operation's permissions and supported principal type before moving an unattended collector or deployment pipeline to v2. ## Keep using Cloud v1 Use this path for devices that are still enrolled in [legacy Wendy Cloud](https://cloud.wendy.sh). The current CLI retains the v1 login flow behind an explicit flag: ```sh wendy cloud login --legacy wendy cloud status ``` If another saved context is active, select the legacy context by the name shown in `status`: ```sh wendy auth use wendy cloud discover --all ``` Check that its dashboard is `cloud.wendy.sh` and its login path is legacy. `wendy auth login --legacy` is an equivalent entry point. Signing into v2 does not replace the credentials installed on your v1 devices. ### Operate existing devices ```sh wendy cloud device apps list --device shop-floor-01 wendy cloud device logs --device shop-floor-01 ``` Legacy discovery supports numeric asset IDs as well as device names. Keep existing scripts on their matching legacy context until you have validated the v2 replacements. To deploy, select the device in the **Cloud** tab of `wendy device set-default`, then run `wendy run` from your project. ### Forward an application port or UDP service Legacy sessions keep their existing TCP and UDP forwarding. Each example needs a service already running on the remote device. ```sh # An existing HTTP service listening on device port 80 wendy cloud tunnel 8080:80 --device shop-floor-01 # An existing UDP service listening on device port 9000 wendy cloud tunnel 9000:9000/udp --device shop-floor-01 ``` For a complete UDP example, use [Reach a UDP service through Wendy Cloud](../guides/tutorials/python/cloud-udp-service/). Keep the tunnel running while using the service. ### Plan your upgrade Check application ports and UDP, unattended access, notification delivery, and Companion support before changing device enrollment. On v2, application ports and UDP need the updated Cloud, CLI, and Agent versions. Use [Moving to Cloud v2](../cloud-migration/) to validate a spare device and plan the cutover. Keep the working v1 deployment until its replacement is verified. ## Connect Cloud to the rest of Wendy | What you want to do | Start here | What to check | | --- | --- | --- | | Alert operators in Companion | [Send notifications from an app](../guides/device-notifications/) | App registration, assignment, notification entitlement, Cloud grant, and a compatible signed-in Companion build. Push delivery is best-effort. | | Record sensor data or model results | [Wendy Data](../data/) | Recordings can stay local. Cloud enrollment alone does not configure upload, retention, or a BI connection. | | Analyze operating data | [Wendy BI](https://wendy.dev/products/wendy-bi) | BI has its own access and data-connection setup. Confirm the supported source and integration with the Wendy team. | | Work through an AI assistant | [MCP setup](../guides/llm-integration/) and [ChatGPT setup](../guides/chatgpt/) | Local tools and hosted access have different setup requirements. Device permissions still apply. | For a product map, see [The Wendy ecosystem](../concepts/ecosystem/). For device identities, permissions, and unfinished security controls, see [Security](../security/). The [Cloud reference](../advanced/cloud/requirements/) contains protocol requirements; requirements are not a guarantee that every workflow is available in the deployed service. --- # Moving to Cloud v2 Source: https://docs.wendy.dev/latest/cloud-migration/ --- title: Moving to Cloud v2 description: Compare Cloud v2 with legacy Cloud, keep existing devices reachable, and validate your migration before changing enrollment. --- Cloud v2 uses organization and device UUIDs, browser sign-in through Wendy Auth, and an authorized service catalog for remote connections. Cloud v1 uses the legacy dashboard and numeric asset identities. They are separate enrollments. Updating the CLI or signing into v2 does not migrate a device, its applications, stored data, or access grants. Start with the [Cloud v2 guide](../cloud/) for a new deployment. If you already operate devices on v1, keep their legacy context while you validate v2 on a spare device. ## Compare the workflows | Capability | Cloud v2 | Legacy Cloud v1 | | --- | --- | --- | | Sign-in with the current CLI | `wendy cloud login` | `wendy cloud login --legacy` | | Dashboard | [cloud.wendy.dev](https://cloud.wendy.dev/login) | [cloud.wendy.sh](https://cloud.wendy.sh) | | Device selector | Name or UUID from discovery | Name or numeric asset ID | | Remote Agent operations | Through the authorized Wendy Agent service | Through the legacy broker | | Raw TCP tunnels | Any device port with updated Cloud, CLI, and Agent; older v2 versions allow only ports 22 and 50052 | Application ports supported by the legacy broker | | UDP forwarding | Any device port with updated Cloud, CLI, and Agent; not available on older v2 versions | Available on compatible legacy broker and Agent versions | | Service-account sign-in | Available for authorized API operations; no operator certificate | Not the v2 service-account flow | | App notifications | Requires app registration, assignment, entitlement, and Cloud grant | Existing integrations must be checked separately during migration | See [tunnel limitations](../advanced/cloud/tunnel/) and [service-account authentication](../advanced/clients/wendy-cli/commands/auth/login/#service-account-login). Do not assume a service account can replace a human operator for remote device access merely because its API sign-in succeeds. ## Keep both contexts available Update to the current CLI, then inspect your saved sessions: ```sh wendy cloud status ``` If your legacy session is missing or expired, restore it with `wendy cloud login --legacy`. Add a v2 session with `wendy cloud login` and choose the intended organization in the browser. Use the context names shown by `status` to select each one: ```sh wendy auth use wendy cloud discover --all wendy auth use wendy cloud discover --all ``` When multiple contexts exist, signing in does not necessarily change the active one. Verify the selected context before each operation. A saved default device also retains its Cloud and organization binding; switching context does not retarget that device. Select the new target explicitly when validating v2. ## Inventory what the deployment depends on Record the current device names and IDs, app manifests and image versions, persistent data locations, users, and access requirements. Include the exact ports and protocols used by dashboards, cameras, collectors, and external APIs. Check which scripts run unattended and how they authenticate. If a workflow needs a custom application port or UDP tunnel, v2 requires the updated Cloud service and CLI and Wendy Agent 2026.10.22-211023 or newer. Test that exact port and protocol on the spare device, and keep the workflow on v1 until it passes. Check Companion's v2 support for the features your operators use, including cameras, app controls, logs, and notifications. App Store availability alone does not establish v1/v2 feature parity. ## Validate a spare device 1. Sign into v2 with an owner or admin account and enroll a spare device using the [Cloud guide](../cloud/). Confirm it appears in the correct organization. 2. Select that Cloud device explicitly, deploy the application, and check app state, logs, and expected output through the connection you intend to use. 3. Check every required external connection. Include custom ports, service accounts, notification recipients, and the intended Companion build. 4. For Data, verify capture, upload if required, and access to the resulting episode. For BI, verify the actual data connection and query result separately. 5. Restart the test device and confirm it reconnects and resumes the expected application behavior before scheduling the production change. ## Move an existing device Arrange a maintenance window and retain physical or local access. Back up required application data and configuration before changing enrollment. Confirm with the Wendy team how existing Cloud records, grants, and stored data will be handled for your deployment; this guide does not provide an automatic transfer. Use the [device unenrollment guide](../device/unenrollment/) for your device's identity and installed versions, then enroll it in the intended v2 organization. Unenrollment changes credentials and can remove remote access. Flashing is not a substitute for revoking the previous Cloud identity. After enrollment, update scripts and saved defaults to the new device identity, restore the required app assignments and grants, and repeat the checks from the spare-device validation. Keep the deployment's previous configuration and a local recovery path until the new setup is accepted. Switching CLI context alone does not restore a device's old enrollment. ## If something does not work | Symptom | What to check | | --- | --- | | The fleet is empty after sign-in | Check the active context, organization, and `discover --all`. V1 devices do not appear automatically in v2. | | A numeric device ID no longer resolves | Select the v2 device by its name or UUID from discovery. | | The service catalog has no entry for a port | The Cloud service, CLI, or Agent predates arbitrary v2 ports. Older v2 versions forward only TCP ports 22 and 50052. | | UDP forwarding is refused | Older v2 versions do not forward UDP. Update the Cloud service, CLI, and Agent, or use a legacy session. | | API login works but remote device access fails | Check the account's permissions and principal type. Service accounts do not receive operator certificates. | | The app runs but a notification does not arrive | Check registration, assignment, entitlement, Cloud grant, recipient organization, and Companion push registration in the [notification guide](../guides/device-notifications/). | See [deployment troubleshooting](../errors/) for build, device, and application readiness failures. --- # How Wendy works Source: https://docs.wendy.dev/latest/concepts/ --- title: How Wendy works description: Understand targets, app configuration, and the build and deploy loop. --- Start with the [quickstart](../) if you have not run an app yet. ## Wendy in 60 seconds ```text YOUR DEVELOPMENT MACHINE Wendy CLI / editor | | build, run, debug v YOUR TARGET WendyOS, or an existing machine with Wendy Agent Your app Camera / GPU / GPIO / ROS 2 | | optional enrollment v Wendy Cloud: fleet management and remote deployment ``` | Name | What it does | | --- | --- | | Wendy CLI | The `wendy` tool on your development machine. | | WendyOS | Wendy's Linux OS for supported hardware and the simulator. It includes the agent. | | Wendy Agent | Runs on the target and manages your apps. Install it on an existing Linux or supported Mac machine. | | Wendy Cloud | Optional remote access and fleet management. | | `wendy.json` | Your app's configuration, including its target platform and hardware permissions. | ## Your development machine and your target The CLI runs on your development machine. A target is the machine that runs your app. WendyOS includes Wendy Agent; existing Linux and supported Mac machines can run the agent without replacing their OS. A simulator is a local VM running WendyOS. `wendy run` selects a target, detects the project, builds for that target, uploads the result, starts the app, and attaches logs. Use `wendy run --watch` to repeat that loop when source files change. Linux targets run containers. Mac targets run native macOS apps. ESP32 uses Wendy Lite and has a separate native firmware workflow. ## App configuration [`wendy.json`](../advanced/apps/wendy.json/) names your app, chooses its platform, and declares the capabilities it needs. Entitlements grant access to hardware and other resources. A simulator can exercise the software deployment path. Physical hardware access is limited to opt-in [serial passthrough](../installation/wendyos-virtual-machine/#serial-passthrough); other peripherals need a real device. ## Optional cloud management You can develop locally without enrolling in Wendy Cloud. The [Cloud guide](../cloud/) covers Cloud v2 setup and a Legacy tab for existing v1 devices. Signing into v2 does not migrate those devices; follow [Moving to Cloud v2](../cloud-migration/) before changing enrollment. For Data, BI, Companion, SensorLink, and the other products, see [The Wendy ecosystem](../concepts/ecosystem/). ## Deeper explanations - [Device selection](../advanced/clients/wendy-cli/device-selection/) - [Build detection and toolchains](../advanced/clients/wendy-cli/commands/build/) - [Multi-service apps](../advanced/apps/wendy-services/) - [WendyOS internals](../advanced/wendyos/requirements/) --- # The Wendy ecosystem Source: https://docs.wendy.dev/latest/concepts/ecosystem/ --- title: The Wendy ecosystem description: Choose the Wendy tools for building apps, connecting sensors, operating devices, collecting data, and analyzing results. --- Wendy provides tools to build and operate AI applications on robots and edge devices. Start with the part that matches your task. The products have separate setup requirements and release status; using one does not automatically configure the others. ## Choose a starting point | Your task | Wendy component | Start here | | --- | --- | --- | | Build and deploy an application | CLI, templates, WendyOS, and Wendy Agent | [Run your first app](../../) | | Test without a physical board | WendyOS VMs and robot simulation profiles | [Simulator setup](../../installation/wendyos-virtual-machine/) and [sim-to-real guidance](../../integrations/sim-to-real/) | | Ask an AI assistant to develop or inspect an app | Wendy Chat, MCP, and coding-agent integrations | [Chat](../../guides/chat/) and [coding-agent setup](../../guides/llm-integration/) | | Reach devices outside your local network | Wendy Cloud | [Cloud v2 and Legacy workflows](../../cloud/) | | Run an ESP32 application | Wendy Lite | [Native ESP-IDF setup](../../installation/wendy-lite-esp32/) | | Use a small device's camera stream | SensorLink on supported Lite firmware | [Lite cameras](../../hardware/camera/#wendy-lite-cameras) and [SensorLink overview](https://wendy.dev/blog/sensor-data-to-model-improvements) | | Record sensors, application events, and model results | Wendy Data | [Record an episode or deploy a campaign](../../data/) | | Analyze operating data and build dashboards | Wendy BI | [BI capabilities and access](https://wendy.dev/products/wendy-bi) | | Inspect devices and receive alerts on an iPhone | Wendy Companion | [App Store](https://apps.apple.com/us/app/wendy-companion/id6761771159) and [device notifications](../../guides/device-notifications/) | | Capture human pose demonstrations | TeleCollect | [Capture and export pose data](https://wendy.dev/blog/telecollect-pose-data-collection) | | Evaluate portable fire monitoring | WendyBox | [WendyBox](https://wendy.dev/products/wendy-box) | ## Understand the boundaries For local development, the CLI talks to Wendy Agent on your device. Linux targets run containers; supported Mac targets run native applications. Wendy Lite uses a separate firmware workflow. You can deploy locally without Cloud enrollment. Initial tool, image, and model downloads can still require internet access. For remote operation, Cloud adds device identities, organizational access, and remote connections. Cloud v2 and v1 use separate enrollments, and v2 tunnels to application ports need updated Cloud, CLI, and Agent versions. Use the [migration guide](../../cloud-migration/) before moving an existing fleet. Wendy Data records episodes on the device. Upload is a separate configuration, and BI needs its own access and data connection. Recording an episode or enrolling a device does not automatically create a BI dashboard, configure historical backtesting, or train a model. To alert an operator, your application can request a notification through the Agent. Cloud authorizes and stores it, and Companion provides the operator's inbox and push experience. App registration, grants, recipient access, and a compatible mobile build are required. ## Build a workflow in stages Start with a locally running app and verify its output. Add Cloud access when you need to operate remotely. Use Data to record a meaningful event and inspect the resulting episode. Connect the data source required by your BI use case, then verify its query result. Configure notifications separately when someone needs to respond to an event. Each stage should produce a result you can check. For example, deploying a campaign arms it; the episode manifest confirms what actually recorded. Cloud accepting a notification does not guarantee a phone received the push. ## Availability and security Use the [roadmap](../../advanced/roadmap/) for release status and each setup guide for target-specific limits. Companion's App Store release and the reusable SDK have separate release tracks. A simulated robot does not establish that a policy is ready for physical hardware. WendyOS, the CLI, and Wendy Agent are open source. Check the relevant product's terms and deployment options for hosted services. Local operation, local model inference, Cloud storage, and hosted AI calls have different network and data requirements. See [Security](../../security/) for implemented controls and work still in progress. --- # Wendy Data Source: https://docs.wendy.dev/latest/data/ --- title: Wendy Data description: Record sensor data, application events, and model results on your device with event-triggered campaigns. --- Wendy Data records what happened on a device around an event. A campaign defines the sources to record, the events that start recording, and how long to keep recording. Each recording becomes an episode with timestamped files, source details, model versions, and checksums. Use campaigns to investigate an emergency stop, collect uncertain model predictions, or save examples of an object detected by a camera. For a single recording, use `wendy data record` without deploying a campaign. ## Local capture, Cloud, and BI Start with a local episode and verify its manifest before adding remote services. Local capture and download do not require Cloud enrollment. On an enrolled device, campaign registration and upload have separate requirements; use `upload.when: manual` when you want to control when data leaves the device. See [current limits](#current-limits) for upload and privacy behavior. [Wendy Cloud](../cloud/) provides remote device access. [Wendy BI](https://wendy.dev/products/wendy-bi) provides analysis and dashboards with its own access and data-connection setup. Recording an episode does not automatically make it queryable in BI or configure model training. Confirm the intended storage and BI integration before building a workflow around it. For an existing v1 deployment, check the [Cloud v2 migration guide](../cloud-migration/), including app-port version requirements and service-account limits. ## What you can record | Data | How Wendy captures it | |---|---| | Camera footage | Video from USB, CSI, and registered IP cameras. Compatible camera streams also support periodic snapshots. | | Audio | Microphone audio throughout an episode, or fragments triggered by an audio-level threshold during the episode. | | ROS 2 topics | Published messages such as LiDAR point clouds, odometry, GPS, IMU readings, joint states, and control commands. | | Device health | Uptime, system load, and total/free RAM, sampled once per second on Linux. | | Application events | Named events such as `emergency_stop`, faults, and task completion, with values and attributes supplied by your app. | | Model results | Predictions, detections, uncertainty, model versions, and references to recorded input samples when supplied. | | Custom measurements | App-provided time series such as temperature, vibration, motor current, or production counts. Streams accept structured data, images, and custom binary payloads. | Campaigns select camera, audio, ROS 2, and telemetry sources. They include application records automatically. Other sensors need to publish ROS 2 topics or write through an [application recording stream](#record-data-from-your-app). The built-in telemetry source does not collect every metric shown by `wendy device telemetry`. ## Record your first episode Start with a device running Wendy Agent and select it explicitly with `--device`. Use `wendy discover` to find its selector. ```sh wendy data sources --device workshop.local wendy data record --device workshop.local --name commissioning --source telemetry ``` The recording runs on the device after the command returns. Stop it when you have captured enough data, then inspect and download the episode ID from the output: ```sh wendy data stop --device workshop.local wendy data episodes --device workshop.local wendy data inspect --device workshop.local wendy data download --device workshop.local --output ./commissioning-episode ``` `inspect` verifies the sealed files' checksums. `download` resumes interrupted transfers, verifies each file, and saves the episode locally. Camera episodes include a browser-playable MP4 when the captured stream can be remuxed; the manifest explains when that file could not be created. Use the source IDs returned by `sources` to add a camera or microphone. Repeat `--source` for multiple sources. With no source flags, `record` selects every healthy discovered source, which can include cameras and microphones. ## Deploy a campaign Save this as `campaign.yaml`. It records device health for ten seconds after an app emits `machine_fault`. You can also trigger it manually to try the workflow before connecting an app. ```yaml version: 1 name: machine-faults sources: - telemetry: true capture: buffer: 0s after_trigger: 10s triggers: - event: machine_fault upload: when: manual export: annotation: none ``` `manual` keeps this example out of automatic cloud upload. The required `export.annotation` field records labeling intent. Here, `none` is descriptive metadata; this release does not create annotation tasks. ```sh wendy data campaign deploy campaign.yaml --device workshop.local wendy data campaign inspect machine-faults --device workshop.local wendy data campaign trigger machine-faults --device workshop.local --reason commissioning ``` After ten seconds of capture and the default two-second wait for late app records, use `episodes`, `inspect`, and `download` as above. Check the manifest for the sources that actually recorded and any errors. A successful deployment means the campaign is armed, not that a recording has already succeeded. On a cloud-enrolled device, deployment also registers `campaign:` in Cloud Apps. For an intentionally offline deployment, pass `--skip-cloud-registration`. Unenrolled devices skip registration automatically. ### Add sensors and triggers Replace `sources` with selectors from `wendy data sources`. For example: ```yaml sources: - camera: v4l2:/dev/video0 - audio: audio:16777217 - ros2: /lidar/points - ros2: /vehicle/odometry - telemetry: true ``` The camera and audio IDs above are examples; use your device's IDs. An audio selector must resolve to a healthy capture source at deployment. Camera and ROS 2 sources can be absent when you deploy. Cameras must resolve when recording starts; a missing ROS 2 topic is reported in the manifest while other sources record. `camera: "*"` selects all healthy agent cameras, including cameras added later, and needs at least one camera when recording starts. Add `model.uncertainty: "> 0.65"` as another trigger to capture uncertain predictions. The app must supply the prediction's uncertainty. Any matching trigger starts an episode; manual triggering remains available. Set `capture.buffer: 10s` for history before the trigger and `capture.after_trigger: 20s` for capture afterward. Application records and continuous camera streams support pre-trigger history. Audio, ROS 2, and telemetry begin at the trigger; snapshot cameras do not keep a video pre-buffer. The manifest reports the history each source actually retained. See the [campaign YAML reference](../advanced/clients/wendy-cli/commands/data/#campaign-yaml-reference) for selectors, capture policies, and all fields. ## Record data from your app Add the [`episode-write` entitlement](../advanced/apps/wendy.json/#episode-write) to your app to submit events and predictions. The agent supplies an app-private socket and attributes records to that app. Its events can trigger deployed campaigns; the entitlement does not grant access to sensors. Declare named `streams` inside the entitlement for custom data. Lightweight streams accept raw payloads over Unix sockets. Durable streams use Protobuf envelopes and acknowledge after syncing the record to local storage, even when no episode is recording. Time-series streams can declare channels, units, and sample timestamps separately from the agent's receipt time. An opaque JSON or binary payload does not trigger a campaign by itself. Declare the stream's `event` or `model` metadata; durable model records can supply uncertainty and input references. Export retained durable records with: ```sh wendy data export-stream --device workshop.local --output records.wdr ``` Plain export keeps the device's retained records. Use `--follow --reclaim` with an output directory for ongoing collection that reclaims each saved chunk. The producer must handle a full journal by buffering and retrying; the agent does not overwrite retained records to make room. The [recording stream reference](../advanced/clients/wendy-cli/commands/data/#application-recording-streams) and [WendyRecording examples](https://github.com/wendylabsinc/WendyOS/tree/main/Examples/WendyRecording) include configuration, senders, and an export reader. ## Detect objects without a model app A campaign's optional `inference` block runs object detection on its selected cameras. Choose a Hugging Face model pinned to a 40-character commit SHA, labels, a confidence threshold, and an inference rate. The agent supports Transformers models and compatible YOLO ONNX exports through `backend: yolo_onnx`. Set an event such as `person_detected` and include the same event in `capture.triggers`. On first use, the agent installs its managed Python runtime and downloads the pinned model. This needs network access and storage outside the episode quota. Check `inference_status` in `campaign inspect` to verify the runtime and each camera. `notify.on: event` can deliver a named event immediately; `notify.on: detection` requires inference. Delivery can use a configured webhook or Wendy Cloud. Cloud delivery requires a separately enabled notification grant for the campaign. `notify.on: episode_committed` records cloud-side notification intent instead of sending an immediate device notification. Use the [inference reference](../advanced/clients/wendy-cli/commands/data/#inference) and [all-camera people example](https://github.com/wendylabsinc/WendyOS/tree/main/Examples/WendyDataPeople) when you are ready to enable model downloads and automatic capture. ## Current limits - Camera snapshots and capture-rate caps require a transport that supplies complete encoded frames, currently local V4L2 cameras with native H.264. Other camera transports record continuously and report unapplied policies in the manifest. Audio supports continuous and level-threshold capture; ROS 2 and telemetry support continuous capture only. - Buffered continuous cameras use their producer's current parameters. Explicit `max_resolution` or `rate` settings on those sources are reported as requested but not achieved. Available history also depends on how long the campaign has been armed and the buffer's byte limit. - `upload.when: wifi` currently behaves as `always`. Use `manual` when automatic upload is not wanted. Local inspection and download work without cloud upload. - `retention.local_quota` is stored but not enforced per campaign. The agent enforces a device-wide episode quota. `export.annotation: cvat` records intent but does not create CVAT tasks; declared privacy transforms are metadata and do not redact captured data. - A campaign records one active episode at a time and drops matching triggers while it is capturing. Different campaigns can record concurrently. Episodes use a device-local monotonic timeline and carry separate UTC confidence and clock evidence. Inspect the manifest before assuming that recordings from different devices are synchronized. See the [full `wendy data` reference](../advanced/clients/wendy-cli/commands/data/) for clock evidence, storage, playback, and command details. --- # CLI and Agent Relationship Source: https://docs.wendy.dev/latest/device/cli-agent-relationship/ --- title: CLI and Agent Relationship description: How the Wendy CLI discovers devices and deploys apps through wendy-agent ---