# The agent and the client The remote user of a bench is the developer running the install → update → field-test loop from their own machine. This page is the mechanism behind [Work from another machine](../usage/remote.md): the two distributions, the wire, and the one place the design deliberately breaks its own rule. ## Two deliverables, one repository | | `plesty.bench` | `plesty.server` | |---|---|---| | What | The client: `BenchClient` + the `plesty-bench` command | The bench application: model, services, presenter, CLI, console, agent | | Ships as | wheel on PyPI, distribution **`plesty-bench`** | wheel on PyPI, distribution **`plesty-server`** (assembled by `packaging/server_dist.py`); light installers run `uv tool install "plesty-server[gui]"`; a source checkout (`uv sync`) for development | | Depends on | `pyzmq`, `click` | `plesty-lib`, `pyyaml`, `pyzmq`, `click` (+ `plesty-lib[gui]` for the console) | | Imports | nothing from `plesty.server` | `plesty.bench` (the console's remote mode) | `pyproject.toml` makes the split: `only-include = ["plesty/bench"]` for this project's wheel, `plesty-bench` as its only console script; the server's dependencies are the `server` extra (and the `dev` group, so `uv sync` in a checkout has everything). The version and the CHANGELOG are shared, and `describe` reports the server version so a client can tell what it is talking to. The point of the split is that a developer machine that only installs, updates and field-tests pulls `pyzmq` and `click` and nothing else — no Qt, no plesty-lib, none of the bench application. ## The contract: the CLI is the API The agent accepts a `plesty-server` argument list and returns the exit code and the output. Anything the command line can do is remote, with no second schema to keep in step. Four request types on the plesty-lib device-server wire (ZMQ DEALER → ROUTER, JSON, `status: ok|error`): ```text {"type": "describe"} → {"status": "ok", "result": {"kind": "plesty-server-agent", "version": "0.1.0", "home": "…", "fleet": "…", "commands": ["declare", …], "requests": ["describe", "exec", "fetch", "device"]}} {"type": "exec", "argv": ["status", "--json"], "token": "…"} → {"status": "ok", "result": {"code": 0, "stdout": "[…]", "stderr": ""}} {"type": "fetch", "path": "repos/plesty-pm100d/exp/reports/pm100d-s120c/field-test.json", "token": "…"} → {"status": "ok", "result": {"path": "…", "size": 2210, "data": ""}} {"type": "device", "name": "pm100d", "request": {"type": "query", "param": "wavelength"}, "token": "…"} → the device server's own reply, forwarded unchanged ``` Because it is the device-server wire, `plesty.lib.service.build_client` and any `describe` probe understand the agent without `plesty-bench` at all. Long work fits without streaming: `install`, `update` and `field-test --no-wait` return a job id, and `jobs log --offset N --json` returns `{text, offset, running, returncode, state}` — `BenchClient.follow()` polls that until the job ends. So a dropped connection costs the output and not the run. `list`, `status`, `reports` and `jobs list` take `--json` for structured results. ## The one exception: `device` `device` relays a request to a declared device's own server. It breaks the rule above deliberately, for two reasons. **`exec` cannot carry it.** A control panel is a long-lived session of many small round trips — a poll per parameter per second — and `exec` answers each one by running the click application. A schema fetch and a stream of reads is not a command invocation. **Nor should the client reach the device directly.** A device server binds `tcp://*`, so it is reachable across the network already — but it checks **no token**. The agent does. Routing through it keeps one authenticated door for a bench instead of one unauthenticated one per instrument, and confines what a client can reach to the devices the bench is actually running. The reply is the device server's own envelope, passed through: an instrument that refused a write says so in its own words rather than as a relay failure. `BenchClient.device(name)` wraps it as a `RemoteDevice` — `describe`, `query`, `write`, `call` — which is exactly plesty-lib's `DeviceHandle`, so the same `DevicePanel` the console opens beside an instrument is driven from a laptop. (one-door-and-what-it-does-not-decide)= ## One door, and what it does not decide The agent compares one shared secret in constant time and learns nothing else about who is calling. That is enough for what it guards — a bench on a lab network — and it is why the console's **master** and **node** roles ([Several benches](../usage/remote.md#driving-other-benches-from-this-console)) are a configuration convention rather than a permission. A master is a bench that has been told to keep a list of other benches and show their devices. Nothing on the wire distinguishes its `exec` from anyone else's: a node cannot tell the master from a shell with the same token, so it cannot refuse one and serve the other. Two benches can each declare themselves master of the same nodes, and both will work. Making it a permission needs a second, control-scoped secret — one the node checks on `device` and on the lifecycle commands, and hands to nobody but its master. That is a change to this page's contract and is deliberately not made here: the asymmetry it would enforce is, today, an asymmetry of configuration. ## Serving it The agent runs three ways, all the same service: `plesty-server agent`, `fleet run --agent` in the headless loop, and a thread of the Qt console (*Server ▸ Start agent*). One service means no second process to supervise and no way for the surfaces to disagree. Settings are per bench — not per fleet and not packaged — so they live in the home as `/agent.yaml` (bind, token, workers, role, Plesty share and bench folder; owner-readable). Every command-line option overrides the file. Saving does not move a *running* agent: its clients connected to the endpoint it bound, with the token it started with, so the console asks for a stop and a start instead. - Every `exec` runs the click application **in-process** with captured output and the agent's `--home`/`-f` prepended — a fresh `Bench` per command, so the agent and a local CLI see the same files. - Requests run on a worker pool; a `describe` is answered while a `start` waits for its probe. Replies go back through the socket thread. - `exec` refuses `agent` and `gui`; `fetch` is confined to the bench home and capped at 16 MB; `device` reaches only the declared devices this bench is running; the token is compared in constant time. - Same trust model as the device servers: the lab network, no TLS. ## Without an agent `ssh lab-bench-03 plesty-server status` needs neither the agent nor `plesty-bench` — only OpenSSH on the bench, which Windows has too. It is the fallback where a second listening port is unwelcome.