Imported from cyrilcaoyang/opentrons-server (
AGENTS.md). Install upstream withnpx skills add cyrilcaoyang/opentrons-server. Copyright stays with the author.
AGENTS.md — shared agent instructions (opentrons-server)
Read this before proposing or editing anything. It is the shared,
model-agnostic instruction file for every coding agent working in this repo.
Anything specific to one agent goes in that agent's own file (e.g.
CLAUDE.md), never here.
This repo layers on the canonical base in
ac-organic-lab/AGENTS.md. Read that first for
the lab-wide picture; this file adds only this repo's specifics and never
weakens anything inherited.
1. The binding contract — do not weaken
Three documents are binding and take precedence over everything here. Agents reference them; they do not restate, reinterpret, or work around them.
AGENT_RULES.md— this repo's rules, which link back to the canonicalac-organic-lab/docs/AGENT_RULES.md.ac-organic-lab/docs/STATUS_SPEC.md— the device contract this gateway implements. v1.2, via the sharedsdl-lab-contractpackage pinned to tagv1.2.0.ac-organic-lab/docs/INTERLOCKS.md— the four-layer safety model. This repo owns layers 1 and 2 (hardware limits in the Pydantic request bodies; the device state machine ingateway/service.py). Layers 3 and 4 live inlab-skillsand project repos.
The short list agents most often need:
- This repo is the device side of the boundary, not a caller of it. It
implements
/statusand/control/*; it never orchestrates other devices. - Never report a state the gateway has not observed.
unknownis the honest answer; a convenientreadyis a contract violation (§2.2). - Never derive
activityfromequipment_status— observe it (§2.3). /statusis side-effect-free. Always.- When something is irreversible, ambiguous, or uncovered: stop and ask a human. The absence of a rule is not permission.
2. What this repo is
An AC-conformant REST gateway fronting an Opentrons OT-2 liquid handler. Two instances of this one codebase run as separate services against two different robots.
lab-skills / dashboard / agents this repo robot
──────────────────────────────▶ gateway/api.py (FastAPI)
gateway/service.py (OT2Service) ──SSH──▶ OT-2 REPL
control/http_control.py ──HTTP─▶ robot-server :31950
src/opentrons_server/gateway/— the STATUS_SPEC surface.api.py(routes, auth, CORS),service.py(the state machine and/statusbuilder — 1.8k lines, the heart of the repo),models.py(this gateway's domain vocabulary + re-exported contract types),claims.py,deck.py,tip_state.py,plate_state.py,events_exporter.py.src/opentrons_server/control/— two interchangeable transports to the robot: SSH REPL (ot2_control.py) and the run-engine HTTP API (http_control.py/http_run.py). Their parity is a tested invariant; seedocs/HTTP_SSH_PARITY.mdanddocs/TRANSPORT_TRADEOFFS.md.ui/— the operator SPA (React 19 + Vite + Tailwind 4), built intosrc/opentrons_server/ui_dist/and shipped inside the wheel. The dashboard frames this panel rather than reimplementing it, so it is the operator surface for both robots.docs/— start atdocs/DEVICE_BRINGUP.mdto bring a robot up,docs/DECK_STATE.mdfor the normalized deck model,docs/OT2_TAILSCALE.mdfor Tailscale/SSH on the robots themselves.
3. Working conventions
-
Environment:
uv, on Windows. From WSL the binary is/mnt/c/SDL_Tools/uv.exe; there is nouvon the WSLPATH. -
Extras matter.
uv sync --extra labware— a plainuv syncstripsopentrons-shared-data, which silently empties the/labwarecatalog and the UI's deck-declare picker. Same class of trap as the Cytation's--extra plr. -
OT-2 trash disposal: propose
drop_tipwith only the pipette, for example{"pipette":"right"}, to use the registered fixed trash. Do not address disposal as labware"12"/ well"A1": modern robot servers represent it as thefixedTrashaddressable area, and loading labware into slot 12 fails. Explicit rack/well destinations are for returning tips. -
Tests: use
.venv.test, not.venv../.venv.test/Scripts/python.exe -m pytest tests/unit -q— 532 tests, no hardware, about a minute..venv/is the running services' environment; syncing or installing into it can disturb a live gateway (§4). -
Never actuate hardware to check a change. Everything in
tests/unit/runs againstdry_run=True, mocks, andtests/fixtures/status_*.json. Add a fixture rather than reaching for a robot. The one sanctioned exception istools/ot2-tip-lifecycle-check.ps1below — an operator-run acceptance check, not a development loop. Reach for it to confirm a shipped change behaves on real hardware, never to find out whether your code works. -
Bench tools live in
tools/(PowerShell, run from the device PC). They exist because the equivalent inline one-liner keeps failing: bash expands$varsbefore PowerShell sees them, and nested quotes inside"$( ... )"break its parser.script does needs ot2-preflight.ps1one-screen state of both gateways before a session; flags a tip left on a head and whether the volume guard is actually live nothing — read-only ot2-tip-lifecycle-check.ps1picks one tip and returns it on Complexation, printing the rack at each step; plan-and-stop unless -Run; homes before releasingresolves its own API key; actuates ot2-enable-assistant.ps1toggles OT2_ASSISTANT_ENABLEDfor one gateway without destroying the rest of its service envelevation (RDP session) ot2-forget-stale-racks.ps1retires tracked tip racks whose slot the deck says holds no rack (a moved rack leaves one, and auto-pick will still send the head there); marks them empty, releasing any mount they explain; plan-and-stop unless -Runresolves its own API key; metadata only, no motion — so unlike the tip check it does not refuse HTE ot2-set-robot-url.ps1repoints one gateway at its robot ( OT2_HTTP_BASE_URL, optionallyOT2_HOST_ALIAS) preserving the other 13 env vars, then re-probes and reports whether the robot actually answered — a stale address hides behind a live session and only surfaces on the next restart; plan-and-stop unless-Run-Runneeds elevation (RDP session); the dry run does notRun them by absolute path:
powershell -NoProfile -ExecutionPolicy Bypass -File <path>. Two traps they encode, worth knowing before writing another:nssm set AppEnvironmentExtrareplaces the whole variable block rather than appending, andnssm getwrites to stderr even when it succeeds — which is a terminating error underErrorActionPreference = "Stop"despite2>$null. -
Live testing happens on Complexation only, through the edge-gated panel at
http://100.64.254.6/ot2/complexation/ui/. Never HTE (ot2_hte, :8020) — it runs real campaigns. This applies to any hands-on check: manual clicks,curlagainst/control/*, bench acceptance. The two deploy checkouts track the same branch, so a change reaches both robots; the testing does not. -
UI:
cd ui && npm run typecheck/npm run build. The build must be committed asui_dist/for the wheel to serve/ui. -
Fail-fast style. Do not add defensive code that swallows exceptions and hides failures — on this device a swallowed error becomes a robot whose state nobody can trust. Report truthfully.
-
Agent API discovery:
/docs/agentserves a read-only equipment guide;/openapi.jsondescribes HTTP routes and/plans/actionssupplies proposal schemas. The built-in assistant and agent MCP exposeget_equipment_docs. Python transport methods are not automatically gateway endpoints; verify the route and plan catalogs before advertising a capability. -
Robot profiles: OT-2 is the default. A separate Flex instance sets
OT2_ROBOT_MODEL=FlexandOT2_TRANSPORT=httpbefore process startup; deck slots and schemas are selected at import. Use the shared profile, not hard-coded numeric slots. Seedocs/FLEX_HTTP_SUPPORT.mdfor limits. -
Direct motion: preserve
force_directon pipette moves. Flexrobot/moveToplans an arc; direct gripper motion usesrobot/moveAxesToorrobot/moveAxesRelative. Substituting these changes the physical path. -
Stop recovery:
ot2_stop_state.jsonbeside the tip-state file (overrideOT2_STOP_STATE_PATH) blocks automatic reconnect after a software stop. Give each gateway its own state paths; do not delete this latch to recover. -
Prefer reading source in
.venv/Lib/site-packages/over searching online for a dependency's usage (sdl_lab_contract,paramiko,opentrons).
4. Recurring pitfalls (project-specific)
-
This tree does not serve any robot. Since 2026-08-08 each gateway runs from its own deploy checkout, with its own venv:
service port runs from ot2-gateway-hte8020 C:\SDL_Deploy\ot2-hteot2-gateway-complexation8021 C:\SDL_Deploy\ot2-complexationEditing
Projects\opentrons-serveris therefore safe — it changes nothing a robot runs. Deploying is an explicit act, in the deploy checkout:git pull→uv sync --extra labware→nssm restart <svc>. Rollback isgit checkout <old-ref>there plus a restart, and it moves one robot without touching the other.Before this, both services ran
uv run --projectout of this tree, so a restart — from a crash, a reboot, anyone's strayuv run— silently deployed whatever was on disk, committed or not. On 2026-08-07 both robots picked up uncommitted work mid-session and ended on different builds of it, because they restarted at different moments;git stashwas unsafe for the same reason. Do not repoint a service back at this tree. -
A commit is still not a deployment. A deploy checkout only moves when someone pulls, so a merged fix can sit unshipped and the two robots can sit on different commits indefinitely. Confirm on the wire (
/status,/openapi.json) before believing a bug is in the source, and check where a service actually runs from withnssm get <svc> AppDirectory. -
Python is pinned to 3.12 by
requires-python = ">=3.10,<3.13"— the upper bound is what makesuv venvchoose 3.12 in a fresh deploy checkout (.python-versionis gitignored, so it does not travel).opentrons-shared-datapullsnumpy~=1.26.4, which has no wheel past cp312; without the pin a fresh venv picks 3.14, tries to build numpy from source, and fails for want of MSVC. This broke the first deploy-checkout build. -
AppEnvironmentExtrareplaces the whole variable block. It does not add one variable — it replaces all of them, state paths included. Read the current block, append, write it back, and verify the variable count before restarting. -
uv synccan break a running service. If a release adds or bumps a dependency, uv must replace the console-script.exethe running service holds open, aborts the whole transaction onos error 32, and can leave the new dependency not installed at all while the service keeps running off memory-resident code. Stop only that service first, sync, start. Full recovery notes inDEVICE_PC_SETUP.md§8. -
Elevation and cache ACLs. Never run
uv syncfrom an elevated shell — it poisons the uv cache's ACLs for the service accounts. UAC prompts only appear in an RDP session, so an elevated command from a headless shell simply hangs. -
Python version is pinned by the labware extra.
opentrons-shared-datapullsnumpy~=1.26.4, which has no wheels past cp312. Do not bulk-upgrade this venv's Python. -
Single-PC concentration. xArm (8000), PlateLoc (8010), both OT-2 gateways (8020/8021), and Cytation 5 (8040) all live on
sdl2-pc-03-cytation. One reboot takes out five workflow-critical services, and the USB-enumeration race on boot is a known failure mode for the serial devices. -
Complexation's robot is reached through the UPLC PC, not its tailnet IP.
ot2traininghas no lab Ethernet; its Wi-Fi radio drops on its own after a reboot (wlan0disconnected, empty scan) and only a hard reboot brings it back. Since 2026-09-05 the gateway points at thenetshUSB bridgehttp://100.64.254.19:31951(DEVICE_BRINGUP.md Network paths). Since 2026-09-06 the gateway watches the path itself: three failed probes (~15 s) flip/statustounknownwithcomponents.robot: unreachableanddetails.robot.readback_age_s, robot-touching actions are refused up front, and a robot that rebooted during the outage gets its session rebuilt (README Robot reachability). Before that,readyanddetails.robot.reachable: truestayed frozen for hours after the robot vanished — do not trust envelopes from older builds on this point. -
The SSH REPL breaks on the first
>>>prompt. Snapshot reads are sent as two separateinvokes routed throughcompile()/exec()for exactly this reason (service.py_REMOTE_SNAPSHOT_*). Do not "simplify" them into one multi-statement send. -
equipment_versionis the gateway's, not the robot's. The robot'sapi_versionlives indetails.robot. These were conflated until 2026-08-07; stored history before that date has the robot's number.
5. Memory & instruction policy
Inherited from the canonical base; the scope boundaries for this repo:
AGENTS.md(this file) — durable, model-agnostic repo knowledge: conventions, commands, architecture facts, recurring pitfalls. When you learn one, update this file.AGENT_RULES.md— binding rules; changes only when a human asks.CLAUDE.md— Claude-Code-specific only. Nothing another agent needs.- Cross-repo or device-PC-wide facts (the shared
sdl2-pc-03layout, uv/ NSSM behaviour, other repos' roles) belong in the agent's global memory, proposed for approval — not written into this repo. - Never commit temporary debugging notes, stale TODOs, or one-off observations to any instruction file.
6. Safety protocol for edits outside this repo
Before editing anything outside this repository — another device repo,
../ac-organic-lab, ~/.claude, service configs on the device PC — first show
the human the exact path, the reason, the proposed change, and whether it
affects only this repo or future global behavior. Do not proceed until they
approve. In particular, ../ac-organic-lab is the central server's repo
mirrored here: contract changes are preferably made there, and any edit from
this PC must be coordinated so the two do not diverge.