> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lagerdata.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Version 0.47.1

> September 11, 2026

## <u>Features</u>

* **`LAGER_GATEWAY_TOKEN` authenticates a CI job against an access-gated box.** Set the
  variable to a token that your auth server minted, and every request carries it. The job
  needs no `lager login` step and no password in repository secrets. The token outranks
  any stored session. Lager never refreshes it, and writes nothing to
  `~/.lager_gateway_auth`, so a self-hosted runner keeps no credential and no stale
  box-to-server mapping between jobs. If the gateway refuses the token, the command stops
  at once and names the server that refused it. It does not advise a login that cannot
  help. The Rust crate reads this variable since 0.2.0, and the Python CLI now matches it.

* **The reference docs cover what shipped from v0.40.0 through v0.47.0.** Each claim
  matches the v0.47.0 source rather than the release notes. New or expanded sections:
  * `lager nets state`, which shipped in 0.34.0 and appeared on no page. The section
    covers state strings, the shared 8 s budget, reason codes with their remedy lines,
    and the `--json` fields.
  * The LabJack U3 across the adc, dac, gpi, gpo, spi, i2c, nets, instruments and
    supported-instruments pages. These pages cover the pins and the SPI pin order, the
    50-byte transfer limit, approximate clock rates, and the 0.04-4.95 V DAC range. They
    also cover external I2C pull-ups, the refused chip-select options, and the LJM
    `"ANY"` limitation.
  * `lager box-config`: the `apply` exit codes, every refusal of the host-network
    pre-flight, `network-mode show` output, switching back, `pip import-legacy`, and
    `LAGER_DISABLE_UART_SERVICE`.
  * DA1469x flash and erase on both debug backends, `DebugNet.connect()` errors and
    backend-specific parameters, and the `erase` and `flash` exit codes.
  * `lager uart --sessions` and `--force`, `lager usb cycle` verdicts per hub,
    `lager webcam snapshot`, `lager exec` inside a CI job with `LAGER_CI_OVERRIDE`, and
    robot-arm detection with `LAGER_ARM_PROBE`.
  * `/etc/lager/ref`, `update --check` exit codes, the install deploy timeout, and the
    pre-built image rules. The host CLI, how a command recognizes its own lock,
    `setup_battery()`, and where the box services write their logs.

## <u>Bug Fixes</u>

* **`lager install` no longer hangs for two minutes at the end, and no longer garbles the
  terminal.** The step that records the deployed version and ref forced an SSH
  pseudo-terminal. When its `sudo` was not passwordless, that step waited on a password
  that no one can type, until a 120-second timeout. The killed pseudo-terminal then left
  the local shell in raw mode, and every line after it staircased. The writes now run
  under `BatchMode` with a short timeout, so an ungranted `sudo` fails at once and
  cleanly. The box's sudoers grant now also covers `/etc/lager/ref`, which it never
  covered before, so on a properly provisioned box the write simply succeeds.

* **`lager install` and `lager update` no longer take ownership of the SSH key
  directory** (`/etc/lager/authorized_keys.d`). The permission repair that makes
  `/etc/lager` writable for the box-config renderers used to `chown -R` the whole tree.
  That swept the key directory into the container's ownership. On a box that runs
  untrusted code, that code can then authorize its own SSH key. The repair now skips that
  one directory, and leaves its ownership as it found it.

* **`lager install` and `lager update` no longer abort on a box whose clone holds a
  divergent tag.** A box cloned before a tag was re-created upstream keeps that tag at
  the old object. An unforced `git fetch` then refuses the tag with a non-zero exit, and
  reports that the fetch clobbers an existing tag. The install's fetch sits in an `&&`
  chain under `set -e`, so the deploy died at *\[5/8] Deploying Box Code*. It stayed dead,
  because every later attempt fetched the same way. The only way out was a force-fetch on
  the box by hand. Both fetches now force. Origin is authoritative for a checkout that
  the very next commands `git reset --hard` and `git clean -fd`.

* **`lager arm set-acceleration` set travel acceleration to the retract value.** It sent
  `M204 P T T`, and the arm firmware reads retract acceleration from `R`. The command and
  `Dexarm.set_acceleration()` now send `M204 P T R`.

* **`lager arm read-and-save-position` asks for confirmation.** The command sends `M889`,
  which replaces the stored calibration of the arm with its current pose. Pass `--yes` to
  skip the prompt.

* **An arm net opens the arm that it names.** `lager nets add-all` records the USB serial
  number of the arm only in the net address. The box ignored that number and opened the
  first `0483:5740` serial device. Other STM32 boards use that USB ID.

