rackhands No. 107 中文 GitHub

Giving an AI agent your BMCs without giving it the power button

2026-10-12 · How rackhands reads Dell iDRAC, HPE iLO, Lenovo XCC, Supermicro and generic Redfish, falls back to IPMI, and keeps destructive operations behind two keys and a signature.

The problem

A BMC is the most privileged interface a server has. The same Redfish endpoint that tells you a fan is failing will also, given one POST, hard power off a database host. When you wire an AI agent into operations, the useful part is the reads (health, sensors, the hardware event log, firmware versions). The dangerous part arrives in the same credential. Handing an agent ipmitool or curl means nothing stands between "check the fans" and chassis power off.

The second problem is vendors. "Read the hardware event log" is a different URL and a different severity field on every BMC. An agent that improvises Redfish calls will get it subtly wrong, and so will most scripts.

rackhands is one static Go binary that is both a CLI and an MCP server. It answers the reads with one normalised shape across vendors, and it treats every operation that can take a machine down as a box in a vault: closed unless two keys are turned and someone signs the register.

Two keys, one box at a time

Dangerous operations are grouped into classes: power, boot, bmc-reset, firmware, virtual-media. Every class is off by default. With no --allow, the CLI refuses with exit code 3 and the MCP server does not even list the tools, so the agent never sees them.

Turning a class on needs two independent keys:

  1. the runtime key, --allow power (or RACKHANDS_ALLOW), chosen by whoever starts the process;
  2. the per-target key, allow: [power] on that machine in targets.yaml, chosen by whoever owns the inventory.

Both must name the class. A typo in the target name, or a machine nobody opened, is refused before anything is sent. Dangerous operations also take exactly one named target: all, tag:prod or a list of names are rejected. Fan-out of power actions is a deliberate non-feature.

$ rackhands --allow power power db-01 force-off
PLAN  power force-off db-01 (Dell PowerEdge R750, ResetType=ForceOff, currently On)
      POST /redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset {"ResetType":"ForceOff"}
      via redfish
Type the target name (db-01) to confirm: db-01
DONE  db-01: accepted (HTTP 204)

The plan shows the exact method, path and body, the model and the current state. You confirm by typing the machine's name, not by pressing y. Scripts pass --yes db-01, which must also match. Every refusal, plan, execution and failure is appended to a JSONL audit log.

The MCP token: what runs is what was shown

An agent can't type a name into a terminal, and "the model promised it asked the human" is not a control. So over MCP a dangerous tool has two phases. Called without a token, it returns {"status":"confirmation_required","plan":…,"confirm_token":…} and does nothing. The server instructions tell the agent to show the plan and wait for a human yes. Called again with the token, it executes.

The token is HMAC-signed, valid for two minutes and usable once. It is bound to the target, class, action, arguments and the protocol (Redfish or IPMI) that will carry the request. If the agent changes force-off to cycle, retargets db-02, or replays yesterday's token, it is rejected and the rejection is audited. Read tools carry readOnlyHint and dangerous ones destructiveHint, so MCP clients that prompt on destructive tools add a third gate on top.

The recommended setup is plain: give the agent a config that uses a read-only BMC account, and keep a separate, privileged config for humans. The two-key model is the second line of defence, not the first.

Vendor quirks that change the right answer

Redfish is a standard, and every vendor reads it a little differently. These are the differences rackhands encodes, each backed by a mockup and a unit test:

VendorWhat differs
Dell iDRAC 8/9The hardware log is Sel; Lifecycle is separate. Health adds the DellSystem subsystem rollups. Firmware inventory lists Previous- and Available- copies that must be hidden.
HPE iLO 4/5/6Health is AggregateHealthStatus under Oem.Hpe (Oem.Hp on iLO 4). IML severity lives in Oem.Hpe.Severity and entries are not always inlined. Manager.Reset wants no ResetType. Unset thresholds read as 0. Virtual media uses OEM actions.
Lenovo XCC/XCC2The log is PlatformLog. Sensors may be only in the Sensors collection. Every PATCH needs If-Match, or XCC rejects it.
Supermicro X11+The log is Log1 on the manager. Some X11 boards have OEM-only virtual media, reported as unsupported up front rather than failing halfway.

Power actions are mapped to whatever the BMC lists in @Redfish.AllowableValues instead of assuming every BMC accepts GracefulRestart. Units and severities are normalised (OK / Warning / Critical), and event times are UTC.

IPMI fallback, and when not to fall back

Older boards and many homelab machines only speak IPMI 2.0. rackhands carries a pure-Go lanplus client (no ipmitool dependency) for FRU inventory, threshold sensors, SEL, chassis power, boot device and BMC cold reset. With both redfish: and ipmi: configured it tries Redfish first and falls back when the service is unreachable or answers 404, 501 or 503. It never falls back on 401 or 403: a wrong password should be fixed, not hidden by quietly trying a second protocol. Results say via: ipmi (fallback) with the reason.

One lesson from this week: many IPMI clients start a lanplus session by asking the BMC which cipher suites it supports (Get Channel Cipher Suites). Some BMCs, and some ipmi_sim builds, never answer that command, so every session stalled for five timeouts before falling through. rackhands now offers suites 17 and 3 directly, which practically every BMC supports, and CI asserts that an IPMI session opens quickly.

Testing a BMC tool without a rack

Unit tests replay vendor mockups (Dell R750, HPE DL380 Gen10, Lenovo SR650 V2, Supermicro X11) through an in-process Redfish server that records every write, so a test can assert "this PATCH carried If-Match" or "nothing was sent after the refusal". CI then runs the real binary against DMTF's Redfish-Mockup-Server serving those mockups plus DMTF's public-rackmount1, and against OpenIPMI ipmi_sim over a real RMCP+ session. The MCP server is driven over stdio through the full plan → token → done sequence. New vendor quirks are added as mockups first, then code, then a test.

Try it

$ go install github.com/Blackman99/rackhands/cmd/rackhands@latest
$ rackhands --host https://10.20.0.11 --user root health

Or download a release binary for linux, macOS or Windows from GitHub Releases. The docs cover targets.yaml, every command and the MCP setup. If your BMC behaves differently, open an issue with an anonymised Redfish capture. Out of scope by design: BIOS settings, RAID, BMC accounts, KVM/SOL and fleet-wide dangerous operations (GOAL.md).