Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ edit yourself keeps the mode you give it until the next `config set`; run
| `gateway.url` | **Worker side.** Base URL of the gateway this worker joins, e.g. `wss://gw.example:4700`; the worker dials `<url>/v1/uplink` and upgrades it to a WebSocket. Joining grants that gateway the `admin` role on this daemon, and `gateway.token` rides on the upgrade request as a bearer credential, so use `wss://` — or plain `ws://` only over loopback or inside your own tunnel. `http://` and `https://` are rejected at load. Unset means "do not join a fleet": the default, and the only thing that changes about a joined worker. | unset |
| `gateway.token` | **Worker side.** The join token (`simlock token create --role worker`, minted on the gateway) this worker presents when it opens its uplink. Required whenever `gateway.url` is set. `simlock config` shows it; the daemon log and the `daemon.started` event show `[redacted]`. It is stored in plain text in `config.json`, which `simlock config set` writes owner-only. | unset |
| `gateway.label` | **Worker side.** Display name for this worker in `simlock worker list`, `status`, the console, and on the lease's `worker` block. Display-only: nothing routes on it and it need not be unique. | the worker's own id |
| `agentDevice.enabled` | **Worker side.** Run one agent-device daemon on this worker, started and watched by Simlock. It sees Simlock's devices and no others, refuses `boot` and `shutdown`, and keeps its files in `~/.simlock/agent-device`. If agent-device is missing, older than 0.21.25, speaks another RPC protocol or needs a newer Node than Simlock runs on, it is not started and Simlock runs on without it. A daemon a killed Simlock left behind is stopped at the next start. | `false` |
| `agentDevice.enabled` | **Worker side.** Run one agent-device daemon on this worker, started and watched by Simlock. It sees Simlock's devices and no others, refuses `boot` and `shutdown`, and keeps its files in `~/.simlock/agent-device`. If agent-device is missing, older than 0.21.25, speaks another RPC protocol or needs a newer Node than Simlock runs on, it is not started and Simlock runs on without it. A daemon a killed Simlock left behind is stopped at the next start. With `http.enabled` also `true`, the agent-device CLI reaches it at `/v1/agent-device` (see [HTTP-API.md](HTTP-API.md#v1agent-device)). | `false` |
| `agentDevice.command` | **Worker side.** The agent-device executable: a name looked up on `PATH`, or a path. Used only when `agentDevice.enabled` is `true`. | `agent-device` |
| `exec.timeoutMs` | **Worker side.** How long one `device.exec` command (`simlock simctl` / `simlock adb` against a gateway or over HTTP) may run before the worker kills it and the operation fails with `EXEC_TIMEOUT`. Authoritative: it bounds the process that actually runs. | `10 minutes` |
| `gateway.routing` | **Gateway side.** Which routing policy places a queued request on a worker. `warm-then-free` is the only policy in v1. Among the workers that can serve the request, in order: a worker whose health is not `running`, or that has requests of its own waiting, is left out; a worker holding a ready device that fits goes first (the device's mode must fit the request, a request with no mode fits a device that serves the worker's default mode, and its runtime must be the one requested or, with none requested, the one the worker would pick: its default runtime if the model pairs with it, otherwise the only runtime the model pairs with); otherwise a worker with no free running slot for the platform or overall is left out (a running device that is not leased counts as free: the worker shuts it down to make room), a worker under its RAM budget for the platform is preferred over one at it (a worker at its budget is still asked when it is the only one left), and the one with the most free running capacity for the platform wins. A worker that is left out is busy, not unable: the request waits for it. Ties go to the lowest worker id. | `warm-then-free` |
Expand Down
66 changes: 66 additions & 0 deletions docs/HTTP-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,72 @@ from that worker's local agents, each with the **`workerId`** whose queue it
is in. A request the gateway has sent to a worker is listed once, as the
gateway's own. A worker's `createdAt` comes from that worker's clock.

### `/v1/agent-device/*`

A **worker** with `agentDevice.enabled` and `http.enabled` both `true` serves
agent-device's proxy URL here. Point the agent-device CLI at it and give it
your Simlock token:

```bash
export AGENT_DEVICE_DAEMON_AUTH_TOKEN=<simlock token>
agent-device connect proxy --daemon-base-url http://<worker>:<port>/v1/agent-device
agent-device open MyApp --platform ios --device "iPhone 17 Pro"
agent-device snapshot
agent-device close
```

With `agentDevice.enabled` `false`, and on a gateway, every `/v1/agent-device`
URL answers `404`.