* **`lager arm` works again after the USB connection to the arm drops.** Before, every
  command failed until the box hardware service restarted. Now the next command opens the
  port again, and `position` retries once.

* **An instrument scan no longer breaks a running `lager arm` command.** The scan sent
  `M105` to an arm that a saved arm net uses, and the arm reply can go to the scan
  instead. The scan now lists that arm from its USB serial number, without a probe.

* **`lager arm move` and `move-by` refuse a timeout longer than 25 seconds.** A longer
  move outlived the 30-second limit on a box hardware call, and the box hardware service
  restarted.

* **`Dexarm.move_to()` and `move_relative()` check the workspace bounds.** They raise
  `OutOfBoundsError` and send nothing, as `lager arm` does.

* **`lager arm --box` lists arm nets without locking the box.**

* **`lager arm go-home` and `Dexarm.go_home()` wait for the arm to reach home.** They
  returned after half a second, before the arm reached home. Now they return when the arm
  firmware reports that the move is done.

* **`lager arm move` and `move-by` refuse an arm on firmware older than V2.1.4.** Rotrics
  swapped the X and Y axes in V2.1.4, so on older firmware a move turned the arm to the
  side. The error tells you to update the firmware. `position` and `go-home` still work.

* **A move before the arm is homed fails at once.** After power-on the firmware ignores
  motion until `go-home` runs, and the move timed out after 15 seconds with a message
  about an obstruction. It now raises `NotHomedError`.

* **A move that times out no longer blames the workspace bounds.** The command checks the
  bounds before it sends a move. The error now says that the arm did not arrive, and that
  a long move can need a larger timeout.

* **`lager arm position` shows the arm error when the arm stops answering.** It failed
  with "hardware service did not respond" after 10 seconds. The box now stops a position
  read after three attempts of 3 seconds each.

## <u>Improvements</u>

* **Four pages said that the host firewall limits the Lager ports to the VPN.** On the
  default network, Docker publishes those ports ahead of the host firewall, as
  `SECURITY.md` states. The install, update, setup and architecture pages now say so, and
  link the Security Model. `update.mdx` also documented a firewall step and a script
  invocation that do not exist.

* **The Logic Analyzer pages said that the feature was not available.** The commands run.
  Both pages, and the scope page, now carry one callout for the MSO5000 triggers and bus
  decoders that still fail. The pages also list exit codes and valid ranges, and drop the
  examples that cannot run.

* **Examples that cannot run are corrected.** `lager debug flash --bin` takes
  `FILE,ADDRESS`, not `ADDRESS FILE`. `lager python --kill` needs the process ID.
  `lager uart` takes a net name, not `/dev/ttyUSB0`. `lager exec` needs `--` before extra
  arguments that start with a dash. `UARTNet.connect()` ignores `timeout`.
  `lager nets add` examples used channel `0` where the box lists `I2C0`, `SPI0` or
  `FIO4-FIO5`.

* **Stale claims are corrected.**
  * pyOCD appeared as a debug backend. J-Link and OpenOCD are the only two.
  * Webcam ports start at 8086, not 8081.
  * `lager usb` commands are case-sensitive, and the `cycle` timing and hub hold apply to
    Plugable docks only.
  * A LabJack U3 supports SPI and I2C.
  * A second role on a Keithley or EA instrument raises a notice, not a block, and two
    Acroname hubs of one model both work.
  * The `.lager` defaults are JSON in `~/.lager`, not INI in the project.
  * Sample output for `lager hello`, `lager boxes`, `lager defaults list`,
    `lager instruments`, `lager logs` and `lager binaries list` matches the CLI.
  * The box runs one `lager` container, not `controller`.
  * The MCP server is read-only only while both opt-in gates are off.
  * Top-level `lager` options are `--version`, `--debug` and `--colorize`. `--box`
    belongs to each command.

* **The README describes what the software does.** The Python example calls
  `psu.set_voltage(3.3)`, because `voltage()` only reads the measured value. The MCP
  section lists the read-only discovery and planning tools that the server exposes by
  default, and names the two opt-in gates. The Rust section places the `lager-net` crate
  in a firmware project, not on the box. The CI section passes `--yes` to the commands
  that ask for confirmation. The Supported Hardware table covers the vendor families that
  the supported-instruments page lists.

## <u>Installation</u>

To install this version:

```bash theme={null}
pip install lager-cli==0.47.1
```

To upgrade from a previous version:

```bash theme={null}
pip install --upgrade lager-cli==0.47.1
```

Then update your boxes:

```bash theme={null}
lager update --box <box-name>
```

## Resources

[View Release on PyPI](https://pypi.org/project/lager-cli/0.47.1/)
