diff --git a/README.md b/README.md index 5790f3d..ab314fa 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Start with the [Quick Start](getting-started/quickstart.md) to install the CLI a ### Automate with AI & code +* [Agent Devices](mcp/agent-devices.md) - give your AI agent a cloud iOS or Android device to drive, with a person on hand when it gets stuck (beta) * [MCP Server](mcp/overview.md) - let Claude, Cursor and other MCP clients run your tests * [CLI Reference](cli/overview.md) - every `dcd` command and flag * [REST API](api/overview.md) - uploads, results and flows over HTTP diff --git a/SUMMARY.md b/SUMMARY.md index f07092a..859ad43 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -59,6 +59,7 @@ * [Status](cli/dcd-status.md) * [List](cli/dcd-list.md) * [Login & Accounts](cli/dcd-login.md) +* [Devices](cli/dcd-device.md) * [Live Sessions](cli/dcd-live.md) ## REST API @@ -67,10 +68,14 @@ * [Uploads](api/uploads.md) * [Results](api/results.md) * [Flows](api/flows.md) +* [Device Sessions](api/device-sessions.md) * [IP Addresses](api/ip-addresses.md) ## AI Agents & MCP +* [Agent Devices](mcp/agent-devices.md) +* [Agent Devices Tools](mcp/agent-devices-tools.md) +* [Agent Instructions](mcp/agent-devices-skill.md) * [MCP Server](mcp/overview.md) * [Tools Reference](mcp/tools.md) diff --git a/api/device-sessions.md b/api/device-sessions.md new file mode 100644 index 0000000..5fdbae4 --- /dev/null +++ b/api/device-sessions.md @@ -0,0 +1,101 @@ +# Device sessions + +The REST API behind [Agent Devices](../mcp/agent-devices.md). The MCP tools call these same endpoints. Authenticate with an [API key](../getting-started/api-keys.md) in `x-app-api-key`, as for the rest of the [REST API](overview.md#authentication). A key limited to **Device sessions only** can reach these endpoints, the hosted MCP server and the build upload endpoints, but nothing else. + +A session is named by its `id` (for example `brave-teal-otter`) in every path. Every response includes a `session` block: its state, time and idle time left, cost so far, warnings and console link. [The tool reference](../mcp/agent-devices-tools.md#the-session-block) lists its fields. + +## Sessions + +| Method and path | What it does | +|---|---| +| `GET /device/models` | The devices you can start: models, OS versions and the defaults | +| `POST /device/sessions` | Start a session. Body: `platform` (`ios` or `android`), and optionally `model`, `os`, `googlePlay`, `binaryUploadId`, `deviceLocale` (such as `de_DE`), `maxDurationMin`, `clientLabel`, `record` (default `true`) | +| `GET /device/sessions` | Your organization's open sessions. `?all=true` adds those that ended in the last day | +| `GET /device/sessions/:id` | One session | +| `DELETE /device/sessions/:id` | Stop a session and release the device | + +## The device + +| Method and path | What it does | +|---|---| +| `POST /device/sessions/:id/install` | Install a build: `{ "binaryUploadId": "..." }`, or `{ "url": "https://..." }` for an `.apk` or a zipped simulator `.app` at a link (up to 500 MB). It installs in the background. A command sent meanwhile waits up to 45 s for it, then answers `LIVE_NOT_READY` | +| `POST /device/sessions/:id/launch` | Launch an app: `{ "appId"?: "...", "clearState"?: true }`. Without `appId`, the installed build | +| `POST /device/sessions/:id/actions` | One action: `tap` (`x`/`y` percent, `text` or `id`), `type` (`text`, and `sensitive: true` to keep it secret), `swipe` (`direction`: `up`, `down`, `left` or `right`, or `from`/`to` points in percent), `press_key` (`key`: `back`, `home`, `enter`, `backspace`, `tab`, `volume_up`, `volume_down`, `lock` or `power`) | +| `POST /device/sessions/:id/run` | Run Maestro: `{ "yaml": "..." }` for one flow, or `{ "flowUploadId": "...", "flow"?: "..." }` for a [flow bundle](#flow-bundles). `env` passes [secrets and variables](#secrets). Waits up to 110 s, or `waitSeconds` (0 to 300), then answers `running` with a `command_id`. A failed run's result includes `screen_text`, the app's visible text after the failure | +| `GET /device/sessions/:id/commands/:commandId` | The status and result of a `run` that answered `running`. `?waitSeconds=` (0 to 300) waits for it to finish instead of answering at once | +| `GET /device/sessions/:id/inspect` | The screen as compact JSON. The on-screen keyboard and the System UI are left out unless you add `?all=true` | +| `GET /device/sessions/:id/screenshot` | The screen as it is now, as a JPEG data URL. `fresh` is `false` when no new frame came within 3 s, so this is the latest there is | +| `GET /device/sessions/:id/logs` | The installed app's recent device log lines. `lines` (default 200, at most 2000), `level` (`warning` or `error`) and `grep` (any case) narrow them. The answer stops at 24 KB, with `truncated: true` when older lines were left out | + +## Flow bundles + +To run several flows, or a flow that runs others, upload them as a zip, then pass its id to `run` as `flowUploadId`. `flow` names the entry file inside the zip; without it, every flow runs. [`dcd device run `](../cli/dcd-device.md) does this for you. + +| Method and path | What it does | +|---|---| +| `POST /device/bundles/upload-url` | `{ "bytes"?: 1234 }`. Returns the bundle's `id`, and signed URLs to `PUT` the zip to: `upload` and `mirror` (either may be `null`). Upload to each one given | +| `POST /device/bundles` | Register it: `{ "id", "sha", "bytes", "supabase": true, "backblaze": true }`, where `sha` is the SHA-256 of the zip in hex, and `supabase` and `backblaze` say which URLs you uploaded to. Returns `{ "bundle" }` | + +A bundle can be up to 50 MB. Bundles are deleted a day after they're uploaded, once no command needs them. + +## Asking a person + +| Method and path | What it does | +|---|---| +| `POST /device/sessions/:id/request-human` | `{ "reason": "..." }`, up to 500 characters. The session waits for a person, with its idle timer paused, for up to 30 minutes. Your [webhook](../notifications/webhook-notifications.md#device-needs-human) gets a `device.needs_human` event | +| `GET /device/sessions/:id/human` | Whether it's still waiting, or was handed back, with the person's note | +| `POST /device/sessions/:id/hand-back` | `{ "note"?: "..." }`, up to 2000 characters. Hand the session back to the agent | + +Taking control of a device is for people in the console. While someone has control, commands that change the device answer `LIVE_HUMAN_IN_CONTROL`, and those already queued wait until the hand-back. + +## Artifacts + +| Method and path | What it does | +|---|---| +| `GET /device/sessions/:id/timeline` | Every command and event, newest first: who sent it, its status, and the screenshot after it. Paged with `limit` (default 50, at most 200) and `before` (the last page's `next_before`) | +| `GET /device/sessions/:id/artifacts` | Short-lived links to the session's video, device log, and the screenshot and screen dump after each action. Paged the same way. The video and log arrive after the session ends | + +## Secrets + +`env` on `run` is an object of names to string values: up to 50 of them, each up to 4096 characters. Names are letters, digits and `_`, and don't start with a digit. Names starting with `DCD_` are reserved. Refer to them in the YAML as `${NAME}`. They're encrypted before they're stored, never shown in the timeline, and masked in the command's output and in `inspect`. + +A `type` action with `sensitive: true` sends its text the same way. + +## Rate limits + +These endpoints have their own limits instead of the API's general one: + +| What | Limit | +|---|---| +| Commands (install, launch, actions, run, asking a person) | 60 a minute, per session | +| Reads (status, inspect, screenshot, logs, timeline, artifacts) | 240 a minute, per session | +| Session lists, device models and bundle uploads | 120 a minute, per organization | + +Over a limit, the API answers `LIVE_RATE_LIMITED` with how long to wait. Stopping an open session is never refused. + +## Errors + +Errors are JSON with a `message` and a stable `error` code: + +| Code | Meaning | +|---|---| +| `LIVE_NOT_ENABLED` | Device sessions aren't enabled for your organization | +| `LIVE_CONCURRENCY_LIMIT` | Your organization has as many sessions open as it may | +| `LIVE_SPEND_CAP` | Your organization reached its monthly spend cap | +| `LIVE_NOT_FOUND` | No such session in your organization | +| `LIVE_NOT_READY` | The device is still starting, or installing an app: retry shortly | +| `LIVE_SESSION_ENDED` | The session has ended | +| `LIVE_INVALID_ACTION` | The request is malformed: the message says how | +| `LIVE_INVALID_APP` | The build isn't one this device can install | +| `LIVE_APP_DOWNLOAD_FAILED` | The build at the URL couldn't be downloaded | +| `LIVE_NO_APP` | No app is installed, and none was named | +| `LIVE_COMMAND_NOT_FOUND` | No such command in this session | +| `LIVE_LOGS_UNAVAILABLE` | The device logs couldn't be read | +| `LIVE_SESSION_REQUIRED` | MCP only: more than one session is open, so pass `session_id` | +| `LIVE_RATE_LIMITED` | Too many requests: slow down | +| `LIVE_NOT_WAITING_FOR_HUMAN` | Hand-back on a session that isn't waiting for a person | +| `LIVE_HUMAN_IN_CONTROL` | A person has taken control in the console: wait for them to hand back | +| `LIVE_CONSOLE_ONLY` | Only a person signed in to the console can do this (take control) | +| `LIVE_NOT_IN_CONTROL` | Only the person in control can do this (remove a held step) | +| `LIVE_SECRETS_UNAVAILABLE` | Secrets can't be accepted right now | +| `API_KEY_SCOPE` | The API key isn't allowed to reach this endpoint | diff --git a/cli/dcd-device.md b/cli/dcd-device.md new file mode 100644 index 0000000..94539a4 --- /dev/null +++ b/cli/dcd-device.md @@ -0,0 +1,134 @@ +# dcd device + +Lease a cloud iOS simulator or Android emulator, drive it, and release it, from your terminal or a script. These are the same device sessions an AI agent uses through [Agent Devices](../mcp/agent-devices.md): you can watch any of them in the console, and they show on the same Devices page. + +::: warning +**Device sessions are in a closed beta** and are billed at **$0.03/min**, excluding Sales Tax. Access is by request: contact support to have it turned on for your organization. `dcd device` works with a [`dcd login`](dcd-login.md) session or an [API key](../getting-started/api-keys.md). +::: + +```bash +dcd device [flags] +``` + +A typical session: + +```bash +dcd device start --platform ios --app ./build/MyApp.app --wait +dcd device launch +dcd device inspect +dcd device run flows/login.yaml +dcd device screenshot -o after-login.jpg +dcd device stop +``` + +## Rules for every subcommand + +- **The session id is optional** when your organization has exactly one open session. With none, or several, the command stops and lists the sessions to choose from. +- **`--json`** prints exactly one JSON object on stdout, on success and on failure: + + ```json + { "ok": true, "session": { "id": "brave-teal-otter", "state": "active", "...": "..." }, "data": { }, "error": null } + ``` + + `session` is the same session block the [REST API](../api/device-sessions.md) returns: state, time and idle time left, cost so far, warnings, and the console link. `list`, `models` and `stop --all` leave it out, because they aren't about one session. On failure, `ok` is `false` and `error` is `{ "code", "message" }`, where `code` is the API's error code (such as `LIVE_SPEND_CAP`) or `null`. +- **Exit codes:** + + | Code | Meaning | + |---|---| + | `0` | Done | + | `1` | Called wrongly: a bad flag or value, or no session to pick | + | `2` | It ran and failed: a flow or launch failed, or the API couldn't do it | + | `3` | A limit was hit: concurrent sessions, spend cap, or rate limit | + | `4` | The session has ended, or there is no such session | + +- **Progress** goes to stderr, so stdout holds only the result. There are no prompts, and no spinners unless you are at a terminal. + +Every subcommand also takes `--api-key` and `--api-url`. + +## `dcd device start` + +Start a session. Without `--wait` it returns at once, possibly with the session still queued; it starts on its own. + +| Flag | Description | +|------|-------------| +| `--platform ` | The platform. Optional with `--model` or `--app`, which imply it | +| `--model ` | A device model, such as `iphone-16` or `pixel-10`. See `dcd device models` | +| `--os ` | The iOS major version or Android API level, such as `26` or `36` | +| `--google-play` | Android only: an image with Google Play services | +| `--app ` | A build to install: a local `.apk`, `.app`, `.zip` or Expo `.tar.gz` (uploaded first), or an HTTPS link to an `.apk` or a zipped `.app` | +| `--app-binary-id ` | A build you already uploaded | +| `--locale ` | The device locale, such as `de_DE` | +| `--name