- **Every route needs the Simlock token.** No token is `401`, a worker join
token is `403`, and so is an `operator` token, with
`refused: agent-device through simlock needs an agent token`.
- **Each agent needs its own Simlock token.** Simlock tells agents apart by
token: two agents sharing one token share one agent-device session.
- **`open` leases a device from Simlock.** The device is picked by Simlock,
and waits in Simlock's queue, up to 330 seconds, when every match is taken.
The lease is an ordinary Simlock lease: `simlock list` shows it, owned by the
token's principal, and the lease ID is the 32-hex ID the CLI chose. `close`
ends the agent-device lease and then frees the Simlock lease, so Simlock
wipes the device for the next agent.
- **Heartbeats** reach agent-device unchanged. They do not renew the Simlock
lease yet, so a Simlock lease ends at its own TTL; pass `ttlMs` on `open`
for a longer one.

What `open` asks Simlock for:

| The agent passes | Simlock is asked for |
| --- | --- |
| `--platform ios` or `android` | that platform, any device Simlock grants |
| `--target mobile`, `tv`, `desktop` | the class `phone`, `tv`, `desktop` |
| `--device "iPhone 17 Pro"` | that model, and no class: the model wins over `--target` |
| `ttlMs` | that TTL, at least 5 seconds; none sent means `lease.defaultTtlMs` |

Messages the route prints. The agent-device CLI reads the message in the body
(a JSON-RPC error on `/rpc`, `{ "ok": false, "error", "code" }` elsewhere), not
the status.

- `agent-device is not available on this simlock: <reason>`: agent-device is
turned on but missing, too old, starting or crashed. The rest of Simlock
works as before.
- `timed out waiting for a device; nothing was leased`.
- `refused by simlock: <message>`: Simlock refused the lease request, for
example a model it does not know, or a `ttlMs` above `lease.maxTtlMs`.
- `refused: only iOS and Android`, `refused: name --platform ios or android`.
- `refused: simlock picks the device`: `--udid` or `--serial` on `open`.
- `refused: this agent-device is too old for simlock; upgrade it`: the CLI
sent no lease ID.
- `refused: send lease calls as lease RPCs`: a lease call inside
`agent_device.command`.
- `refused: ttlMs must be a positive number of milliseconds`.
- `refused: simlock leases only plain devices`: `backend`,
`retainOnClose: true`, a `provider*` or `aws*` key, or a `leaseProvider`
other than `proxy`.
- `refused: agent-device through simlock needs an agent token`.
- `refused: request body over 1 MiB` and `refused: send one JSON-RPC request`
(a body that is not JSON, a JSON array, or an object with no string
`method`); both answer with `id: null`.

## Errors

Every failure is the same shape the daemon protocol uses:
Expand Down
4 changes: 4 additions & 0 deletions docs/internal/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ modules with.
| `AgentDeviceSupervisor` | `src/agent-device/host/agent-device-supervisor.ts` | Starting, watching and stopping the one agent-device daemon: reaping one a killed simlock left (by a pid record and an identity check), the private state dir and policy file, the daemon's environment, readiness through `daemon.json`, the port and `/health`, restart on death, and the availability state (`off`, `starting`, `running`, `unavailable` with a reason and remedy). | Serve agent-device traffic; report to `simlock doctor`; decide which devices exist (the drivers' environments do). |
| Install lookup, compatibility, daemon environment | `src/agent-device/host/install.ts`, `src/agent-device/shared/` | Finding the agent-device install and its daemon entry; the minimum versions and RPC protocol; the environment and policy data. | Run a process. |
| `AgentDeviceHealthProbe` | `src/agent-device/host/health.ts` | The `GET /health` read, with a time limit. | Judge the answer. |
| Front | `src/agent-device/front/` | The `/v1/agent-device/*` mount: the role check, the `/rpc` body check, turning an agent-device `allocate` into a simlock `lease.request` (and its cancel when the CLI leaves) and a `release` into a `lease.release`, and the front map of the leases it made. | Check a token (the app does); run the proxy; renew leases (#473). |
| `AgentDeviceHost`, `AgentDeviceUpstream` | `src/agent-device/host/host.ts`, `host/upstream.ts` | `@agent-device/proxy` in front of the daemon, one proxy per daemon start; the tenant each request acts as; the port the daemon is reached through and its supervisor adapter. | Know simlock leases. |
| Wire rules | `src/agent-device/shared/` | The path prefixes, the `/rpc` body reader, the call classifier, the lease-call field reader, the reply reader and the front's replies. | Decide what a call means. |
| `FakeAgentDevice` | `src/agent-device/testing.ts` | The in-memory agent-device daemon the proxy sits in front of in tests. | Appear in production code. |

## Drivers

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"mutate": "node scripts/mutate.mjs"
},
"dependencies": {
"@agent-device/proxy": "file:vendor/agent-device-proxy-0.21.27-dev-54e4e4d.tgz",
"@hono/node-server": "2.0.10",
"@hono/zod-validator": "^0.9.1",
"@modelcontextprotocol/sdk": "1.29.0",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading