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: 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):

{"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": "<base64>"}}
{"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 <id> --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

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) 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 <home>/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.