embedded-lab-control 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- embedded_lab_control-0.2.0/CHANGELOG.md +1514 -0
- embedded_lab_control-0.2.0/LICENSE +21 -0
- embedded_lab_control-0.2.0/MANIFEST.in +20 -0
- embedded_lab_control-0.2.0/PKG-INFO +855 -0
- embedded_lab_control-0.2.0/README.md +808 -0
- embedded_lab_control-0.2.0/SECURITY.md +110 -0
- embedded_lab_control-0.2.0/config/labctl.yaml.example +197 -0
- embedded_lab_control-0.2.0/config/systemd/labctl-mcp.service +31 -0
- embedded_lab_control-0.2.0/config/systemd/labctl-monitor.service +31 -0
- embedded_lab_control-0.2.0/config/systemd/labctl-web.service +38 -0
- embedded_lab_control-0.2.0/config/udev/port-mapping.yaml.example +35 -0
- embedded_lab_control-0.2.0/docs/AGENT_RULES.md +602 -0
- embedded_lab_control-0.2.0/docs/CLAUDE_CODE_AGENTS.md +430 -0
- embedded_lab_control-0.2.0/docs/DECISIONS.md +285 -0
- embedded_lab_control-0.2.0/docs/HARDWARE_MAP.md +213 -0
- embedded_lab_control-0.2.0/docs/IMPLEMENTATION.md +956 -0
- embedded_lab_control-0.2.0/docs/MCP_SERVER.md +916 -0
- embedded_lab_control-0.2.0/docs/OPERATIONS.md +374 -0
- embedded_lab_control-0.2.0/docs/RELEASING.md +115 -0
- embedded_lab_control-0.2.0/docs/RESOLVED_BUG_ser2net_missing_dev_prefix.md +75 -0
- embedded_lab_control-0.2.0/docs/RESOLVED_FEATURE_sdwire_power_safety.md +90 -0
- embedded_lab_control-0.2.0/docs/RESOLVED_SPEC_activity_stream.md +341 -0
- embedded_lab_control-0.2.0/docs/RESOLVED_SPEC_claims.md +642 -0
- embedded_lab_control-0.2.0/docs/RESOLVED_SPEC_sdwire_read_access.md +297 -0
- embedded_lab_control-0.2.0/docs/SPEC_actuators.md +527 -0
- embedded_lab_control-0.2.0/docs/SPEC_storage_device_guardrails.md +371 -0
- embedded_lab_control-0.2.0/pyproject.toml +105 -0
- embedded_lab_control-0.2.0/scripts/discover-usb-serial.sh +86 -0
- embedded_lab_control-0.2.0/scripts/generate-udev-rules.py +270 -0
- embedded_lab_control-0.2.0/scripts/install-services.sh +233 -0
- embedded_lab_control-0.2.0/scripts/install-udev.sh +101 -0
- embedded_lab_control-0.2.0/scripts/kasa-debug.py +393 -0
- embedded_lab_control-0.2.0/scripts/update.sh +445 -0
- embedded_lab_control-0.2.0/setup.cfg +4 -0
- embedded_lab_control-0.2.0/skills/deploy-and-test.md +176 -0
- embedded_lab_control-0.2.0/src/embedded_lab_control.egg-info/PKG-INFO +855 -0
- embedded_lab_control-0.2.0/src/embedded_lab_control.egg-info/SOURCES.txt +143 -0
- embedded_lab_control-0.2.0/src/embedded_lab_control.egg-info/dependency_links.txt +1 -0
- embedded_lab_control-0.2.0/src/embedded_lab_control.egg-info/entry_points.txt +2 -0
- embedded_lab_control-0.2.0/src/embedded_lab_control.egg-info/requires.txt +33 -0
- embedded_lab_control-0.2.0/src/embedded_lab_control.egg-info/top_level.txt +1 -0
- embedded_lab_control-0.2.0/src/labctl/__init__.py +8 -0
- embedded_lab_control-0.2.0/src/labctl/actuators/__init__.py +56 -0
- embedded_lab_control-0.2.0/src/labctl/actuators/base.py +183 -0
- embedded_lab_control-0.2.0/src/labctl/actuators/lcus1.py +232 -0
- embedded_lab_control-0.2.0/src/labctl/actuators/mock.py +81 -0
- embedded_lab_control-0.2.0/src/labctl/actuators/runtime.py +1178 -0
- embedded_lab_control-0.2.0/src/labctl/cli.py +7485 -0
- embedded_lab_control-0.2.0/src/labctl/core/__init__.py +36 -0
- embedded_lab_control-0.2.0/src/labctl/core/admin_files.py +388 -0
- embedded_lab_control-0.2.0/src/labctl/core/audit.py +269 -0
- embedded_lab_control-0.2.0/src/labctl/core/claim_keepalive.py +342 -0
- embedded_lab_control-0.2.0/src/labctl/core/config.py +1068 -0
- embedded_lab_control-0.2.0/src/labctl/core/database.py +880 -0
- embedded_lab_control-0.2.0/src/labctl/core/images.py +503 -0
- embedded_lab_control-0.2.0/src/labctl/core/manager.py +2815 -0
- embedded_lab_control-0.2.0/src/labctl/core/models.py +815 -0
- embedded_lab_control-0.2.0/src/labctl/core/safe_open.py +109 -0
- embedded_lab_control-0.2.0/src/labctl/extras.py +128 -0
- embedded_lab_control-0.2.0/src/labctl/health/__init__.py +43 -0
- embedded_lab_control-0.2.0/src/labctl/health/alerts.py +275 -0
- embedded_lab_control-0.2.0/src/labctl/health/checks.py +533 -0
- embedded_lab_control-0.2.0/src/labctl/health/daemon.py +397 -0
- embedded_lab_control-0.2.0/src/labctl/mcp_server.py +5254 -0
- embedded_lab_control-0.2.0/src/labctl/power/__init__.py +13 -0
- embedded_lab_control-0.2.0/src/labctl/power/base.py +145 -0
- embedded_lab_control-0.2.0/src/labctl/power/kasa.py +446 -0
- embedded_lab_control-0.2.0/src/labctl/power/shelly.py +89 -0
- embedded_lab_control-0.2.0/src/labctl/power/tasmota.py +95 -0
- embedded_lab_control-0.2.0/src/labctl/sdwire/__init__.py +10 -0
- embedded_lab_control-0.2.0/src/labctl/sdwire/controller.py +1116 -0
- embedded_lab_control-0.2.0/src/labctl/sdwire/power_guard.py +190 -0
- embedded_lab_control-0.2.0/src/labctl/serial/__init__.py +18 -0
- embedded_lab_control-0.2.0/src/labctl/serial/boot_test.py +342 -0
- embedded_lab_control-0.2.0/src/labctl/serial/capture.py +430 -0
- embedded_lab_control-0.2.0/src/labctl/serial/keys.py +89 -0
- embedded_lab_control-0.2.0/src/labctl/serial/proxy.py +802 -0
- embedded_lab_control-0.2.0/src/labctl/serial/ser2net.py +338 -0
- embedded_lab_control-0.2.0/src/labctl/serial/udev.py +273 -0
- embedded_lab_control-0.2.0/src/labctl/services.py +210 -0
- embedded_lab_control-0.2.0/src/labctl/web/__init__.py +9 -0
- embedded_lab_control-0.2.0/src/labctl/web/activity_broadcaster.py +204 -0
- embedded_lab_control-0.2.0/src/labctl/web/api.py +1003 -0
- embedded_lab_control-0.2.0/src/labctl/web/app.py +202 -0
- embedded_lab_control-0.2.0/src/labctl/web/auth.py +97 -0
- embedded_lab_control-0.2.0/src/labctl/web/claims.py +232 -0
- embedded_lab_control-0.2.0/src/labctl/web/static/css/style.css +477 -0
- embedded_lab_control-0.2.0/src/labctl/web/static/js/app.js +76 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/activity.html +202 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/base.html +49 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/claim_refused.html +38 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/console.html +237 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/dashboard.html +100 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/login.html +25 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/sbc_detail.html +306 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/settings.html +78 -0
- embedded_lab_control-0.2.0/src/labctl/web/templates/status_history.html +65 -0
- embedded_lab_control-0.2.0/src/labctl/web/views.py +496 -0
- embedded_lab_control-0.2.0/src/labctl/web/websocket.py +208 -0
- embedded_lab_control-0.2.0/tests/__init__.py +0 -0
- embedded_lab_control-0.2.0/tests/conftest.py +142 -0
- embedded_lab_control-0.2.0/tests/fixtures/kasa_hs300_fw112.json +937 -0
- embedded_lab_control-0.2.0/tests/integration/__init__.py +0 -0
- embedded_lab_control-0.2.0/tests/integration/test_actuator_cli.py +1097 -0
- embedded_lab_control-0.2.0/tests/integration/test_auth.py +401 -0
- embedded_lab_control-0.2.0/tests/integration/test_cli.py +2606 -0
- embedded_lab_control-0.2.0/tests/integration/test_cli_boot_test_claims.py +207 -0
- embedded_lab_control-0.2.0/tests/integration/test_cli_claim_enforcement.py +1346 -0
- embedded_lab_control-0.2.0/tests/integration/test_cli_destructive.py +333 -0
- embedded_lab_control-0.2.0/tests/integration/test_pre_power_paths.py +1151 -0
- embedded_lab_control-0.2.0/tests/integration/test_web.py +933 -0
- embedded_lab_control-0.2.0/tests/integration/test_web_claim_enforcement.py +860 -0
- embedded_lab_control-0.2.0/tests/unit/__init__.py +0 -0
- embedded_lab_control-0.2.0/tests/unit/test_activity_broadcaster.py +197 -0
- embedded_lab_control-0.2.0/tests/unit/test_actuator_driver.py +108 -0
- embedded_lab_control-0.2.0/tests/unit/test_actuator_manager.py +484 -0
- embedded_lab_control-0.2.0/tests/unit/test_actuator_runtime.py +1492 -0
- embedded_lab_control-0.2.0/tests/unit/test_admin_files.py +459 -0
- embedded_lab_control-0.2.0/tests/unit/test_audit.py +628 -0
- embedded_lab_control-0.2.0/tests/unit/test_boot_test.py +533 -0
- embedded_lab_control-0.2.0/tests/unit/test_claim_enforcement.py +719 -0
- embedded_lab_control-0.2.0/tests/unit/test_claim_keepalive.py +438 -0
- embedded_lab_control-0.2.0/tests/unit/test_config.py +857 -0
- embedded_lab_control-0.2.0/tests/unit/test_database.py +1005 -0
- embedded_lab_control-0.2.0/tests/unit/test_extras.py +345 -0
- embedded_lab_control-0.2.0/tests/unit/test_health.py +706 -0
- embedded_lab_control-0.2.0/tests/unit/test_images.py +1497 -0
- embedded_lab_control-0.2.0/tests/unit/test_lcus1_driver.py +415 -0
- embedded_lab_control-0.2.0/tests/unit/test_manager.py +2393 -0
- embedded_lab_control-0.2.0/tests/unit/test_mcp_claim_keepalive.py +399 -0
- embedded_lab_control-0.2.0/tests/unit/test_mcp_http_auth.py +1114 -0
- embedded_lab_control-0.2.0/tests/unit/test_mcp_server.py +5717 -0
- embedded_lab_control-0.2.0/tests/unit/test_operations_doc.py +83 -0
- embedded_lab_control-0.2.0/tests/unit/test_power.py +936 -0
- embedded_lab_control-0.2.0/tests/unit/test_proxy.py +633 -0
- embedded_lab_control-0.2.0/tests/unit/test_safe_open.py +287 -0
- embedded_lab_control-0.2.0/tests/unit/test_sdwire.py +1375 -0
- embedded_lab_control-0.2.0/tests/unit/test_sdwire_power_guard.py +271 -0
- embedded_lab_control-0.2.0/tests/unit/test_ser2net.py +423 -0
- embedded_lab_control-0.2.0/tests/unit/test_serial_capture.py +567 -0
- embedded_lab_control-0.2.0/tests/unit/test_serial_capture_parts.py +62 -0
- embedded_lab_control-0.2.0/tests/unit/test_serial_keys.py +69 -0
- embedded_lab_control-0.2.0/tests/unit/test_services.py +193 -0
- embedded_lab_control-0.2.0/tests/unit/test_udev.py +360 -0
- embedded_lab_control-0.2.0/tests/unit/test_update_script.py +566 -0
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-10-31
|
|
10
|
+
|
|
11
|
+
First release on PyPI, as `embedded-lab-control` (the import package and
|
|
12
|
+
the CLI are still `labctl`): `pipx install "embedded-lab-control[web,mcp]"`,
|
|
13
|
+
adding `kasa`, `sdwire` and `actuators` as needed (see the README for what
|
|
14
|
+
each extra enables). Python 3.10-3.13; the `sdwire` extra needs 3.12+, and
|
|
15
|
+
Kasa KLAP v2 devices need 3.11+. Everything below the upgrade notes is
|
|
16
|
+
the full history since development started; there was no earlier public
|
|
17
|
+
release.
|
|
18
|
+
|
|
19
|
+
**Highlights**
|
|
20
|
+
|
|
21
|
+
- MCP server on the mcp 2.x SDK, over stdio or HTTP (API-key auth, TLS,
|
|
22
|
+
image uploads), with a two-step confirmation for destructive tools and
|
|
23
|
+
a deny-all allowlist for host file paths.
|
|
24
|
+
- Hardware claims enforced on every surface (CLI, REST, web UI, MCP),
|
|
25
|
+
including boards that share a plug outlet, SDWire, console adapter or
|
|
26
|
+
relay channel; long calls keep the caller's claim alive.
|
|
27
|
+
- Power control for Tasmota, Kasa (incl. KLAP v2 strips) and Shelly;
|
|
28
|
+
SDWire SD-card multiplexers; USB relay actuators with per-board
|
|
29
|
+
bindings (`pre_power` straps applied on every power-on path); ser2net
|
|
30
|
+
serial access with udev-stable names; health monitoring; activity
|
|
31
|
+
stream.
|
|
32
|
+
- Session-based web UI and REST API with optional HTTPS.
|
|
33
|
+
|
|
34
|
+
**Upgrading from a source install (`labctl` 0.1.0)**
|
|
35
|
+
|
|
36
|
+
- The distribution was renamed `labctl` → `embedded-lab-control`.
|
|
37
|
+
`scripts/update.sh` and `scripts/install-services.sh` uninstall the old
|
|
38
|
+
distribution first; if you upgrade a venv by hand, run
|
|
39
|
+
`pip uninstall labctl` before installing (both own `labctl/`, and
|
|
40
|
+
removing the old one later would delete the new install's files).
|
|
41
|
+
- The database migrates itself to schema v12 on first use.
|
|
42
|
+
- `labctl mcp --http PORT` now really binds PORT (it always used 8000):
|
|
43
|
+
the shipped unit's server moves to `127.0.0.1:8080`. Clients wired to
|
|
44
|
+
`:8000` must move. A non-loopback `--host` requires `auth.enabled`;
|
|
45
|
+
HTTP clients then send `Authorization: Bearer <api_key>`.
|
|
46
|
+
- MCP host file access is deny-all until `mcp.allowed_read_paths` /
|
|
47
|
+
`mcp.allowed_write_paths` are set (`flash_image`, `sdwire_update`
|
|
48
|
+
copies, `boot_test`); `update.sh` creates `/var/lib/labctl/images` and
|
|
49
|
+
`/var/lib/labctl/output` and prints the snippet to add. The CLI is
|
|
50
|
+
unaffected.
|
|
51
|
+
- Destructive MCP tools return a plan and a `confirm_token` first
|
|
52
|
+
(`mcp.confirm_destructive`, `mcp.confirm_exempt`).
|
|
53
|
+
- Commands that operate or reconfigure a board another user has claimed
|
|
54
|
+
are refused unless overridden (`--override-claim`, REST
|
|
55
|
+
`"override_claim": true`, operators only on REST/web), and the override
|
|
56
|
+
is logged.
|
|
57
|
+
- Removed config keys (`proxy.enabled`, `proxy.log_retention_days`,
|
|
58
|
+
`proxy.idle_timeout`, `health.update_status_on_check`) are ignored
|
|
59
|
+
with a warning. Several commands that used to read a confirmation from
|
|
60
|
+
stdin now need `--yes` without a terminal.
|
|
61
|
+
- After updating a host with generated udev rules, run
|
|
62
|
+
`labctl serial udev --install --reload` (rules are now
|
|
63
|
+
`root:dialout 0660`).
|
|
64
|
+
|
|
65
|
+
**For packagers**: `mcp>=2.2,<3` pulls in `mcp-types` (pinned exactly
|
|
66
|
+
by mcp), `httpx2` and `opentelemetry-api`. The sdist includes the tests,
|
|
67
|
+
config samples and systemd units, docs and scripts; the wheel carries only
|
|
68
|
+
the `labctl` package.
|
|
69
|
+
|
|
70
|
+
### Added (P2 publish)
|
|
71
|
+
- Release workflow (`.github/workflows/release.yml`): pushing a `vX.Y.Z`
|
|
72
|
+
tag runs the CI gates (`ci.yml`, now also callable as a reusable
|
|
73
|
+
workflow), checks the tag matches `labctl.__version__` and that this
|
|
74
|
+
file has the version's section, builds and `twine check`s the sdist
|
|
75
|
+
and wheel once, publishes them to TestPyPI and then, after approval of
|
|
76
|
+
the protected `pypi` environment, to PyPI with trusted publishing (no
|
|
77
|
+
API tokens), and creates the GitHub Release. Running it by hand only
|
|
78
|
+
builds and checks. `docs/RELEASING.md` has the one-time setup, the
|
|
79
|
+
release steps and what to do when a step fails.
|
|
80
|
+
|
|
81
|
+
### Changed (P2 release prep)
|
|
82
|
+
- A command whose optional dependency is missing now says which extra to
|
|
83
|
+
install, in one line, instead of a traceback or `pip install <package>`:
|
|
84
|
+
`labctl web` (and REST) and `labctl user hash-password`/`add`/`verify`
|
|
85
|
+
(werkzeug) → `[web]`, `labctl mcp` → `[mcp]`, Kasa plugs → `[kasa]`,
|
|
86
|
+
SDWire → `[sdwire]`, USB relays → `[actuators]`. Each message gives
|
|
87
|
+
`pip install "embedded-lab-control[<extra>]"` and the `pipx inject`
|
|
88
|
+
equivalent with the extra's version floors for the running Python;
|
|
89
|
+
the same text reaches the CLI, REST and MCP (`labctl/extras.py`).
|
|
90
|
+
Below Python 3.12 the SDWire message says it needs 3.12+ (the extra
|
|
91
|
+
installs nothing there) and gives no install command. Only the extra's
|
|
92
|
+
own package being absent is reported this way: an installed but broken
|
|
93
|
+
or incompatible one gets its own one-line error carrying the real
|
|
94
|
+
ImportError and the versions the extra wants (still an error line,
|
|
95
|
+
not a traceback). A missing Kasa library
|
|
96
|
+
no longer also logs a "Failed to load Kasa credentials" warning, and
|
|
97
|
+
isn't remembered for the life of the process.
|
|
98
|
+
- README: install from PyPI with pipx or pip, a table of what each extra
|
|
99
|
+
enables, and Python version notes; the source install and the
|
|
100
|
+
systemd install from a git checkout (`scripts/install-services.sh`)
|
|
101
|
+
are unchanged.
|
|
102
|
+
- `docs/HARDWARE_MAP.md` is now a generic guide to mapping USB serial
|
|
103
|
+
adapters (USB paths, `labctl serial discover/add/udev`, assigning
|
|
104
|
+
ports, troubleshooting) with placeholder paths and names.
|
|
105
|
+
- `scripts/install-udev.sh` requires the rules file as its argument (it
|
|
106
|
+
defaulted to a repository file that no longer exists) and refuses a
|
|
107
|
+
file with no `SYMLINK` assignment or with a non-comment line that
|
|
108
|
+
isn't a udev rule, such as the message `labctl serial udev` prints
|
|
109
|
+
when no adapters are registered. The README and `HARDWARE_MAP.md` note
|
|
110
|
+
that installing a rules file by hand skips the claim check
|
|
111
|
+
`labctl serial udev --install` makes. `labctl ports -v` with no ports points
|
|
112
|
+
at `labctl serial discover` instead of `discover-usb-serial.sh`.
|
|
113
|
+
- The source distribution now includes the tests, `config/` (sample
|
|
114
|
+
config, systemd units, udev mapping example), `docs/`, `scripts/`,
|
|
115
|
+
`skills/`, `CHANGELOG.md` and `SECURITY.md` (`MANIFEST.in`); the wheel
|
|
116
|
+
is unchanged. The test suite passes from an unpacked sdist.
|
|
117
|
+
- CI runs on pushes to `main`, on pull requests (so a PR branch is
|
|
118
|
+
tested once, not twice) and by hand (`workflow_dispatch`).
|
|
119
|
+
|
|
120
|
+
### Removed (P2 release prep)
|
|
121
|
+
- This lab's `config/udev/99-lab-serial.rules`,
|
|
122
|
+
`config/udev/port-mapping.yaml` and `config/ser2net/labctl-ser2net.yaml`.
|
|
123
|
+
Rules are generated with `labctl serial udev` and the ser2net config
|
|
124
|
+
with `labctl ser2net generate`; `config/udev/port-mapping.yaml.example`
|
|
125
|
+
(now with placeholder paths and names) remains for the legacy
|
|
126
|
+
`generate-udev-rules.py`.
|
|
127
|
+
|
|
128
|
+
### Added (#2: long calls keep the caller's claim alive)
|
|
129
|
+
- A claim's deadline moved only when a call started, so one long call
|
|
130
|
+
(a 10-run `boot_test`, a long `serial_capture`, flashing a large
|
|
131
|
+
image) could outlast it, and another agent could then claim the board
|
|
132
|
+
and power-cycle it mid-run. MCP `boot_test`, `serial_capture`,
|
|
133
|
+
`sdwire_update` and `flash_image`, and CLI `boot-test`, now heartbeat a
|
|
134
|
+
claim the caller already holds on the board from a background thread
|
|
135
|
+
while they run (every quarter of the claim's current duration, 5-60 s,
|
|
136
|
+
so a renew to a shorter claim is followed). They never claim a board
|
|
137
|
+
for the caller. Each heartbeat is one conditional UPDATE
|
|
138
|
+
(`ResourceManager.heartbeat_held_claim`, which replaces the unused
|
|
139
|
+
`heartbeat_claim`) that never revives a released, expired or
|
|
140
|
+
taken-over claim; `touch_claim` now says whether a row changed. A
|
|
141
|
+
database error is a missed heartbeat (the call goes on) until the
|
|
142
|
+
claim's deadline passes without a successful one, or, for a caller
|
|
143
|
+
holding no claim, until the checks have failed for 60 s in a row; if
|
|
144
|
+
the claim can't be read when the call starts, it runs without
|
|
145
|
+
keepalive (logged). The keepalive follows the claim the call started
|
|
146
|
+
with (its heartbeat matches that claim's id): a claim released and
|
|
147
|
+
taken again, even by the same user, counts as lost.
|
|
148
|
+
- Boot tests check again before each run's power cycle and stop once
|
|
149
|
+
someone else claims the board or a board sharing its hardware, the
|
|
150
|
+
caller's claim is lost (released, force-released or expired), or, over
|
|
151
|
+
MCP with `claims.require_claim`, nobody holds it. The check runs
|
|
152
|
+
before the heartbeat, so a refused check extends nothing. A stopped
|
|
153
|
+
test says `STOPPED after N of M runs: <reason>` and reports
|
|
154
|
+
`Result: INCOMPLETE, ...` (or `Result: STOPPED before the first run`,
|
|
155
|
+
noting that the board is left off if the deploy powered it off)
|
|
156
|
+
instead of a pass rate; MCP results start with `Stopped: <reason>` and
|
|
157
|
+
the CLI exits 1. A CLI test started with `--override-claim` isn't
|
|
158
|
+
stopped by the claims it overrode, but is by any claim taken after it
|
|
159
|
+
started.
|
|
160
|
+
- `sdwire_update` and `flash_image` with `reboot=true` leave the board
|
|
161
|
+
off, and say why, when the check fails after the card is written:
|
|
162
|
+
`Not power cycled: <reason>` (`Not powered on:`) when the caller's own
|
|
163
|
+
claim was lost, `<board> is left off (not power cycled) because
|
|
164
|
+
'<board>' is now claimed by '<agent>'` when someone else claimed the
|
|
165
|
+
board or a board sharing its hardware.
|
|
166
|
+
- `/deploy-and-test` skill: claim after a successful build, sizing the
|
|
167
|
+
claim for the run (for `boot_test`, at least runs × (timeout + cycle
|
|
168
|
+
delay + a few seconds) plus the deploy and a margin); renew it between
|
|
169
|
+
long steps; release it on every path (deploy failure, boot timeout, a
|
|
170
|
+
confirmation not given). Resolves the claims spec's open question on
|
|
171
|
+
the skill.
|
|
172
|
+
|
|
173
|
+
### Changed (#41: options and config keys that did nothing)
|
|
174
|
+
- `serial.base_tcp_port` (and `LABCTL_BASE_TCP_PORT`) now sets where TCP
|
|
175
|
+
port auto-assignment starts (`port assign` without `-t`, MCP, REST, web,
|
|
176
|
+
`import`): one above the highest port in use, and at least the base. It
|
|
177
|
+
must be 1-65535 (anything else keeps 4000, with a warning). The web
|
|
178
|
+
settings page showed it blank (it read `ser2net.base_tcp_port`).
|
|
179
|
+
- `ser2net generate` no longer gives every port without a TCP port
|
|
180
|
+
4000. With `--install` each such port is assigned one and saved, as
|
|
181
|
+
`port assign` without `-t` would (one above the highest in use, at
|
|
182
|
+
least `serial.base_tcp_port`; refused past 65535), and the command
|
|
183
|
+
says which, with `port assign`'s claim checks (one shared helper). If
|
|
184
|
+
any such port is on a board another user has claimed, or one sharing
|
|
185
|
+
a claimed board's console device, `--install` refuses and saves and
|
|
186
|
+
writes nothing (leaving the port out would change that board's
|
|
187
|
+
console), listing them; `--override-claim` (new; recorded in the
|
|
188
|
+
activity log) assigns them anyway. Claimed boards whose ports already
|
|
189
|
+
have a TCP port are written as before. Nothing is saved unless the
|
|
190
|
+
config file can be written (checked first), and each save repeats
|
|
191
|
+
`port assign`'s check, so a claim taken meanwhile still refuses; any
|
|
192
|
+
failure after a save lists the TCP ports already saved. A port whose
|
|
193
|
+
save fails is left out and reported; the config is still written and
|
|
194
|
+
the command exits 1.
|
|
195
|
+
Without `--install` nothing is saved: those ports are left out of the
|
|
196
|
+
output and the number each would get is shown (stderr).
|
|
197
|
+
- `ser2net generate --install` writes `ser2net.config_file` (default
|
|
198
|
+
`/etc/ser2net.yaml`, unchanged); it always wrote `/etc/ser2net.yaml`.
|
|
199
|
+
The write is atomic when labctl can create files in the directory (a
|
|
200
|
+
temporary file there, given the old file's owner and mode, fsynced and
|
|
201
|
+
renamed over it), and otherwise rewritten in place (e.g. a labctl group
|
|
202
|
+
member, who can write `/etc/ser2net.yaml` but not `/etc`); a symlink on
|
|
203
|
+
the path or a hard-linked file is refused, since it may run as root. The
|
|
204
|
+
permission-denied message names the configured path; when the file is
|
|
205
|
+
really absent from a directory labctl can reach, it says to create it
|
|
206
|
+
with `sudo labctl ser2net generate --install` (and, for
|
|
207
|
+
`/etc/ser2net.yaml`, then rerun `install-services.sh` or `update.sh`
|
|
208
|
+
to make it group-writable); when the path can't be reached, it says so.
|
|
209
|
+
`install-services.sh`, and now `update.sh` (for a file created after the
|
|
210
|
+
install), make only `/etc/ser2net.yaml` group-writable (`root:labctl
|
|
211
|
+
664`; a symlink there is left alone).
|
|
212
|
+
- `health.status_retention_days` now prunes status history: the monitor
|
|
213
|
+
deletes older entries at startup and then hourly; `0` keeps all of it.
|
|
214
|
+
Each board's newest entry per status, and the one in effect at the
|
|
215
|
+
cutoff, are always kept, so a board stable for longer than the
|
|
216
|
+
retention keeps its uptime. Schema v12 indexes `status_log(logged_at)`.
|
|
217
|
+
An hourly run is cheap lookups (newest row, rows past the cutoff, all
|
|
218
|
+
rows) and prunes only when one changed since the monitor's last prune.
|
|
219
|
+
Pruning deletes in transactions of at most 5,000 rows, so the first
|
|
220
|
+
prune of a long history doesn't hold the write lock for long.
|
|
221
|
+
- Uptime (`GET /api/sbcs/<name>/uptime`): the 24-hour figure starts from
|
|
222
|
+
the status in effect 24 hours ago, so a board up the whole day shows
|
|
223
|
+
100% (it showed 0% with no status change in the window), and times are
|
|
224
|
+
compared in the database's clock (UTC), not local time.
|
|
225
|
+
- `claims.require_agent_name` is enforced: MCP `claim_sbc` and
|
|
226
|
+
`POST /api/claims/<sbc>` refuse (`agent_name_required`) a claim that
|
|
227
|
+
would be recorded under a placeholder name: no `agent_name` and no
|
|
228
|
+
authenticated user (or, over MCP stdio, no name given earlier in the
|
|
229
|
+
session). CLI claims are always named (`--name` or `$USER`). A REST
|
|
230
|
+
`agent_name` that isn't a string is a 400.
|
|
231
|
+
- MCP over unauthenticated HTTP no longer remembers an `agent_name`
|
|
232
|
+
across calls: the server can't tell its clients apart, so one client's
|
|
233
|
+
name named every other client's claims and releases. It is used for
|
|
234
|
+
the call that gives it; stdio and per-user (authenticated) names are
|
|
235
|
+
still remembered.
|
|
236
|
+
- MCP `claim_sbc` defaults `duration_minutes` to
|
|
237
|
+
`claims.default_duration_minutes`, like the CLI and REST (it was 30).
|
|
238
|
+
- `post-power` bindings can no longer be created: nothing acts on them
|
|
239
|
+
yet. `bind --phase` no longer offers `post-power`, and labctl
|
|
240
|
+
(`ResourceManager`) refuses `post_power`; MCP `bind` refuses it before
|
|
241
|
+
its claim checks. Bindings
|
|
242
|
+
stored with it before are kept and listed as inactive (`bindings list`
|
|
243
|
+
marks them `*`, `bindings status` says so, MCP output carries
|
|
244
|
+
`sample_phase_note`).
|
|
245
|
+
- `ports --all` lists the USB serial devices (`/dev/ttyUSB*`,
|
|
246
|
+
`/dev/ttyACM*`) no `/dev/lab` symlink points at; the flag did nothing.
|
|
247
|
+
- `remove`, `sdwire remove`, `serial remove`, `actuator remove` and
|
|
248
|
+
`power-all` ask only on a terminal. Without one they no longer read the
|
|
249
|
+
answer from stdin (a piped `y` used to confirm) and exit 1 asking for
|
|
250
|
+
`--yes`. Declining `actuator remove` now exits 1, like the others (it
|
|
251
|
+
printed "Aborted." and exited 0).
|
|
252
|
+
- `install-services.sh` and `update.sh` make the services' database
|
|
253
|
+
writable by the `labctl` group, so group members can write it from the
|
|
254
|
+
CLI without `sudo -u labctl`: a new config puts it in
|
|
255
|
+
`/var/lib/labctl/db` (setgid `2775`; the file, created empty if
|
|
256
|
+
missing, and any `-wal`/`-shm` are `664`; SQLite gives new `-wal`/`-shm`
|
|
257
|
+
files the database's mode). A database configured elsewhere is left
|
|
258
|
+
alone, with a note. `database_path` is compared with the directory
|
|
259
|
+
resolved (a symlinked `/var/lib/labctl` still matches; the database file
|
|
260
|
+
itself is never followed). The labctl home gets group search (`g+x`,
|
|
261
|
+
nothing else) when it belongs to the `labctl` group and lacks it; if
|
|
262
|
+
group members still can't traverse it, the scripts warn. Both scripts
|
|
263
|
+
run the same root-safe step,
|
|
264
|
+
`python -m labctl.core.admin_files shared-db` (new; also new: `create`),
|
|
265
|
+
and a problem there (a symlinked database file, a non-regular `-wal`, a
|
|
266
|
+
config that won't parse) is a warning: the scripts carry on, and
|
|
267
|
+
`update.sh` still restarts the services.
|
|
268
|
+
**Upgrading:** an existing install keeps its database where it is
|
|
269
|
+
(e.g. `/var/lib/labctl/labctl.db`). To share it with the group, as root
|
|
270
|
+
throughout (your account may not see into `/var/lib/labctl`): stop the
|
|
271
|
+
services, move `labctl.db` (and any `labctl.db-wal`/`-shm`) into
|
|
272
|
+
`/var/lib/labctl/db/`, check it arrived (`sudo ls -l
|
|
273
|
+
/var/lib/labctl/db/labctl.db`; if it isn't there, `update.sh` creates a
|
|
274
|
+
new, empty database), set `database_path: /var/lib/labctl/db/labctl.db`
|
|
275
|
+
in `/etc/labctl/config.yaml` and copy it to the service config, run
|
|
276
|
+
`update.sh` (or `install-services.sh`), and start the services. README
|
|
277
|
+
"Moving the database" has the commands.
|
|
278
|
+
- REST `POST /api/sbcs/<name>/power` returns 400, not 500, for a
|
|
279
|
+
non-string `action` or a JSON body that isn't an object.
|
|
280
|
+
|
|
281
|
+
### Removed
|
|
282
|
+
- Config keys that were parsed but never acted on: `proxy.enabled`
|
|
283
|
+
(and `LABCTL_PROXY_ENABLED`), `proxy.log_retention_days`,
|
|
284
|
+
`proxy.idle_timeout` and `health.update_status_on_check` (use
|
|
285
|
+
`monitor --no-update`). A config that still sets one loads, with a
|
|
286
|
+
warning that it's ignored.
|
|
287
|
+
|
|
288
|
+
### Security
|
|
289
|
+
- MCP `sdwire_to_host` fails closed (#41): it switches the card only when
|
|
290
|
+
the board's plug reports it off. It used to go ahead when the board had
|
|
291
|
+
no plug or its state couldn't be read; it now refuses, like
|
|
292
|
+
`sdwire_ls`/`cat`/`info`, unless `force=true`. CLI `labctl sdwire host`
|
|
293
|
+
does the same: it refuses a board with no plug or an unreadable or
|
|
294
|
+
unknown plug state unless `--force` (which, as before, skips the power
|
|
295
|
+
check), and so do CLI `sdwire ls`/`cat`/`info` (they refused only a
|
|
296
|
+
plug reporting ON). These now also read a card already on the host in
|
|
297
|
+
place, as the MCP read tools do, and take `--force` to skip the check.
|
|
298
|
+
One check (`labctl.sdwire.power_guard`) serves all of them. The MCP
|
|
299
|
+
read tools' refusal points at `sdwire_to_host(force=true)` for a board
|
|
300
|
+
you've made sure is off.
|
|
301
|
+
- A per-board cycle delay stored before `edit --cycle-delay` checked the
|
|
302
|
+
range (e.g. `1e9`, NaN, negative) is refused on every path before
|
|
303
|
+
anything is switched or written (#41): MCP/REST/web cycles, the reboot
|
|
304
|
+
after `sdwire flash`/`update`, `boot-test` and the recovery commands, CLI
|
|
305
|
+
and MCP. Only CLI `power cycle` checked it before; the others slept on
|
|
306
|
+
it. Clear it with `labctl edit <name> --cycle-delay -1`.
|
|
307
|
+
- `labctl serial remove` and `labctl serial udev --install` check claims
|
|
308
|
+
(#41). `serial remove` refuses an adapter that a board someone else has
|
|
309
|
+
claimed has a port on, by any path that reaches it (including a port
|
|
310
|
+
assigned by `/dev/ttyUSB0` behind the adapter's link, which isn't linked
|
|
311
|
+
to the registration). `serial udev --install` refuses when the new rules
|
|
312
|
+
would remove or re-point a `/dev/lab/<name>` link such a board uses
|
|
313
|
+
(every link, if the installed rules file can't be read). Both take
|
|
314
|
+
`--override-claim`. `serial remove` asks for confirmation only on a
|
|
315
|
+
terminal; without one it refuses unless `--yes` (see the no-terminal
|
|
316
|
+
entry above). MCP
|
|
317
|
+
`remove_serial_device` refuses an adapter a board someone else has
|
|
318
|
+
claimed has a port on, the same way.
|
|
319
|
+
- Writing a board's SD card fails closed when the board has no power plug
|
|
320
|
+
(#41): `sdwire flash`/`update` (CLI) and MCP `sdwire_update`/`flash_image`
|
|
321
|
+
used to switch the card silently; they now refuse unless `--no-plug-ok`
|
|
322
|
+
(MCP `no_plug_ok=true`), which says you've made sure the board is off.
|
|
323
|
+
The flag never skips the power-off of a board with a plug nor excuses a
|
|
324
|
+
failed one. The MCP plans show `will_do.power_off`.
|
|
325
|
+
- Under `sudo`, the CLI acts for the user who ran sudo (`SUDO_USER`): its
|
|
326
|
+
claim identity (`cli-<user>@<host>`), agent name and activity-log actor,
|
|
327
|
+
so e.g. `sudo labctl serial udev --install` doesn't turn your own claim
|
|
328
|
+
into someone else's.
|
|
329
|
+
- MCP confirmation tokens are bound to the plan they were issued with
|
|
330
|
+
(`will_do`): if it differs at confirmation (e.g. the board's stored cycle
|
|
331
|
+
delay changed), the call is refused and the caller asks for a new plan.
|
|
332
|
+
A claimed board's power-cycling tools return the claim refusal, not a
|
|
333
|
+
plan.
|
|
334
|
+
- REST and web-UI power cycles don't overlap: the guard and the 5 s rate
|
|
335
|
+
limit are per plug outlet, so a cycle of an outlet (by any board on it)
|
|
336
|
+
requested while another is running (it blocks for the whole delay) is
|
|
337
|
+
refused with `429`, and `on`/`off` of that outlet meanwhile with `409`.
|
|
338
|
+
The guard covers REST and the web UI in one server process only: power
|
|
339
|
+
operations from the CLI, MCP, `sdwire` reboots or `boot-test` aren't
|
|
340
|
+
seen by it (a lab-wide outlet lock is tracked in #38).
|
|
341
|
+
- `labctl edit --cycle-delay` (setting or clearing it) is claim-checked
|
|
342
|
+
against the boards sharing the board's hardware as well, since a stored
|
|
343
|
+
delay is the minimum of every board on the outlet. Other `edit` changes
|
|
344
|
+
still check the board alone.
|
|
345
|
+
|
|
346
|
+
### Changed
|
|
347
|
+
- One cycle-delay rule on every surface (#41): the board's stored delay
|
|
348
|
+
(`labctl edit --cycle-delay`), or 3 s if none is set, is the default
|
|
349
|
+
and the minimum of every power cycle, not only CLI `power cycle` and
|
|
350
|
+
recovery: MCP `power_cycle` (whose `delay` now defaults to it), REST
|
|
351
|
+
`cycle` (likewise; the response adds `delay_seconds`), the web UI's
|
|
352
|
+
power-cycle button, the reboot after `sdwire flash`/`update` and every
|
|
353
|
+
`boot-test` run (CLI and MCP). A smaller requested delay is raised to
|
|
354
|
+
it, so MCP and REST no longer allow less than 3 s on a board without a
|
|
355
|
+
stored delay. Boards sharing a plug outlet are cycled together, so the
|
|
356
|
+
largest of their minimums applies (each board's stored delay, or 3 s
|
|
357
|
+
without one; an out-of-range one on any of them refuses), and the CLI
|
|
358
|
+
warning, MCP results and REST `delay_source` name the board it came
|
|
359
|
+
from. A negative requested delay is an error everywhere (CLI
|
|
360
|
+
`power cycle --delay` used to raise it to the minimum). The `--dry-run` plans of
|
|
361
|
+
`sdwire flash`/`update` and the MCP confirmations of `power_cycle`,
|
|
362
|
+
`enter_/exit_recovery`, `boot_test` and `sdwire_update` (with
|
|
363
|
+
`reboot=true`) show the delay and where it came from
|
|
364
|
+
(`will_do.cycle_delay_seconds`, `cycle_delay_source`).
|
|
365
|
+
- Generated udev rules (`labctl serial udev`, and the legacy
|
|
366
|
+
`scripts/generate-udev-rules.py`) make `/dev/lab/*` devices
|
|
367
|
+
`root:dialout 0660`. They were world-writable (`0666`), so any local
|
|
368
|
+
user could open a console or drive a relay. The labctl services are in
|
|
369
|
+
`dialout` and ser2net runs as root; a user who drives relays from the
|
|
370
|
+
CLI must now be in `dialout` (`labctl serial udev --install` says so
|
|
371
|
+
when the user isn't). Installed rules change on the next
|
|
372
|
+
`labctl serial udev --install --reload`.
|
|
373
|
+
- `install-services.sh` and `update.sh` make the service copy of the config
|
|
374
|
+
(`/var/lib/labctl/.config/labctl/config.yaml`, which holds the same API
|
|
375
|
+
keys and credentials as `/etc/labctl/config.yaml`) mode `640`; it was
|
|
376
|
+
created world-readable (`644`), so any local user could read the keys
|
|
377
|
+
wherever the labctl home is traversable by others (its mode comes from
|
|
378
|
+
`useradd`'s `HOME_MODE`, `0750` on current Ubuntu/Debian, or from an
|
|
379
|
+
admin: it is `2775` on the reference lab host).
|
|
380
|
+
- The install and update scripts (run as root) no longer follow symlinks
|
|
381
|
+
or hard links planted under `/var/lib/labctl` (writable by the `labctl`
|
|
382
|
+
service user) when they seed `/etc/labctl/config.yaml` from the service
|
|
383
|
+
config, create the service config, change its owner and mode, or create
|
|
384
|
+
and re-permission the `images`/`output` directories. These go through
|
|
385
|
+
`labctl.core.admin_files`: no symlink on any path component below the
|
|
386
|
+
(resolved) labctl home, single-link regular files only, descriptor-based
|
|
387
|
+
ownership changes, `O_EXCL` creation, byte-for-byte copies. Previously a
|
|
388
|
+
compromised service could have root hand it `/etc/labctl/config.yaml`,
|
|
389
|
+
or copy an arbitrary root-only file into a config its group can read.
|
|
390
|
+
Remaining by-path root operations there (`chown -R`, `mkdir -p`, the
|
|
391
|
+
drift `cmp` and the allowlist config read) are tracked in #19.
|
|
392
|
+
- **MCP over HTTP now authenticates with the web users' API keys (breaking
|
|
393
|
+
for HTTP clients when `auth.enabled` is true).** With auth enabled, every
|
|
394
|
+
request to `labctl mcp --http` needs `Authorization: Bearer <api_key>`
|
|
395
|
+
for a user in `auth.users` (401 otherwise); keys are re-read per request,
|
|
396
|
+
so revoking one needs no restart. Sessions are bound to the user that
|
|
397
|
+
opened them, destructive-tool confirm tokens to the user they were issued
|
|
398
|
+
to, and each user is a separate claimant (one user's claim blocks
|
|
399
|
+
another's mutating calls); claims and audit entries record the user. The
|
|
400
|
+
server refuses to start without auth on a non-loopback `--host` or with
|
|
401
|
+
non-loopback names in `mcp.allowed_hosts` / `allowed_origins` (a proxy
|
|
402
|
+
that forwards `Host: 127.0.0.1` can't be detected: enable auth before
|
|
403
|
+
adding one), with auth enabled but no API key configured, or when a
|
|
404
|
+
config file it should read can't be loaded (a fallback to another file
|
|
405
|
+
or to defaults could drop the authentication the intended config
|
|
406
|
+
enables; this includes `/etc/labctl/config.yaml` being unreadable to a
|
|
407
|
+
user outside the `labctl` group, who must then pass `-c`). A running
|
|
408
|
+
MCP server (either transport) now keeps the configuration it started
|
|
409
|
+
with until restarted; only API keys are re-read, from the same file, per
|
|
410
|
+
request (a broken file refuses every key). Users
|
|
411
|
+
without a string username can't authenticate by API key. New `mcp.allowed_hosts` / `mcp.allowed_origins` accept extra
|
|
412
|
+
`Host` / `Origin` headers (e.g. a reverse proxy's public name) on top of
|
|
413
|
+
the loopback names. See `docs/MCP_SERVER.md` (D012).
|
|
414
|
+
- API key lookups (web and MCP) no longer raise on a non-ASCII key in a
|
|
415
|
+
request header, and a non-string `api_key` in the config never matches.
|
|
416
|
+
- **Destructive MCP tools now require a two-step confirmation (breaking for
|
|
417
|
+
MCP clients).** The 23 tools annotated destructive (power off/cycle,
|
|
418
|
+
flashing, SD updates, `serial_send`, deletions, actuator/binding verbs,
|
|
419
|
+
recovery, force release) return a plan and a single-use `confirm_token`
|
|
420
|
+
on the first call and act only when called again with the same arguments
|
|
421
|
+
plus that token (120 s expiry, bound to tool and arguments). Configure
|
|
422
|
+
with `mcp.confirm_destructive` (default `true`) and `mcp.confirm_exempt`.
|
|
423
|
+
See `docs/MCP_SERVER.md` (D012).
|
|
424
|
+
- **MCP host file access is now allowlisted (breaking).** `flash_image`
|
|
425
|
+
(`image_path`, `post_flash_copies` sources), `sdwire_update` (`copies`
|
|
426
|
+
sources) and `boot_test` (`image`, `output_dir`) only accept absolute host
|
|
427
|
+
paths inside the new `mcp.allowed_read_paths` / `mcp.allowed_write_paths`
|
|
428
|
+
settings. Both default to empty, which denies all host file access over
|
|
429
|
+
MCP. Paths are resolved (symlinks, `..`) before the check and are refused
|
|
430
|
+
before any power or SD-mux change. Checked files and directories are
|
|
431
|
+
opened at check time without following symlinks (`labctl.core.safe_open`)
|
|
432
|
+
and all later I/O uses those handles, so a symlink swapped in after the
|
|
433
|
+
check can't redirect it; raw images are streamed into `sudo dd` instead
|
|
434
|
+
of being opened by root. Image format and copied file names follow the
|
|
435
|
+
path given. The CLI is unaffected. Fresh installs
|
|
436
|
+
get `/var/lib/labctl/images` (read) and `/var/lib/labctl/output` (write);
|
|
437
|
+
`scripts/update.sh` creates those directories on existing hosts and prints
|
|
438
|
+
the config to add (D012).
|
|
439
|
+
- Web UI: without a configured `auth.secret_key`, the session-signing key is
|
|
440
|
+
now random per process. It used to fall back to the constant
|
|
441
|
+
`labctl-dev-key` when auth was disabled; because the signed session
|
|
442
|
+
carries the CSRF token, anyone could forge it. Browser sessions now reset
|
|
443
|
+
when the web service restarts unless a key is configured.
|
|
444
|
+
- MCP `sdwire_ls` / `sdwire_cat` / `sdwire_info` no longer have side effects.
|
|
445
|
+
A card already switched to the host is read in place and left there
|
|
446
|
+
(host mode is confirmed with a one-sector `sudo dd` read, because the
|
|
447
|
+
reader's reported size lags a switch in either direction); a
|
|
448
|
+
card on the DUT side is switched to the host and back only when the
|
|
449
|
+
board's power plug reports OFF. With no power plug, an unreadable plug, or
|
|
450
|
+
an unknown state they now refuse (`power_unknown`) instead of switching
|
|
451
|
+
the card away from a possibly running board. They are annotated read-only.
|
|
452
|
+
Callers that relied on reading a plug-less SBC's card must switch it
|
|
453
|
+
explicitly with `sdwire_to_host` first.
|
|
454
|
+
- Read-only SD card mounts (`sdwire_ls`/`sdwire_cat` and the CLI
|
|
455
|
+
equivalents) no longer replay a dirty filesystem journal, which wrote to
|
|
456
|
+
the card despite `-o ro`: ext2/3/4 mount with `noload`, XFS/F2FS with
|
|
457
|
+
`norecovery`, btrfs with `rescue=nologreplay`.
|
|
458
|
+
- MCP `sdwire_to_host` now waits (up to 10 s) until the card is readable on
|
|
459
|
+
the host before returning, and warns if it isn't, so an immediate
|
|
460
|
+
follow-up read doesn't misjudge the mux position.
|
|
461
|
+
|
|
462
|
+
### Changed (documentation audit)
|
|
463
|
+
- README, CLI help, MCP tool descriptions and docs were audited against
|
|
464
|
+
the code and corrected. Users will notice:
|
|
465
|
+
- README examples that failed as written: `labctl power on <sbc>` (not
|
|
466
|
+
`power <sbc> on`), `port assign` with its port type, `monitor` (no
|
|
467
|
+
`--foreground`), `-r` on `claim`/`force-release`/`request-release`.
|
|
468
|
+
The REST table lists the real routes, with the power endpoint's
|
|
469
|
+
`delay`, `override_claim` and status codes (400/409 `switched:
|
|
470
|
+
false`/429/500).
|
|
471
|
+
- Install instructions use the extras from a checkout (`pip install -e
|
|
472
|
+
".[web,mcp,kasa,sdwire,actuators]"`): the package isn't on PyPI yet.
|
|
473
|
+
The README gives the Python requirements, the config search order
|
|
474
|
+
(a personal `~/.config/labctl/config.yaml` wins over `/etc`), and the
|
|
475
|
+
`labctl serial udev` flow in place of `install-udev.sh`. The SDWire
|
|
476
|
+
sudoers rule adds `parted` and `blkid` and gets its own file.
|
|
477
|
+
- New README sections on relays, bindings and recovery (`pre_power`)
|
|
478
|
+
and on the common flags (`--yes`, `--override-claim`, `--dry-run`).
|
|
479
|
+
- `docs/MCP_SERVER.md` documents the actuator, binding and recovery
|
|
480
|
+
tools; the prompt names are `debug_sbc` and `lab_report`; 24 tools
|
|
481
|
+
need confirmation; every image-upload error is listed.
|
|
482
|
+
- CLI help and MCP tool descriptions say what commands actually do,
|
|
483
|
+
e.g. `sdwire flash`/`update` power the board off and leave it off
|
|
484
|
+
without a reboot; `power on`/`cycle` set the `pre_power` relays of
|
|
485
|
+
boards sharing the outlet too; MCP/REST cycles don't use the board's
|
|
486
|
+
stored cycle delay; `actuator set` points at `labctl bindings
|
|
487
|
+
actuate`; `bind --phase post-power` is stored but not acted on yet.
|
|
488
|
+
- `SECURITY.md` lists the controls and states the web UI's default
|
|
489
|
+
exposure: the shipped unit listens on `0.0.0.0` over HTTP, auth is
|
|
490
|
+
off by default, and CSRF applies only with auth.
|
|
491
|
+
- `config/labctl.yaml.example` lists the environment variables that
|
|
492
|
+
actually override settings, and the `claims`, `proxy`, `health` and
|
|
493
|
+
`database` keys and `mcp.allow_admin_actuator_ops` with their
|
|
494
|
+
defaults.
|
|
495
|
+
- `docs/OPERATIONS.md`: `run_health_check` is `shared-resource` (it
|
|
496
|
+
writes nothing); claim-unchecked commands and aliases are complete.
|
|
497
|
+
- `docs/HARDWARE_MAP.md` is a generic example; `SPEC_actuators.md`,
|
|
498
|
+
`STATUS.md`, `CLAUDE.md` (schema v11) and `IMPLEMENTATION.md` match
|
|
499
|
+
what was built; the `deploy-and-test` skill covers confirmation,
|
|
500
|
+
claims and image upload.
|
|
501
|
+
|
|
502
|
+
### Fixed
|
|
503
|
+
- The SD card is no longer switched to the host while the board may be
|
|
504
|
+
running (bus contention can corrupt the card). `boot-test` with an image
|
|
505
|
+
(CLI and MCP) didn't power the board off at all; `sdwire flash` and
|
|
506
|
+
`sdwire update` (CLI and MCP) powered it off on a best-effort basis and
|
|
507
|
+
carried on when that failed. All of them now refuse, writing nothing,
|
|
508
|
+
when the board can't be switched off. A `boot-test` deploy that fails
|
|
509
|
+
once the board is off hands the card back to the board and says the
|
|
510
|
+
board was left off (it used to be a traceback for errors other than
|
|
511
|
+
RuntimeError).
|
|
512
|
+
- `mcp.allow_admin_actuator_ops` turns on only for a YAML boolean `true`.
|
|
513
|
+
It was read with `bool()`, so a quoted `"false"` (or any other string)
|
|
514
|
+
enabled `actuator_add`/`actuator_remove`/`actuator_set` over MCP; any
|
|
515
|
+
non-boolean value now leaves them off, with a warning.
|
|
516
|
+
- `labctl edit --cycle-delay` refuses a delay over 86400 seconds, NaN or
|
|
517
|
+
infinity (nothing is written; `-inf` no longer counts as "clear"). The
|
|
518
|
+
stored delay is the default and the floor of `power cycle`, so such a
|
|
519
|
+
value made every later cycle of the board fail, with no `--delay` able
|
|
520
|
+
to get round it; `power cycle` now says how to clear one stored
|
|
521
|
+
earlier.
|
|
522
|
+
|
|
523
|
+
- `pre_power` relay bindings (e.g. a `recovery_mode` strap) are now set
|
|
524
|
+
before power-on on every path, not just CLI `power on`/`power cycle` and
|
|
525
|
+
the recovery composites (#36). The MCP `power_on`/`power_cycle`/
|
|
526
|
+
`boot_test` tools and the reboot after `sdwire_update`/`flash_image`,
|
|
527
|
+
the web UI and REST power actions, and CLI `power-all on`, `boot-test`
|
|
528
|
+
and the reboot after `sdwire flash`/`update` used to switch power and
|
|
529
|
+
leave the strap however it was last set, so a board could boot in or out
|
|
530
|
+
of recovery depending on which interface turned it on. Boards that share
|
|
531
|
+
the plug outlet have their straps set too, since the outlet powers them
|
|
532
|
+
all (this also applies to `enter-recovery`/`exit-recovery`). A relay
|
|
533
|
+
that can't be opened, or whose channel is busy with another operation,
|
|
534
|
+
now stops these before power is switched (cycles check before powering
|
|
535
|
+
off; a flash or file update with a reboot checks before it powers the
|
|
536
|
+
board off and writes the card): MCP and the CLI say so in one line, the
|
|
537
|
+
REST API answers 409 with `"switched": false` when nothing was switched
|
|
538
|
+
(a failure after power-off stays a 500), the web UI flashes it, after a
|
|
539
|
+
flash or file update the tools report that the power cycle failed and
|
|
540
|
+
why, and `boot-test` records the run as failed. A strap that fails after
|
|
541
|
+
power-off leaves the board off, and the error says so. A power-cycle
|
|
542
|
+
delay over 86400 seconds, or not a number, is now refused before
|
|
543
|
+
power-off: by CLI `power cycle` (also with `--dry-run`, and when the
|
|
544
|
+
board's stored delay is out of range; a delay below the board's minimum
|
|
545
|
+
is still raised to it), and, with negative delays too, by MCP
|
|
546
|
+
`power_cycle` and by the REST API with a 400. It used to switch the
|
|
547
|
+
board off, then fail.
|
|
548
|
+
- A relay that labctl can't open no longer crashes `power on`/`power
|
|
549
|
+
cycle` (and the binding verbs and recovery composites) with a
|
|
550
|
+
traceback; nor does an actuator whose driver isn't implemented. The
|
|
551
|
+
error is one `Error:` line, and the board is never powered on without
|
|
552
|
+
its strap. `power cycle`, `enter-recovery` and `exit-recovery` now
|
|
553
|
+
check every relay their pre-power bindings need before powering off,
|
|
554
|
+
so a relay they can't open no longer leaves the board off. A
|
|
555
|
+
permission error says what to do: join the group that owns the device
|
|
556
|
+
(only when that group grants read/write and isn't root's), then start
|
|
557
|
+
a new login and restart any service or MCP client running labctl. `labctl actuator set` and the MCP
|
|
558
|
+
`actuator_set` tool report the same error instead of raising.
|
|
559
|
+
- Over stdio, the MCP `list_images` tool said only that uploads need the
|
|
560
|
+
HTTP transport, which read as a dead end. It now lists what a stdio
|
|
561
|
+
session (which runs on the lab host) can do instead, with this server's
|
|
562
|
+
paths: pass an absolute path inside `mcp.allowed_read_paths`, copy the
|
|
563
|
+
image into `mcp.upload_dir` (under a unique temp name, then renamed into place;
|
|
564
|
+
it says whether the session can write there) and use its bare name, or
|
|
565
|
+
upload to the host's HTTP service. The last two are offered only when
|
|
566
|
+
bare names resolve (`upload_dir` inside `allowed_read_paths`). With
|
|
567
|
+
uploads off or `upload_dir` unreadable, a stdio session is still pointed
|
|
568
|
+
at host paths, and the server instructions no longer say files must be
|
|
569
|
+
uploaded over HTTP.
|
|
570
|
+
- Kasa strips that only answer KLAP discovery (e.g. HS300 firmware 1.1.2,
|
|
571
|
+
which closed the legacy port 9999) are controlled per outlet again.
|
|
572
|
+
python-kasa types such a device by its family (`IOT.SMARTPLUGSWITCH`)
|
|
573
|
+
as a single plug with no outlets: state reads failed with
|
|
574
|
+
`KeyError: 'relay_state'`, and an on/off for any outlet was sent to the
|
|
575
|
+
strip as a whole, not to that outlet. labctl now reads the sysinfo and
|
|
576
|
+
lets python-kasa pick the class from it, as python-kasa's own `connect`
|
|
577
|
+
does, rebuilding the device on the same connection when discovery
|
|
578
|
+
picked another. A Kasa device that reports no outlets now refuses an
|
|
579
|
+
outlet index above 1 instead of switching the whole device.
|
|
580
|
+
- Kasa power control works again with IOT devices whose firmware moved to
|
|
581
|
+
KLAP login version 2 (e.g. HS300 hardware 2.0, firmware 1.1.2): every
|
|
582
|
+
command failed with `AuthenticationError: Device response did not match
|
|
583
|
+
our challenge` even with correct credentials, because python-kasa before
|
|
584
|
+
0.12 always used KLAP v1 for IOT devices. The `kasa` extra now requires
|
|
585
|
+
`python-kasa>=0.12` on Python 3.11+. python-kasa 0.8 and later need
|
|
586
|
+
Python 3.11, so on 3.10 it pins 0.7.7 (it allowed 0.5, which lacks
|
|
587
|
+
APIs labctl uses) and those devices still can't be controlled there.
|
|
588
|
+
When an authentication error persists with credentials configured and
|
|
589
|
+
python-kasa < 0.12, the error message now says so and names the upgrade.
|
|
590
|
+
- `labctl mcp --http` waits up to 15 s for a port that is in use before
|
|
591
|
+
starting (seen once on a deploy: the port was still in use ~1 s after
|
|
592
|
+
the old server had exited, cause unknown; the new server's start failed
|
|
593
|
+
and systemd restarted it 10 s later). It probes the addresses uvicorn
|
|
594
|
+
will bind, then lets uvicorn bind as before. `update.sh` also waits,
|
|
595
|
+
for every service, while a unit is `deactivating`, `activating` (as
|
|
596
|
+
while systemd restarts a failed start) or `reloading`, within
|
|
597
|
+
`MCP_START_TIMEOUT` (now measured in seconds), and says when systemd
|
|
598
|
+
had to restart one; it had reported `labctl-mcp` as failed.
|
|
599
|
+
- The test suite no longer loads the machine's real config. About 115
|
|
600
|
+
tests loaded config without naming a file, so on a lab host whose user
|
|
601
|
+
can read `/etc/labctl` they used the production config and database; a
|
|
602
|
+
run during the image-upload work migrated a production database to a
|
|
603
|
+
pre-release schema v10 (an `images` table without `file_id`, so uploads
|
|
604
|
+
failed with 500). Schema v11 adds the missing column to such a
|
|
605
|
+
database, so a backup taken in between can be restored safely. Every test, and every process it starts, now gets its own home
|
|
606
|
+
and no system config. New `LABCTL_SYSTEM_CONFIG` environment variable:
|
|
607
|
+
replaces `/etc/labctl/config.yaml` in the config search order.
|
|
608
|
+
- REST `DELETE /api/sbcs/<name>` on a claimed board answered `500`; it is
|
|
609
|
+
now `409 "sbc_claimed"` (#21). A non-numeric `duration_minutes` on
|
|
610
|
+
claim or renew answered `500`, now `400`; renew durations are now
|
|
611
|
+
bounded like claims. The web plug form refuses an outlet index below 1
|
|
612
|
+
(index 0 switches every outlet on a Tasmota device).
|
|
613
|
+
- REST and web claims used a session ID the next request often didn't
|
|
614
|
+
share (a per-request ID without auth, an API-key prefix with it), so a
|
|
615
|
+
claim made over REST could not always be renewed or released; they now
|
|
616
|
+
use `web:<user>` (`web:anonymous` without auth) and record the user as
|
|
617
|
+
owner. Claims made before the upgrade keep their old ID: release them
|
|
618
|
+
with force-release if needed.
|
|
619
|
+
- A command given a config file by name (`-c FILE` or `LABCTL_CONFIG`)
|
|
620
|
+
fails if it doesn't load (YAML error or invalid value) instead of
|
|
621
|
+
running on built-in defaults (commands that don't read the config,
|
|
622
|
+
e.g. `user generate-key` / `hash-password` / `add`, `completion` and
|
|
623
|
+
`--help`, still run). `labctl mcp` handles this itself: over
|
|
624
|
+
HTTP it refuses to start, over stdio it starts with a warning.
|
|
625
|
+
`labctl web` refuses to start if any config file it reads doesn't load
|
|
626
|
+
(e.g. `/etc/labctl/config.yaml` unreadable to a user outside the
|
|
627
|
+
`labctl` group: pass `-c`), since it would otherwise serve without the
|
|
628
|
+
login the intended config enables.
|
|
629
|
+
- A config value that is valid YAML but not a valid setting (e.g.
|
|
630
|
+
`timeout_seconds: 10s`, or a list at the top level) is a load error
|
|
631
|
+
(warning; the next file in the search order, or built-in defaults)
|
|
632
|
+
instead of a crash.
|
|
633
|
+
- An MCP `boot_test` `output_dir` that is an existing file is reported as
|
|
634
|
+
"not a directory" instead of a possible race.
|
|
635
|
+
- A config section header with nothing under it (e.g. `mcp:` followed only
|
|
636
|
+
by commented-out entries) no longer crashes every labctl command; it
|
|
637
|
+
counts as an empty section. A section that isn't a mapping is ignored
|
|
638
|
+
with a warning.
|
|
639
|
+
- Boot-test per-run output files (`run_NN.txt`) are written with
|
|
640
|
+
`O_NOFOLLOW` and refused if hard-linked, so a link planted in a shared
|
|
641
|
+
output directory can't redirect the (DUT-controlled) output into another
|
|
642
|
+
file. The default `/var/lib/labctl/output` is now also sticky (`3775`).
|
|
643
|
+
A run file that can't be written no longer fails the whole boot test;
|
|
644
|
+
the summary (also the MCP result) lists every file that wasn't saved.
|
|
645
|
+
- `labctl -c FILE mcp` now makes the MCP server use FILE. The server loaded
|
|
646
|
+
its config itself and ignored `-c`, so it read
|
|
647
|
+
`~/.config/labctl/config.yaml` first (for the systemd unit: the service
|
|
648
|
+
user's config, not the `/etc/labctl/config.yaml` the unit names). This
|
|
649
|
+
matters now that the MCP host-file allowlist lives in config. FILE is
|
|
650
|
+
exclusive, as with any explicit path: never another config. The same
|
|
651
|
+
exclusive mode is available as `LABCTL_CONFIG_EXCLUSIVE=1` alongside
|
|
652
|
+
`LABCTL_CONFIG`. (How a running server treats later changes to FILE:
|
|
653
|
+
see Security, MCP over HTTP.)
|
|
654
|
+
- MCP claim sweeper (issue #3): a crashed `mcp-stdio` session whose PID was
|
|
655
|
+
reused by another process is now recognised as gone (the process start
|
|
656
|
+
time is compared with the session's epoch), so its claim is released as
|
|
657
|
+
`session-lost` rather than later as `expired`.
|
|
658
|
+
|
|
659
|
+
### Changed
|
|
660
|
+
- Kasa: driver errors (bad outlet, unreachable plug) inside the MCP server
|
|
661
|
+
are reported as such, not as an unrelated asyncio error.
|
|
662
|
+
- Power plugs: `labctl plug assign --index` and MCP `assign_power_plug`
|
|
663
|
+
refuse an outlet index below 1 (Tasmota's `Power0` switches every
|
|
664
|
+
relay). Rows saved with such an index (earlier, or by `labctl import`)
|
|
665
|
+
count as sharing every outlet on that address for claim checks.
|
|
666
|
+
- MCP `serial_capture` is claim-checked like `serial_send`: it holds the
|
|
667
|
+
board's ser2net session.
|
|
668
|
+
- HTTP MCP sessions are labelled `mcp-http` (claims' `session_kind` and
|
|
669
|
+
activity-log actors), not `mcp-stdio`; the dead-session sweep covers both.
|
|
670
|
+
- **Package renamed for PyPI**: the distribution is now
|
|
671
|
+
`embedded-lab-control` (`pip install embedded-lab-control`); the import
|
|
672
|
+
package and CLI remain `labctl`. The version is single-sourced from
|
|
673
|
+
`labctl.__version__` (also used by `/api/health` and the web footer, which
|
|
674
|
+
previously hard-coded `0.1.0`). License metadata is now the SPDX
|
|
675
|
+
expression `MIT`, and the LICENSE file ships in the wheel and sdist.
|
|
676
|
+
- **MCP server migrated to the mcp 2.x SDK** (`mcp>=2.2,<3`, `MCPServer`
|
|
677
|
+
replaces `FastMCP`). `mcp>=1.0` had started resolving to 2.2, where
|
|
678
|
+
`mcp.server.fastmcp` no longer exists and `labctl mcp` crashed on import.
|
|
679
|
+
Tools, resources and prompts are unchanged. mcp 2.x runs sync tools and
|
|
680
|
+
resources on worker threads; labctl keeps 1.x semantics by serializing tool
|
|
681
|
+
calls and the hardware-touching resources (`lab://power/{sbc}`,
|
|
682
|
+
`lab://health/{sbc}`, `lab://status`) under one lock (D011). The server
|
|
683
|
+
now reports labctl's version in `serverInfo`.
|
|
684
|
+
- `scripts/update.sh` and `scripts/install-services.sh` uninstall the old
|
|
685
|
+
`labctl` distribution before installing `embedded-lab-control`, so the
|
|
686
|
+
first upgrade doesn't leave two distributions owning `labctl/` (a later
|
|
687
|
+
`pip uninstall labctl` would otherwise delete the new install).
|
|
688
|
+
- The `sdwire` extra only installs on Python ≥ 3.12 (PyPI `sdwire` requires
|
|
689
|
+
it). On 3.10/3.11 SDWire commands report that the package is missing.
|
|
690
|
+
- Development: `src/` and `tests/` are now clean under `black`, `isort` and
|
|
691
|
+
`flake8`, which are pinned exactly in the `dev` extra
|
|
692
|
+
(`black==26.5.1`, `isort==9.0.1`, `flake8==7.4.1`). Formatting-only apart
|
|
693
|
+
from removing dead code flagged by flake8 (unused imports/variables).
|
|
694
|
+
|
|
695
|
+
### Added
|
|
696
|
+
- MCP `serial_send` takes `keys` (named keys: ESC, Enter, Tab, Space,
|
|
697
|
+
Backspace (0x08), arrows, Home/End/Insert/Delete/PgUp/PgDn, F1-F12,
|
|
698
|
+
Ctrl-A..Ctrl-Z, in xterm encodings; each key is written 50 ms after
|
|
699
|
+
the previous one, for consoles that time out a lone ESC quickly) and
|
|
700
|
+
`data_hex` (raw bytes, e.g. for firmware whose terminal type wants
|
|
701
|
+
other function-key codes, or 0x7F to erase at a Linux prompt), so
|
|
702
|
+
agents can drive UEFI/U-Boot menus: a raw ESC in `data` is dropped
|
|
703
|
+
before it reaches the server (#33). `newline` defaults to off when
|
|
704
|
+
either is given. Its confirmation now shows the exact bytes it will
|
|
705
|
+
send (`will_do`), and an unknown key or bad hex is refused before a
|
|
706
|
+
token is issued. F11 has been checked on a Jetson's UEFI; the other
|
|
707
|
+
encodings have not been checked on real firmware yet.
|
|
708
|
+
- **Image uploads over MCP HTTP** (D014): `PUT /images/<name>` on the MCP
|
|
709
|
+
HTTP port, with the same API key, stores a file in the new
|
|
710
|
+
`mcp.upload_dir` (default empty: off); `flash_image`, `sdwire_update`
|
|
711
|
+
copies and `boot_test` take an uploaded image's bare name. New tools
|
|
712
|
+
`list_images` (with upload instructions) and `delete_image`. Limits:
|
|
713
|
+
bare names, `mcp.max_upload_mb` (8192), 1 GiB kept free, optional
|
|
714
|
+
`?sha256=` check, atomic replace with `?overwrite=1`. Only the uploader
|
|
715
|
+
or an operator may replace or delete an image; uploads and deletions are
|
|
716
|
+
in the activity log. Schema v10 (`images` table).
|
|
717
|
+
- **TLS for the MCP HTTP server**: `labctl mcp --http PORT --cert FILE
|
|
718
|
+
--key FILE`, or `mcp.cert_file`/`mcp.key_file`. A non-loopback bind
|
|
719
|
+
without TLS logs a warning. `labctl mcp --check` prints the `tls:` and
|
|
720
|
+
`uploads:` settings.
|
|
721
|
+
- Claim enforcement, part 3 (D013): the web UI and REST API refuse to
|
|
722
|
+
operate or reconfigure a board another user has claimed (REST: `409`
|
|
723
|
+
`"sbc_claimed"`; web UI: a page naming the holder), including boards
|
|
724
|
+
sharing its hardware and boards already wired to a port or plug being
|
|
725
|
+
assigned. Operators may override (`"override_claim": true`, or the
|
|
726
|
+
button on the refusal page), recorded as `claim_override`; agents
|
|
727
|
+
(`role: agent`) get `403 "not_operator"`, also for force-release (which
|
|
728
|
+
any REST or web user could do before). `claims.require_claim` applies
|
|
729
|
+
to the REST API. REST claims record the user as owner, so one user is
|
|
730
|
+
one claimant across REST, the web UI and MCP over HTTP. See
|
|
731
|
+
docs/OPERATIONS.md.
|
|
732
|
+
- Claim enforcement, part 2 (D013): CLI commands that operate or
|
|
733
|
+
reconfigure a board refuse when another user has claimed it (or a board
|
|
734
|
+
sharing its plug outlet, SDWire, console adapter or relay channel), naming
|
|
735
|
+
the holder, before any confirmation question. `--override-claim` acts
|
|
736
|
+
anyway and is recorded in the activity log as `claim_override`;
|
|
737
|
+
`power-all` skips claimed boards; `import` checks every board before
|
|
738
|
+
changing any; `claim` refuses a board whose shared hardware someone else
|
|
739
|
+
holds. `remove` releases the user's own CLI claim before deleting, and
|
|
740
|
+
`remove --force` is now an alias for `--override-claim` (also logged).
|
|
741
|
+
See docs/OPERATIONS.md for the list. Assigning a serial port a TCP port
|
|
742
|
+
another board's port already uses is now refused (CLI, MCP, web): the
|
|
743
|
+
two would share one ser2net console, reaching a claimed board's console
|
|
744
|
+
through another board.
|
|
745
|
+
- Claim enforcement, part 1 (D013): MCP tools that change a board's
|
|
746
|
+
configuration (`update_sbc`, `assign_*`/`remove_*` for ports, plugs and
|
|
747
|
+
addresses, `sdwire_assign`/`unassign`, `bind`/`unbind`) now refuse under
|
|
748
|
+
another user's claim, as do `actuator_set`/`actuator_remove` on channels
|
|
749
|
+
bound to a claimed board. A claim also covers the board's hardware:
|
|
750
|
+
operating a board that shares a plug outlet, SDWire, serial device or
|
|
751
|
+
relay channel with a board someone else claimed is refused, as are
|
|
752
|
+
claiming it and assigning such hardware to another board.
|
|
753
|
+
- `auth.users[].role`: `operator` (default) or `agent`. Only operators may
|
|
754
|
+
`force_release_sbc`; agents get `not_operator`.
|
|
755
|
+
- `claims.require_claim` (default `false`): MCP calls that change a board
|
|
756
|
+
refuse with `claim_required` unless the caller has claimed it.
|
|
757
|
+
- Claims record their authenticated owner (schema v9): the same user's
|
|
758
|
+
other MCP sessions, and the same user after a server restart, count as
|
|
759
|
+
the claimant (use, renew, release). Claiming a board you already hold
|
|
760
|
+
returns `already_claimed` and renews it for the requested duration
|
|
761
|
+
(boards sharing its hardware aren't re-checked).
|
|
762
|
+
- `docs/CLAUDE_CODE_AGENTS.md`: connecting Claude Code agents to the HTTP
|
|
763
|
+
MCP server with one labctl user and API key each, so claims keep them
|
|
764
|
+
apart. Covers `claude mcp add` (manual) and a shared `--mcp-config` file
|
|
765
|
+
with the key taken from `LABCTL_API_KEY` at launch (streamlined), agent
|
|
766
|
+
instructions, identity checks and key revocation.
|
|
767
|
+
- `SECURITY.md`: how to report vulnerabilities privately, and what's in
|
|
768
|
+
scope. `docs/MCP_SERVER.md` opens its security section with an overview
|
|
769
|
+
of every control (defaults, settings), known limits and a deployment
|
|
770
|
+
checklist; the README links them.
|
|
771
|
+
- `labctl mcp --http PORT --check` runs the HTTP startup checks (the same
|
|
772
|
+
code the server runs) without serving: whether clients need API keys
|
|
773
|
+
(`auth: required` / `none`), the config file loaded, and startup
|
|
774
|
+
warnings; exit 1 if the server would refuse to start or a config file it
|
|
775
|
+
would read is unparseable, unreadable, or (when named by
|
|
776
|
+
`LABCTL_CONFIG`) missing. A `-c FILE` that is missing or unreadable is
|
|
777
|
+
rejected by the CLI itself (exit status 2).
|
|
778
|
+
- `scripts/update.sh`, after restarting, shows `labctl-mcp`'s journal from
|
|
779
|
+
this restart (auth mode, startup warnings) and, whenever auth is
|
|
780
|
+
enabled, reminds that HTTP clients must send their API key; for any
|
|
781
|
+
service that failed to start it prints that service's log since the
|
|
782
|
+
restart. Before installing anything it refuses a config that doesn't
|
|
783
|
+
load (YAML error or an invalid value). The MCP server prints its auth
|
|
784
|
+
mode on every HTTP start and
|
|
785
|
+
logs startup warnings (short keys, unknown confirm exemptions, unchecked
|
|
786
|
+
Host headers; over stdio also config files it couldn't load) whatever
|
|
787
|
+
`log_level` says; over HTTP an unloadable config makes it refuse to
|
|
788
|
+
start instead (see Security).
|
|
789
|
+
- Destructive CLI commands (19: `power off`/`cycle`, `sdwire flash`/`update`/
|
|
790
|
+
`host`/`unassign`, `serial send`, `actuator set`, binding verbs, recovery,
|
|
791
|
+
`force-release`, `boot-test`, record removals) take `--yes/-y` and ask for
|
|
792
|
+
confirmation, but only when stdin is a terminal: cron, CI, systemd, pipes
|
|
793
|
+
and agents' tool calls are not asked. Scripts run from an interactive
|
|
794
|
+
shell inherit the terminal and will be asked: add `--yes` to them. `power cycle`, `sdwire flash` and `sdwire update` take
|
|
795
|
+
`--dry-run` (validate and print the plan; change nothing).
|
|
796
|
+
- `docs/OPERATIONS.md`: every CLI command, MCP tool and MCP resource
|
|
797
|
+
classified as read / db-write / shared-resource / system-write /
|
|
798
|
+
hardware / destructive (issue #7). A test keeps it in sync with the code.
|
|
799
|
+
- MCP tool annotations (`title`, `readOnlyHint`, `destructiveHint`,
|
|
800
|
+
`idempotentHint`, `openWorldHint`) on all 52 tools, defined in one table in
|
|
801
|
+
`mcp_server.py`. A unit test fails if a tool is added without an entry.
|
|
802
|
+
- `labctl mcp --host ADDR` (default `127.0.0.1`) for the HTTP transport.
|
|
803
|
+
- `labctl mcp` without the `mcp` extra now prints an install hint instead of
|
|
804
|
+
a traceback.
|
|
805
|
+
- GitHub Actions CI (`.github/workflows/ci.yml`) on push and pull request:
|
|
806
|
+
Python 3.10–3.13 matrix running flake8, black, isort and pytest with
|
|
807
|
+
coverage (XML uploaded as an artifact), plus a gitleaks scan of the full
|
|
808
|
+
git history. Reviewed gitleaks false positives are listed in
|
|
809
|
+
`.gitleaksignore`.
|
|
810
|
+
- `labctl serial send` send pacing to avoid receiver UART RX-FIFO overrun
|
|
811
|
+
on sustained sends (issue #8). A gapless burst can leave a receiver no
|
|
812
|
+
window to drain its RX FIFO, dropping a contiguous mid-payload byte run.
|
|
813
|
+
Two new mutually-exclusive knobs feed the payload to ser2net in delayed
|
|
814
|
+
slices within a single TCP connection:
|
|
815
|
+
- `--interbyte-delay-ms N` — one byte at a time, N ms between bytes.
|
|
816
|
+
- `--chunk-size B --chunk-delay-ms N` — B bytes per slice, N ms between.
|
|
817
|
+
Pacing applies to both send-only and `--capture` paths. Hardware/software
|
|
818
|
+
flow control (RTS/CTS, XON/XOFF) is tracked separately in issue #9.
|
|
819
|
+
|
|
820
|
+
### Fixed
|
|
821
|
+
- `labctl mcp --http PORT` ignored PORT and always bound `127.0.0.1:8000`.
|
|
822
|
+
It now binds `127.0.0.1:PORT` (or `--host`). **Deployment note:** the
|
|
823
|
+
shipped systemd unit uses `--http 8080`, so a deployed server moves from
|
|
824
|
+
`:8000` to `:8080`. With a loopback bind, the SDK's DNS-rebinding guard
|
|
825
|
+
rejects non-localhost `Host` headers (HTTP 421).
|
|
826
|
+
- `labctl.sdwire` failed to import on Python 3.10 (`datetime.UTC` is
|
|
827
|
+
3.11+), despite `requires-python >= 3.10`.
|
|
828
|
+
- Hotfix: per-connection SQLite pragmas (`busy_timeout`, `synchronous=NORMAL`,
|
|
829
|
+
`foreign_keys=ON`) are now best-effort. After the WAL switch, callers
|
|
830
|
+
without OS-level write permission on the DB file (e.g. users outside
|
|
831
|
+
the `labctl` group running `labctl serial send`) hit
|
|
832
|
+
`sqlite3.OperationalError: attempt to write a readonly database`
|
|
833
|
+
at connection open, because SQLite auto-downgrades the connection to
|
|
834
|
+
read-only and then rejects write-side pragmas. We now wrap each
|
|
835
|
+
pragma in `try/except sqlite3.OperationalError` and log at debug.
|
|
836
|
+
Writable connections still get full tuning; read-only callers keep
|
|
837
|
+
working. Regression test added in `tests/unit/test_database.py`.
|
|
838
|
+
- Transient `sqlite3.OperationalError: database is locked` errors under
|
|
839
|
+
multi-process load (monitor daemon + CLI + MCP + batch jobs hitting
|
|
840
|
+
the same SQLite file). Root cause was the combination of default
|
|
841
|
+
rollback-journal mode (readers and writers exclude each other across
|
|
842
|
+
the whole DB), the default 5s `sqlite3.connect()` busy timeout, and
|
|
843
|
+
`Database.initialize()` re-running its `sqlite_master` probe on every
|
|
844
|
+
`get_database()` call. Fix:
|
|
845
|
+
- `PRAGMA journal_mode=WAL` applied on first `initialize()` (persists
|
|
846
|
+
in the DB file header — concurrent processes inherit it). Readers
|
|
847
|
+
no longer block writers and vice versa; only writer-vs-writer is
|
|
848
|
+
serialized, and labctl writes commit sub-millisecond.
|
|
849
|
+
- `PRAGMA synchronous=NORMAL` (WAL-safe pairing, faster commits, a
|
|
850
|
+
few-second worst-case write loss on power failure — fine for a lab
|
|
851
|
+
DB).
|
|
852
|
+
- Per-connection `busy_timeout` mirrors `Config.database.timeout_seconds`
|
|
853
|
+
(new; default **10s**), and the same value is passed as
|
|
854
|
+
`sqlite3.connect(timeout=…)`. Backstops the narrow windows WAL
|
|
855
|
+
still serializes (schema migrations, WAL checkpoints).
|
|
856
|
+
- `initialize()` is now cached per-process: subsequent calls on the
|
|
857
|
+
same `Database` instance return immediately without re-opening a
|
|
858
|
+
connection. Long-lived processes (daemon, MCP, web) pay the
|
|
859
|
+
sqlite_master probe exactly once; short-lived CLI invocations still
|
|
860
|
+
pay one initialize() per run.
|
|
861
|
+
- New `database` config section (`database.timeout_seconds`, default
|
|
862
|
+
10.0) exposed via `Config.database`. Wired through `get_database` /
|
|
863
|
+
`get_manager` and the four call sites (CLI, web app, MCP server).
|
|
864
|
+
- Note: WAL mode creates `-wal` and `-shm` sidecar files next to the
|
|
865
|
+
DB file. Backup scripts that previously copied just `labctl.db` now
|
|
866
|
+
need to include those sidecars (or quiesce the daemon and run
|
|
867
|
+
`PRAGMA wal_checkpoint(TRUNCATE)` before the backup).
|
|
868
|
+
- `LABCTL_DATABASE_TIMEOUT` env var overrides
|
|
869
|
+
`database.timeout_seconds` at load time, parallel to the other
|
|
870
|
+
`LABCTL_*` overrides. Useful for ad-hoc tuning under contention
|
|
871
|
+
without editing config.yaml.
|
|
872
|
+
- `INSERT OR IGNORE` on the `schema_version` bootstrap inserts
|
|
873
|
+
eliminates a latent cross-process race where two fresh-DB
|
|
874
|
+
initializers could both pass the `sqlite_master` probe and the
|
|
875
|
+
losing one would fail with a PRIMARY KEY conflict. Independent
|
|
876
|
+
of the lock fix but the WAL change made it slightly more likely
|
|
877
|
+
by shrinking the serialization window.
|
|
878
|
+
- `Database._init_lock` is a `threading.RLock` (not `Lock`) so a
|
|
879
|
+
callback into `initialize()` while the lock is held does not
|
|
880
|
+
self-deadlock. Defensive — nothing in the current codebase
|
|
881
|
+
triggers re-entry.
|
|
882
|
+
|
|
883
|
+
### Added (actuators feature, P1–P7, PR #4)
|
|
884
|
+
|
|
885
|
+
First-class support for USB-controlled relays as provisioned lab
|
|
886
|
+
resources, with per-target purpose **bindings** (recovery_mode,
|
|
887
|
+
boot_select, power_button, …) so verbs are semantic
|
|
888
|
+
(`labctl enter-recovery jetson-nano-2`) rather than device-specific.
|
|
889
|
+
Built per `docs/SPEC_actuators.md`. Since #37, `pre_power` straps are
|
|
890
|
+
set on every power-on path (see Fixed above).
|
|
891
|
+
|
|
892
|
+
- **Schema v8** — `actuators`, `actuator_channels`, `bindings` tables
|
|
893
|
+
with CHECK constraints on enum columns, UNIQUE on
|
|
894
|
+
`actuator_channel_id` (one binding per channel), UNIQUE on
|
|
895
|
+
`(sbc_id, purpose)` (one purpose per SBC). Migration v7→v8.
|
|
896
|
+
- **Driver layer** (`labctl.actuators`):
|
|
897
|
+
- `RelayDriver` ABC with structured `WriteOutcome` /
|
|
898
|
+
`ProbeResult` so composites can abort safely on USB drop mid-sequence.
|
|
899
|
+
- `lcus1_serial` — CH340-based 4-byte ASCII protocol at 9600/8N1,
|
|
900
|
+
write-only, software-tracked state, side-effect-free probe.
|
|
901
|
+
- `MockRelayDriver` for tests with programmable failure modes.
|
|
902
|
+
- `get_driver()` registry; future drivers raise NotImplementedError.
|
|
903
|
+
- **Runtime layer** (`labctl.actuators.runtime`):
|
|
904
|
+
- Per-channel non-blocking locks ⇒ ChannelBusyError for concurrent
|
|
905
|
+
verbs; verb/shape congruence enforced (BindingShapeError).
|
|
906
|
+
- `actuate_binding` / `release_binding` / `press_binding` —
|
|
907
|
+
desired_state persisted only on WriteOutcome.OK.
|
|
908
|
+
- `apply_pre_power_bindings` — re-applies pre_power straps before
|
|
909
|
+
`power_on` / `power_cycle` so recovery_mode survives power cycles.
|
|
910
|
+
- `enter_recovery` / `exit_recovery` — pre-flight probe + power_off
|
|
911
|
+
+ sleep + (de)assert + power_on, with explicit abort if the
|
|
912
|
+
actuator is unreachable BEFORE any power transition.
|
|
913
|
+
- `apply_safe_drive_on_startup` — daemon-start reconciliation that
|
|
914
|
+
leaves `desired_state=asserted` channels alone (recovery
|
|
915
|
+
mid-flash survives a daemon restart) and drives everything else
|
|
916
|
+
to `default_state`.
|
|
917
|
+
- **CLI**:
|
|
918
|
+
- `labctl actuator add/list/probe/remove/set`
|
|
919
|
+
- `labctl bind`, `labctl unbind`, `labctl bindings list`
|
|
920
|
+
- `labctl bindings actuate / release / press / status`
|
|
921
|
+
- `labctl enter-recovery / exit-recovery`
|
|
922
|
+
- `power on` / `power cycle` now consult pre_power bindings.
|
|
923
|
+
- **MCP**: complete tool set — provisioning, binding management,
|
|
924
|
+
operation verbs (claim-gated), and recovery composites.
|
|
925
|
+
- **SBC claim composition**: `claim_sbc` snapshot-checks per-channel
|
|
926
|
+
locks; refuses with ChannelBusyError if a verb is mid-flight.
|
|
927
|
+
- **Wiring-polarity validation** at bind time: refuses
|
|
928
|
+
`default_state == active`.
|
|
929
|
+
- **`pyproject.toml`**: new optional `actuators` extra pulling
|
|
930
|
+
`pyserial>=3.5`.
|
|
931
|
+
|
|
932
|
+
Tests: **897/897 pass.** ~140 new tests across schema migration,
|
|
933
|
+
manager CRUD, driver ABC contract, LCUS-1 frame builder + I/O,
|
|
934
|
+
runtime verbs, composite power integration, daemon-start safe-drive,
|
|
935
|
+
claim composition, CLI surface, and MCP tools.
|
|
936
|
+
|
|
937
|
+
#### Post-review hardening (PR #4)
|
|
938
|
+
|
|
939
|
+
- Hardware writes from automatic power-flow paths now leave audit
|
|
940
|
+
trails alongside operator-initiated verbs: `apply_pre_power_bindings`
|
|
941
|
+
emits `pre_power_apply` events (one per strap driven); daemon-start
|
|
942
|
+
`apply_safe_drive_on_startup` emits one synthetic `safe_drive`
|
|
943
|
+
summary event with held/drove/failed counts and details.
|
|
944
|
+
- `_validate_actuator_device_path` now rejects path traversal:
|
|
945
|
+
refuses any input containing `..` outright, then normalizes
|
|
946
|
+
remaining paths with `os.path.normpath` before the allow-list
|
|
947
|
+
check. Previously `/dev/lab/../etc/passwd` slipped through.
|
|
948
|
+
- `_audit_actuator_event` / `_audit_raw_actuator_set` / new
|
|
949
|
+
`_audit_safe_drive` wrap their emit call in try/except so a
|
|
950
|
+
bookkeeping failure can't fail a verb that physically succeeded.
|
|
951
|
+
- `_admin_actuator_ops_allowed` now logs at debug on config-load
|
|
952
|
+
errors so silent gate-disables are diagnosable.
|
|
953
|
+
- `pre_power_apply` audit details now include `cycle_changed` so a
|
|
954
|
+
paired `actuate` → `pre_power_apply` row pair (the normal
|
|
955
|
+
`enter_recovery` shape) reads as "operator drove the strap, then
|
|
956
|
+
the power-flow rechecked it" rather than as a double-toggle.
|
|
957
|
+
|
|
958
|
+
### Fixed
|
|
959
|
+
- Kasa multi-outlet strips no longer hit themselves with parallel KLAP
|
|
960
|
+
handshakes during monitor cycles. Previously each outlet of an N-outlet
|
|
961
|
+
strip opened its own KLAP session in parallel, so the strip
|
|
962
|
+
rate-limited bursts of N concurrent auth attempts as
|
|
963
|
+
`AuthenticationError`. Symptom in production logs: every monitor cycle
|
|
964
|
+
produced a wave of "challenge mismatch" errors against `192.168.4.140`
|
|
965
|
+
(a 6-outlet HS300) followed by an exhausted-retry ERROR per outlet,
|
|
966
|
+
then a fresh wave 60 s later. Retries never helped because the cause
|
|
967
|
+
was contention, not transience. Fix: a process-wide per-host state
|
|
968
|
+
cache (5 s TTL) with a per-host serialization lock. The first caller
|
|
969
|
+
in a cycle pays one KLAP handshake and populates is-on for every
|
|
970
|
+
outlet; concurrent callers on sibling outlets read from the cache.
|
|
971
|
+
Writes invalidate so the next read reflects the change.
|
|
972
|
+
|
|
973
|
+
### Added
|
|
974
|
+
- `labctl status` now appends the last 5 audit-log events under the SBC
|
|
975
|
+
table for at-a-glance recent-action context (oldest first so the newest
|
|
976
|
+
sits closest to the prompt).
|
|
977
|
+
- Per-SBC `power_cycle_delay_seconds` (schema v7). Acts as both the
|
|
978
|
+
default when `labctl power cycle --delay` is omitted *and* the floor
|
|
979
|
+
when a smaller value is passed (the CLI raises it with a warning).
|
|
980
|
+
Set/clear with `labctl edit <name> --cycle-delay <seconds>`
|
|
981
|
+
(negative value clears the override). Global fallback default is 3 s.
|
|
982
|
+
|
|
983
|
+
### Changed
|
|
984
|
+
- Kasa retry path now logs at WARNING for each retry attempt (was DEBUG,
|
|
985
|
+
invisible at default log levels), logs at INFO when a retry succeeds,
|
|
986
|
+
and logs at ERROR on final failure. Default retry count bumped from 1
|
|
987
|
+
to 2 (3 total attempts) to absorb intermittent KLAP auth flakes.
|
|
988
|
+
- `labctl power cycle --delay` is now optional. Default and floor are
|
|
989
|
+
resolved per-SBC: SBC value if set, otherwise 3 s. Help text corrected
|
|
990
|
+
(was "default: 2s", code default was 2.0 — now 3.0).
|
|
991
|
+
- Monitor daemon now runs two cadences (2026-04-30):
|
|
992
|
+
- **Fast track (`health.check_interval`, default 10 s)** — ping +
|
|
993
|
+
serial probes, parallelized across SBCs via a thread pool. Sets
|
|
994
|
+
`sbcs.status` more responsively.
|
|
995
|
+
- **Slow track (`health.power_check_interval`, default 60 s)** —
|
|
996
|
+
power probes, also parallelized. Stays at 60 s by default to limit
|
|
997
|
+
pressure on plug controllers (Kasa KLAP especially).
|
|
998
|
+
- **`health.min_sleep_seconds` (default 1.0)** — floor on the
|
|
999
|
+
between-cycle sleep so a runaway cycle (elapsed ≥ interval) can't
|
|
1000
|
+
pin a CPU spinning.
|
|
1001
|
+
- **Default `check_interval` 60 s → 10 s** for fresh installs;
|
|
1002
|
+
existing configs keep their explicit value. Set
|
|
1003
|
+
`power_check_interval: 10` in config to also tighten power
|
|
1004
|
+
polling once Phase 2 lands (Kasa broadcast discovery).
|
|
1005
|
+
- `HealthChecker.check_all` is now parallel (ThreadPoolExecutor,
|
|
1006
|
+
max 16 workers). Wall time is the *max* of per-SBC probe latencies
|
|
1007
|
+
rather than the sum. Per-SBC failures isolate; one bad probe
|
|
1008
|
+
returns an empty summary instead of dropping the row.
|
|
1009
|
+
- `labctl status --fast` staleness threshold derives from
|
|
1010
|
+
`2 × power_check_interval` (was hardcoded 120 s) so it adapts when
|
|
1011
|
+
you change the cadence.
|
|
1012
|
+
|
|
1013
|
+
### Added
|
|
1014
|
+
- `labctl status --fast` (alias `-f`) renders from the daemon's cached
|
|
1015
|
+
power observation instead of probing every plug live. Boot times go
|
|
1016
|
+
from "stuck on the slowest plug" to instant: 6.6s → 0.15s on a 6-SBC
|
|
1017
|
+
lab. Stale observations (older than 2× the monitor interval, default
|
|
1018
|
+
120s) render dimmed so it's obvious when the daemon is behind. The
|
|
1019
|
+
default mode still does live probing — `--fast` is opt-in. Schema v6
|
|
1020
|
+
adds `last_power_state` and `last_power_at` columns to `sbcs`; the
|
|
1021
|
+
monitor daemon UPDATEs them every cycle. 11 new tests cover the
|
|
1022
|
+
schema migration, manager observation API, and `--fast` rendering
|
|
1023
|
+
including staleness dimming and the no-observation-yet case
|
|
1024
|
+
(2026-04-27)
|
|
1025
|
+
|
|
1026
|
+
### Fixed
|
|
1027
|
+
- `labctl connect` now puts the local TTY in raw mode for the duration
|
|
1028
|
+
of the session, so each keystroke is forwarded immediately. Previously
|
|
1029
|
+
it shelled out to `nc`/`telnet`, which left the local terminal in
|
|
1030
|
+
cooked/line-buffered mode — boot menus with ~3s timeouts (Jetson
|
|
1031
|
+
U-Boot, Raspberry Pi, etc.) silently dropped keystrokes because they
|
|
1032
|
+
sat in the kernel until the user hit Enter. Native loop with select()
|
|
1033
|
+
+ tty.setraw + TCP_NODELAY; non-TTY stdin/stdout still falls back to
|
|
1034
|
+
`nc`. New escape sequence: Ctrl+] then 'q' (or Ctrl+\) to disconnect,
|
|
1035
|
+
Ctrl+] then Ctrl+] to send a literal Ctrl+]. 12 regression tests
|
|
1036
|
+
cover the escape state machine and TTY/non-TTY dispatch (2026-04-26)
|
|
1037
|
+
|
|
1038
|
+
- `labctl serial list` showed registered adapters as "unassigned" when
|
|
1039
|
+
their assignment rows had `serial_device_id = NULL`. Root cause:
|
|
1040
|
+
`ResourceManager.assign_serial_port()` only populated the FK when
|
|
1041
|
+
the caller explicitly supplied `serial_device_id`. Callers that
|
|
1042
|
+
passed only a `/dev/lab/<name>` path (MCP `assign_serial_port`,
|
|
1043
|
+
REST API, and CLI without `--serial-device`) left the FK NULL, so
|
|
1044
|
+
the two-tier adapter→port link was broken while the port itself
|
|
1045
|
+
kept working at the device layer.
|
|
1046
|
+
Fix: `assign_serial_port()` now auto-resolves the FK from
|
|
1047
|
+
`device_path` (expects `/dev/lab/<name>` or a bare name). New
|
|
1048
|
+
`labctl serial repair [--apply]` command backfills the FK for
|
|
1049
|
+
existing orphan rows (dry-run by default). 6 new regression tests
|
|
1050
|
+
cover the auto-resolve and repair paths (2026-04-19)
|
|
1051
|
+
|
|
1052
|
+
### Added
|
|
1053
|
+
- Activity stream Phase D — polish (2026-04-20)
|
|
1054
|
+
- MCP resources `lab://activity/recent` and
|
|
1055
|
+
`lab://activity/{sbc_name}` now expose the most recent activity
|
|
1056
|
+
events as JSON
|
|
1057
|
+
- `labctl activity export --format ndjson` exports activity events
|
|
1058
|
+
for downstream processing
|
|
1059
|
+
- The existing claim-sweep worker now also prunes activity events
|
|
1060
|
+
older than 30 days
|
|
1061
|
+
- Regression coverage added for MCP resources, CLI export, and
|
|
1062
|
+
activity retention pruning
|
|
1063
|
+
- Activity stream Phase C — source fidelity (2026-04-20)
|
|
1064
|
+
- MCP mutating tools now run inside `activity_context()` so audit
|
|
1065
|
+
events record `source=mcp` and the active MCP session ID as `actor`
|
|
1066
|
+
instead of falling back to `internal`
|
|
1067
|
+
- Flask request lifecycle now sets per-request audit attribution for
|
|
1068
|
+
authenticated REST API and web dashboard mutations (`api:<user>` /
|
|
1069
|
+
`web:<user>`)
|
|
1070
|
+
- Web claim endpoints now derive the claimant name from the correct
|
|
1071
|
+
session key (`session["user"]`)
|
|
1072
|
+
- Regression tests added for MCP, API-key, and logged-in web
|
|
1073
|
+
mutation attribution
|
|
1074
|
+
- Activity stream Phase B — live (2026-04-19)
|
|
1075
|
+
- `ActivityBroadcaster` background thread polls `audit_log` every
|
|
1076
|
+
500ms and fans new rows to per-subscriber queues. Cross-process:
|
|
1077
|
+
events from CLI/MCP/daemon reach browsers because everyone writes
|
|
1078
|
+
to the same DB.
|
|
1079
|
+
- `GET /activity` — web page with live feed, source/result filter
|
|
1080
|
+
chips, pause/resume, and server-rendered hydration of the last
|
|
1081
|
+
200 events.
|
|
1082
|
+
- `GET /activity/stream` — Server-Sent Events endpoint, session-
|
|
1083
|
+
authed. 15s keepalive heartbeats.
|
|
1084
|
+
- `GET /api/activity` — JSON query endpoint (API-key auth), same
|
|
1085
|
+
filters as the CLI.
|
|
1086
|
+
- `labctl activity tail --follow` — streams live events by polling
|
|
1087
|
+
the DB directly (works when the web service is stopped).
|
|
1088
|
+
- Activity link added to the nav bar.
|
|
1089
|
+
- 9 broadcaster tests (no-replay semantics, fan-out, unsubscribe,
|
|
1090
|
+
SSE frame format, slow-consumer drain, hydration + live).
|
|
1091
|
+
- `labctl services status` — reports on systemd units labctl depends
|
|
1092
|
+
on: `labctl-monitor`, `labctl-mcp`, `labctl-web`, and `ser2net`.
|
|
1093
|
+
Shows ActiveState/SubState, uptime, restart counter, and recent
|
|
1094
|
+
error-level journal lines for any unit that is unhealthy or has
|
|
1095
|
+
restarted. Exits non-zero if anything is not running. Extend the
|
|
1096
|
+
list with `--unit`. Non-privileged (uses `systemctl show` and
|
|
1097
|
+
`journalctl -u`) (2026-04-19)
|
|
1098
|
+
- Activity stream Phase A — foundation (2026-04-19)
|
|
1099
|
+
- `labctl.core.audit` module: `emit()`, `activity_context()`,
|
|
1100
|
+
`set_context()`, redaction, millisecond timestamps
|
|
1101
|
+
- Schema v5: extends `audit_log` with `actor`, `source`, `result`,
|
|
1102
|
+
`claim_id` columns + indexes
|
|
1103
|
+
- CLI boundary sets actor/source context in `main()`
|
|
1104
|
+
- `labctl activity tail` queries the stream with
|
|
1105
|
+
`--sbc/--actor/--source/--result/--since/-n` filters
|
|
1106
|
+
- Power CLI commands emit activity events on success/failure
|
|
1107
|
+
- Existing manager `_audit_log` calls route through `audit.emit()`
|
|
1108
|
+
- 18 tests: schema migration, emit, contextvars, redaction, truncation
|
|
1109
|
+
- Spec: `docs/RESOLVED_SPEC_activity_stream.md`
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
- Hardware claims polish, Phase E (2026-04-16)
|
|
1113
|
+
- `ClaimsConfig.validate()` clamps invalid bounds at load time
|
|
1114
|
+
- `prune_released_claims()` auto-deletes old released claims (wired
|
|
1115
|
+
into CLI expire + MCP sweep)
|
|
1116
|
+
- `get_claim_metrics()` aggregate stats — exposed via `labctl claims stats`
|
|
1117
|
+
and `lab://claims/metrics` MCP resource
|
|
1118
|
+
- `AGENT_RULES.md` section 11: claim workflow, naming, duration guide
|
|
1119
|
+
- Hardware claims operator tooling, Phase D (2026-04-16)
|
|
1120
|
+
- Web REST API: full claim lifecycle at `/api/claims/*`
|
|
1121
|
+
- Dashboard: claim badges on SBC cards (agent, time remaining, request warning)
|
|
1122
|
+
- SBC detail page: claim section with holder info, pending requests,
|
|
1123
|
+
and force-release button
|
|
1124
|
+
- MCP tool responses: `_claim_advisory()` appends pending release request
|
|
1125
|
+
notices to all 10 gated tools on success
|
|
1126
|
+
- Hardware claims expiry and dead-session handling, Phase C (2026-04-16)
|
|
1127
|
+
- MCP `atexit` handler: releases this session's claims on clean exit
|
|
1128
|
+
- `release_dead_sessions()`: checks `kill -0 <pid>` for mcp-stdio claims;
|
|
1129
|
+
dead process + expired grace → auto-release as `session-lost`
|
|
1130
|
+
- Background daemon thread in MCP server sweeps every 30s
|
|
1131
|
+
- `labctl claims expire` CLI command for cron-driven sweeps
|
|
1132
|
+
- Grace period respected: dead sessions within grace window are left alone
|
|
1133
|
+
- Hardware claims MCP integration, Phase B (2026-04-16)
|
|
1134
|
+
- MCP tools: `claim_sbc`, `release_sbc`, `renew_sbc_claim`, `list_claims`,
|
|
1135
|
+
`get_claim`, `request_sbc_release`, `force_release_sbc`
|
|
1136
|
+
- MCP resources: `lab://claims`, `lab://claims/{sbc_name}`,
|
|
1137
|
+
`lab://claims/history/{sbc_name}`
|
|
1138
|
+
- Claim enforcement on 10 mutating MCP tools — other-agent claims
|
|
1139
|
+
return structured `sbc_claimed` JSON errors with hints
|
|
1140
|
+
- Session identity via `mcp-stdio:<pid>-<start_epoch>` for stdio transport
|
|
1141
|
+
- Heartbeat fires on every claimant tool call (reads and writes)
|
|
1142
|
+
- Hardware claims (exclusive access coordination), Phase A (2026-04-16)
|
|
1143
|
+
- SQLite schema v4 adds `claims` and `claim_requests` tables
|
|
1144
|
+
- Partial unique index enforces at most one active claim per SBC
|
|
1145
|
+
- CLI: `labctl claim`, `release`, `renew`, `force-release`, `request-release`
|
|
1146
|
+
- CLI: `labctl claims list | show <sbc> | history <sbc>`
|
|
1147
|
+
- `labctl status` surfaces the current claim holder and pending release requests
|
|
1148
|
+
- `labctl remove <sbc>` refuses while a claim is active (use `--force` to override)
|
|
1149
|
+
- Manager ops: `claim_sbc`, `release_claim`, `renew_claim`, `heartbeat_claim`,
|
|
1150
|
+
`get_active_claim`, `list_active_claims`, `list_claim_history`,
|
|
1151
|
+
`force_release_claim`, `expire_stale_claims`, `record_release_request`
|
|
1152
|
+
- Structured exceptions: `ClaimConflict`, `ClaimNotFoundError`, `NotClaimantError`,
|
|
1153
|
+
`UnknownSBCError`
|
|
1154
|
+
- Acquisition runs expire-stale sweep inside the transaction so past-deadline
|
|
1155
|
+
claims don't block new acquisitions even when no sweeper has run
|
|
1156
|
+
- Spec: `docs/RESOLVED_SPEC_claims.md`
|
|
1157
|
+
- SDWire power safety interlock (2026-04-09)
|
|
1158
|
+
- `sdwire_to_host` rejects if SBC is powered on (prevents SD card bus contention)
|
|
1159
|
+
- `--force` flag (CLI) / `force` parameter (MCP) to override when SBC is halted but power on
|
|
1160
|
+
- `sdwire_update` and `flash_image` auto power-off SBC before switching to host
|
|
1161
|
+
- Best-effort: allows operation when power state is unknown
|
|
1162
|
+
- `flash_image` MCP tool and enhanced `labctl sdwire flash` CLI (2026-04-06)
|
|
1163
|
+
- Supports .img, .img.xz, .img.gz compressed formats
|
|
1164
|
+
- Safety checks: block device size validation (max 256 GB), mount check, image format validation
|
|
1165
|
+
- Post-flash file copies to boot partition (e.g., custom config.txt)
|
|
1166
|
+
- On flash failure, SD card left on host for inspection
|
|
1167
|
+
- Automatic power-off before flash, optional reboot after
|
|
1168
|
+
- MCP CRUD tools for full device lifecycle management (2026-04-04)
|
|
1169
|
+
- Remove tools: `remove_serial_port`, `remove_network_address`, `remove_power_plug`
|
|
1170
|
+
- Serial device tools: `add_serial_device`, `remove_serial_device`, `serial_discover`
|
|
1171
|
+
- SDWire tools: `sdwire_add`, `sdwire_remove`, `sdwire_assign`, `sdwire_unassign`, `sdwire_discover`
|
|
1172
|
+
- AI assistants can now fully manage lab resources without CLI
|
|
1173
|
+
- `/deploy-and-test` Claude Code skill (2026-04-02)
|
|
1174
|
+
- Orchestrates build, deploy, boot, capture, and analysis via MCP tools
|
|
1175
|
+
- Auto-discovers config from `.claude/deploy-config.json`, `CLAUDE.md`, or user prompts
|
|
1176
|
+
- Boot reliability testing mode with `--runs N`
|
|
1177
|
+
- Located at `skills/deploy-and-test.md`, opt in via project settings.json
|
|
1178
|
+
- Serial I/O and boot test tools (2026-04-02)
|
|
1179
|
+
- `labctl serial capture <port> --timeout N --until <pattern>` — capture serial output with timeout or pattern match
|
|
1180
|
+
- `labctl serial send <port> <data>` — send data to serial port, optional `--capture` for response
|
|
1181
|
+
- `labctl boot-test <sbc>` — automated boot reliability testing: deploy, reboot N times, report success rate
|
|
1182
|
+
- MCP tools: `serial_capture`, `serial_send`, `boot_test`
|
|
1183
|
+
- Port name resolution: accepts port alias or SBC name (defaults to console port)
|
|
1184
|
+
- 33 new tests for serial capture, send, and boot test
|
|
1185
|
+
- SDWire SD card multiplexer support (2026-03-31)
|
|
1186
|
+
- Supports SDWire, SDWireC, and SDWire3 devices via `sdwire` Python library
|
|
1187
|
+
- `labctl sdwire` CLI command group: discover, add, remove, list, assign, unassign, dut, host, flash, update
|
|
1188
|
+
- `labctl sdwire flash <sbc> <image>` — automated workflow: switch to host, dd image, switch to DUT, power cycle
|
|
1189
|
+
- `labctl sdwire update <sbc> -p N -c src:dest -r old:new -d file` — atomic partition operations: copy, rename, delete files, optional reboot
|
|
1190
|
+
- Destination paths relative to partition root (e.g., `kernel.img` not `/boot/firmware/kernel.img`)
|
|
1191
|
+
- Database schema v3: `sdwire_devices` and `sdwire_assignments` tables
|
|
1192
|
+
- SDWire shown in `labctl info` output
|
|
1193
|
+
- MCP resource `lab://sdwire-devices` and tools `sdwire_to_dut`, `sdwire_to_host`, `sdwire_update`
|
|
1194
|
+
- Export/import includes SDWire device assignments
|
|
1195
|
+
- Sudoers-based privilege escalation for SD card mount/umount/dd/sync operations (enables MCP server to manage SD cards without running as root)
|
|
1196
|
+
- 55 new tests (322 total)
|
|
1197
|
+
- Global `--delay` / `-d` CLI option (2026-03-29)
|
|
1198
|
+
- Adds a delay in seconds before any command executes
|
|
1199
|
+
- Useful for chaining commands after power cycles (e.g., `labctl -d 30 health-check`)
|
|
1200
|
+
- Suppressed with `--quiet`, prints "Waiting Xs..." otherwise
|
|
1201
|
+
- MCP (Model Context Protocol) server for AI integration (2026-03-28)
|
|
1202
|
+
- `labctl mcp` command — stdio (default) and HTTP transports
|
|
1203
|
+
- 7 resources: lab://sbcs, lab://sbcs/{name}, lab://power/{name},
|
|
1204
|
+
lab://serial-devices, lab://ports, lab://health/{name}, lab://status
|
|
1205
|
+
- 10 tools: power_on/off/cycle, run_health_check, add/remove/update_sbc,
|
|
1206
|
+
assign_serial_port, assign_power_plug, set_network_address
|
|
1207
|
+
- 2 prompts: debug-sbc (guided debugging), lab-report (status overview)
|
|
1208
|
+
- Uses official `mcp` Python SDK with FastMCP
|
|
1209
|
+
- 28 comprehensive tests for tools and resources
|
|
1210
|
+
- Two-tier serial device management (2026-03-28)
|
|
1211
|
+
- `serial_devices` database table for registering physical USB-serial adapters
|
|
1212
|
+
- `labctl serial` CLI command group: discover, add, remove, list, rename, udev
|
|
1213
|
+
- Port assignments now support `--alias` for human-friendly names and `--serial-device` to link to registered adapters
|
|
1214
|
+
- `labctl connect` resolves by alias, SBC name, or device path
|
|
1215
|
+
- `labctl serial udev --install --reload` generates udev rules from database (replaces manual YAML editing)
|
|
1216
|
+
- Pure-Python USB discovery via `/sys` and `udevadm` (no external script dependency)
|
|
1217
|
+
- Database schema v2 migration (adds serial_devices table, alias/serial_device_id columns to serial_ports)
|
|
1218
|
+
- Export/import includes serial devices and port aliases
|
|
1219
|
+
- Web UI updated: alias field in port assignment form, alias column in port table
|
|
1220
|
+
- REST API updated: serial device endpoints, alias in port endpoints
|
|
1221
|
+
- Udev rules file group-writable by labctl group (no sudo needed for install)
|
|
1222
|
+
- Sudoers entries for passwordless udevadm and ser2net restart
|
|
1223
|
+
- Kasa power retry logic (2026-03-28)
|
|
1224
|
+
- Retries up to 2 times on transient KLAP authentication failures
|
|
1225
|
+
- Handles intermittent HS300 firmware handshake errors transparently
|
|
1226
|
+
- Native HTTPS support for web server (2026-03-28)
|
|
1227
|
+
- `--cert` and `--key` CLI flags on `labctl web` command
|
|
1228
|
+
- `web:` config section for persistent SSL configuration (cert_file, key_file)
|
|
1229
|
+
- Documentation for self-signed certificate generation and setup
|
|
1230
|
+
- Systemd service updated with SSL cert read access and port 443 capability
|
|
1231
|
+
- Kasa Smart Power Strip support (2026-03-28)
|
|
1232
|
+
- Auto-detects plugs vs strips via `kasa.Discover.discover_single()`
|
|
1233
|
+
- Multi-outlet control using `plug_index` (1-based) for individual outlets
|
|
1234
|
+
- TP-Link cloud credential support via `kasa:` config section (required for KLAP devices)
|
|
1235
|
+
- Proper async cleanup (`device.disconnect()`) to prevent unclosed session warnings
|
|
1236
|
+
- Detailed logging at all levels (DEBUG/INFO/ERROR) for troubleshooting
|
|
1237
|
+
- SBC renaming support (2026-03-28)
|
|
1238
|
+
- `labctl edit <name> --rename <new-name>` CLI command
|
|
1239
|
+
- Rename via REST API (`PUT /api/sbcs/<name>` with `{"name": "new-name"}`)
|
|
1240
|
+
- Rename via web UI edit form
|
|
1241
|
+
- CLI logging initialization (2026-03-28)
|
|
1242
|
+
- `logging.basicConfig()` wired to config `log_level` in CLI entrypoint
|
|
1243
|
+
- `-v` flag sets DEBUG, `-q` flag sets WARNING
|
|
1244
|
+
- Log output goes to stderr to avoid interfering with command output
|
|
1245
|
+
- `scripts/kasa-debug.py` — standalone debug script for Kasa device troubleshooting
|
|
1246
|
+
- Device discovery, status, and power control
|
|
1247
|
+
- `test-auth` action with credential hash diagnostics
|
|
1248
|
+
- Verbose mode includes python-kasa internal debug output
|
|
1249
|
+
|
|
1250
|
+
### Changed
|
|
1251
|
+
- Default log level changed from INFO to WARNING (2026-03-28)
|
|
1252
|
+
- Kasa controller logging lowered from INFO/ERROR to DEBUG (2026-03-28)
|
|
1253
|
+
- Prevents log noise in `labctl status` output
|
|
1254
|
+
- `labctl ser2net generate` uses port alias as connection name when available (2026-03-28)
|
|
1255
|
+
- `labctl ser2net reload` auto-escalates to sudo when needed (2026-03-28)
|
|
1256
|
+
|
|
1257
|
+
### Fixed
|
|
1258
|
+
- Critical: `get_status_history()` and `get_uptime()` called nonexistent `execute_query()` (2026-04-03)
|
|
1259
|
+
- Replaced with `execute()`, fixed tuple indexing to dict-style access for `sqlite3.Row`
|
|
1260
|
+
- Critical: Open redirect in login — `next` parameter now rejects absolute and protocol-relative URLs (2026-04-03)
|
|
1261
|
+
- Path traversal protection in SDWire file operations (2026-04-03)
|
|
1262
|
+
- Copy, rename, and delete paths validated via `realpath` to stay under mount point
|
|
1263
|
+
- Null checks after `execute_one()` on 5 insert-then-retrieve sites in manager (2026-04-03)
|
|
1264
|
+
- Power controller error handling improvements (2026-04-03)
|
|
1265
|
+
- Added warning-level logging to Tasmota/Shelly bare exception handlers
|
|
1266
|
+
- Added timeout to Kasa `future.result()` to prevent indefinite blocking
|
|
1267
|
+
- Fixed missing space in Kasa error message formatting
|
|
1268
|
+
- SerialProxy thread safety fixes (2026-04-03)
|
|
1269
|
+
- `asyncio.Lock` for write lock acquisition to prevent TOCTOU race
|
|
1270
|
+
- Snapshot `clients` dict before broadcast iteration
|
|
1271
|
+
- Guard orphaned `create_task(self.stop())` with state check
|
|
1272
|
+
- ser2net generate: add /dev/lab/ prefix for bare device names (2026-04-09)
|
|
1273
|
+
- ser2net format string: revert to `<baud><parity><databits><stopbits>` (e.g. `115200n81`) — ser2net rejects the comma-separated form with "unknown parameter 8n1" (2026-04-16)
|
|
1274
|
+
- Monitor daemon: `AlertManager.trigger()` call on power state change used kwargs instead of a single `Alert` object, raising `TypeError` each cycle. The uncaught exception aborted `run_once()` mid-iteration, preventing DB status updates for any SBCs processed after the crashing one. Added regression tests for `MonitorDaemon` (2026-04-17)
|
|
1275
|
+
- Consolidated SBC.to_dict() serialization, eliminating duplication between MCP and API (2026-04-03)
|
|
1276
|
+
- Atomic upsert (INSERT...ON CONFLICT) for serial ports, network addresses, power plugs, SDWire assignments (2026-04-03)
|
|
1277
|
+
- Config load exception now logs warning instead of silently continuing (2026-04-03)
|
|
1278
|
+
- SessionLogger rotation handles disk errors without leaking file handles (2026-04-03)
|
|
1279
|
+
- ser2net format string: extracted PARITY_CHARS constant (2026-04-03)
|
|
1280
|
+
- Extracted `_load_serial_device()` helper, eliminating 3x code duplication in manager (2026-04-03)
|
|
1281
|
+
- Removed unused `_connection` field from Database class (2026-04-03)
|
|
1282
|
+
- Simplified redundant `is True`/`is False` checks in Shelly controller (2026-04-03)
|
|
1283
|
+
- SessionLogger timestamps now include milliseconds to prevent collision (2026-04-03)
|
|
1284
|
+
- MCP parameter validation: `power_cycle` rejects negative delay, `boot_test` requires runs >= 1 and timeout > 0 (2026-04-03)
|
|
1285
|
+
- Power control API rate limiting: 5s minimum between power cycles per SBC (HTTP 429) (2026-04-03)
|
|
1286
|
+
- Health check power probe using wrong function signature (2026-03-28)
|
|
1287
|
+
- `get_controller(sbc.power_plug)` → `PowerController.from_plug(sbc.power_plug)`
|
|
1288
|
+
- Monitor daemon ping failing under systemd sandbox (2026-03-28)
|
|
1289
|
+
- Removed `NoNewPrivileges=yes`, added `AmbientCapabilities=CAP_NET_RAW`
|
|
1290
|
+
- Kasa unclosed aiohttp sessions on authentication failure (2026-03-28)
|
|
1291
|
+
- `_get_device()` now disconnects device in all error paths
|
|
1292
|
+
- USB serial discovery returning root hub path instead of device-specific paths (2026-03-28)
|
|
1293
|
+
- Authentication system (2026-03-06)
|
|
1294
|
+
- Session-based web login with username/password
|
|
1295
|
+
- API key authentication via `X-API-Key` header for REST endpoints
|
|
1296
|
+
- CSRF protection on all web POST/PUT/DELETE forms
|
|
1297
|
+
- Auth disabled by default — existing deployments unaffected
|
|
1298
|
+
- `/api/health` remains open without authentication for monitoring tools
|
|
1299
|
+
- `AuthConfig` and `UserConfig` dataclasses in config module
|
|
1300
|
+
- Login page template with dark theme styling
|
|
1301
|
+
- Logout button in navigation bar when logged in
|
|
1302
|
+
- `labctl user` CLI command group:
|
|
1303
|
+
- `labctl user hash-password` — generate werkzeug password hash
|
|
1304
|
+
- `labctl user generate-key` — generate random API key
|
|
1305
|
+
- `labctl user add <username>` — interactive user creation with YAML output
|
|
1306
|
+
- `labctl user verify <username>` — verify password against config
|
|
1307
|
+
- Auth section in `config/labctl.yaml.example`
|
|
1308
|
+
- 14 authentication integration tests (188 total)
|
|
1309
|
+
- `[tool.setuptools.package-data]` in pyproject.toml for templates and static files
|
|
1310
|
+
- Project documentation structure
|
|
1311
|
+
- ARCHITECTURE.md - System design and component specifications
|
|
1312
|
+
- IMPLEMENTATION.md - Milestone-based TODO lists
|
|
1313
|
+
- AGENT_RULES.md - Development process and standards
|
|
1314
|
+
- STATUS.md - Current project state tracking
|
|
1315
|
+
- CHANGELOG.md - This file
|
|
1316
|
+
- M1.1 Project Setup (2025-12-31)
|
|
1317
|
+
- Created project directory structure (src/labctl, tests, scripts, config, docs)
|
|
1318
|
+
- Added pyproject.toml with dependencies (click, pyyaml, requests)
|
|
1319
|
+
- Created virtual environment with dev tools (pytest, black, isort, flake8)
|
|
1320
|
+
- Installed ser2net system package
|
|
1321
|
+
- M1.2 udev Rules System (2025-12-31)
|
|
1322
|
+
- Created `scripts/discover-usb-serial.sh` for USB device enumeration
|
|
1323
|
+
- Created `scripts/generate-udev-rules.py` for rule generation from YAML mapping
|
|
1324
|
+
- Created `scripts/install-udev.sh` for udev rules installation
|
|
1325
|
+
- Created `docs/HARDWARE_MAP.md` with USB topology documentation
|
|
1326
|
+
- Symlinks now created under `/dev/lab/` for deterministic device access
|
|
1327
|
+
- M1.3 ser2net Configuration (2025-12-31)
|
|
1328
|
+
- Created `src/labctl/serial/ser2net.py` for config generation
|
|
1329
|
+
- Support for baud rate, parity, local-only connections
|
|
1330
|
+
- Generated config for 5 ports on TCP 4000-4004
|
|
1331
|
+
- All ports verified accessible via TCP
|
|
1332
|
+
- M1.4 Basic CLI Structure (2025-12-31)
|
|
1333
|
+
- Created `src/labctl/cli.py` with Click framework
|
|
1334
|
+
- Implemented `labctl ports` - lists configured serial ports with TCP mappings
|
|
1335
|
+
- Implemented `labctl connect <port>` - connects via TCP or direct serial
|
|
1336
|
+
- Added 8 CLI integration tests
|
|
1337
|
+
- M1.5 Configuration File (2025-12-31)
|
|
1338
|
+
- Created `src/labctl/core/config.py` with Config dataclasses
|
|
1339
|
+
- YAML config loading from ~/.config/labctl/config.yaml
|
|
1340
|
+
- Environment variable overrides (LABCTL_DEV_DIR, LABCTL_BASE_TCP_PORT, etc.)
|
|
1341
|
+
- Added `-c/--config` CLI option
|
|
1342
|
+
- Added 17 unit tests for config module
|
|
1343
|
+
- **Milestone 1 Complete!**
|
|
1344
|
+
- M2.1-2.5 Data Layer (2025-12-31)
|
|
1345
|
+
- Created `src/labctl/core/database.py` with SQLite schema
|
|
1346
|
+
- Tables: sbcs, serial_ports, network_addresses, power_plugs, status_log, audit_log
|
|
1347
|
+
- Foreign keys with cascade delete
|
|
1348
|
+
- Schema versioning support
|
|
1349
|
+
- Created `src/labctl/core/models.py` with data models
|
|
1350
|
+
- SBC, SerialPort, NetworkAddress, PowerPlug dataclasses
|
|
1351
|
+
- Status, PortType, AddressType, PlugType enums
|
|
1352
|
+
- from_row() methods for database deserialization
|
|
1353
|
+
- Created `src/labctl/core/manager.py` with ResourceManager
|
|
1354
|
+
- Full CRUD operations for SBCs
|
|
1355
|
+
- Serial port, network address, power plug assignment
|
|
1356
|
+
- Audit logging for all operations
|
|
1357
|
+
- Added CLI SBC management commands
|
|
1358
|
+
- `labctl list` - tabular output with filters (--project, --status)
|
|
1359
|
+
- `labctl add <name>` - create SBC with options
|
|
1360
|
+
- `labctl remove <name>` - delete with confirmation
|
|
1361
|
+
- `labctl info <name>` - detailed SBC view
|
|
1362
|
+
- `labctl edit <name>` - update properties
|
|
1363
|
+
- Added CLI port assignment commands
|
|
1364
|
+
- `labctl port assign <sbc> <type> <device>` - with auto TCP port
|
|
1365
|
+
- `labctl port remove <sbc> <type>`
|
|
1366
|
+
- `labctl port list` - all port assignments
|
|
1367
|
+
- Added 27 new tests (8 database, 19 manager)
|
|
1368
|
+
- Total: 62 tests passing
|
|
1369
|
+
- M2.6-2.7 Network and ser2net Commands (2025-12-31)
|
|
1370
|
+
- Added `labctl network set <sbc> <type> <ip>` with --mac, --hostname options
|
|
1371
|
+
- Added `labctl network remove <sbc> <type>`
|
|
1372
|
+
- Added `labctl ser2net generate` - generates config from database
|
|
1373
|
+
- Supports --output file and --install flag
|
|
1374
|
+
- Added `labctl ser2net reload` - restarts ser2net service
|
|
1375
|
+
- **Milestone 2 Complete!**
|
|
1376
|
+
- M3 Power Control (2025-12-31)
|
|
1377
|
+
- Created power module `src/labctl/power/` with controller framework
|
|
1378
|
+
- PowerController ABC with power_on, power_off, power_cycle, get_state
|
|
1379
|
+
- PowerState enum (ON, OFF, UNKNOWN)
|
|
1380
|
+
- Factory function get_controller() for plug type dispatch
|
|
1381
|
+
- Implemented TasmotaController for Tasmota smart plugs
|
|
1382
|
+
- HTTP API client with multi-relay support
|
|
1383
|
+
- Implemented ShellyController for Shelly devices
|
|
1384
|
+
- Implemented KasaController stub for TP-Link Kasa (requires python-kasa)
|
|
1385
|
+
- Added CLI plug commands
|
|
1386
|
+
- `labctl plug assign <sbc> <type> <address>` with --index option
|
|
1387
|
+
- `labctl plug remove <sbc>`
|
|
1388
|
+
- Added CLI power commands
|
|
1389
|
+
- `labctl power on/off/cycle/status <sbc>`
|
|
1390
|
+
- `labctl power-all on|off` with --project filter
|
|
1391
|
+
- Added 19 power module tests (81 total)
|
|
1392
|
+
- **Milestone 3 Complete!**
|
|
1393
|
+
- M4 CLI Completion (2025-12-31)
|
|
1394
|
+
- Added `labctl console <sbc>` - connect to SBC's serial console by name
|
|
1395
|
+
- Looks up port from database, supports --type for jtag/debug
|
|
1396
|
+
- Added `labctl ssh <sbc>` - SSH to SBC by name
|
|
1397
|
+
- Looks up IP from database, uses configured ssh_user
|
|
1398
|
+
- Supports --user override
|
|
1399
|
+
- Added `labctl status` - overview with color-coded status and power state
|
|
1400
|
+
- Added `labctl export` - export SBCs to YAML/JSON
|
|
1401
|
+
- Added `labctl import <file>` - import SBCs with --update option
|
|
1402
|
+
- Added `labctl completion` - shell completion for bash/zsh/fish
|
|
1403
|
+
- **Milestone 4 Complete!**
|
|
1404
|
+
- M5 Web Interface (2025-12-31)
|
|
1405
|
+
- Created Flask web module with application factory pattern
|
|
1406
|
+
- Implemented REST API endpoints:
|
|
1407
|
+
- `GET/POST /api/sbcs` - list and create SBCs
|
|
1408
|
+
- `GET/PUT/DELETE /api/sbcs/<name>` - CRUD operations
|
|
1409
|
+
- `GET/POST /api/sbcs/<name>/power` - power status and control
|
|
1410
|
+
- `GET /api/ports` - list serial ports
|
|
1411
|
+
- `GET /api/health` and `/api/status` - system status
|
|
1412
|
+
- Created web dashboard with dark theme:
|
|
1413
|
+
- Dashboard page with SBC card grid
|
|
1414
|
+
- SBC detail page with all information
|
|
1415
|
+
- Power control buttons (on/off/cycle)
|
|
1416
|
+
- Status indicators and responsive layout
|
|
1417
|
+
- Added `labctl web` command with --host, --port, --debug options
|
|
1418
|
+
- **Milestone 5 Complete!**
|
|
1419
|
+
- M6 Multi-Client Serial Access (2026-01-01)
|
|
1420
|
+
- Created serial proxy module `src/labctl/serial/proxy.py`
|
|
1421
|
+
- SerialProxy class with asyncio-based fan-out architecture
|
|
1422
|
+
- First-writer-wins write lock policy
|
|
1423
|
+
- Multiple readers can view console output simultaneously
|
|
1424
|
+
- ProxyManager for managing multiple proxy instances
|
|
1425
|
+
- Added session logging
|
|
1426
|
+
- SessionLogger class for timestamped traffic logs
|
|
1427
|
+
- Logs stored in ~/.local/share/labctl/logs/
|
|
1428
|
+
- Format: [timestamp] [direction] data
|
|
1429
|
+
- Added ProxyConfig to config.py
|
|
1430
|
+
- Configurable port range (default 5000-5099)
|
|
1431
|
+
- Write policy selection (first, all, queue)
|
|
1432
|
+
- Max clients per proxy, idle timeout
|
|
1433
|
+
- Added CLI proxy commands
|
|
1434
|
+
- `labctl proxy start <sbc>` - start proxy for SBC (foreground)
|
|
1435
|
+
- `labctl proxy list` - list running proxies (placeholder)
|
|
1436
|
+
- `labctl sessions [sbc]` - list connected clients (placeholder)
|
|
1437
|
+
- Added web console with xterm.js
|
|
1438
|
+
- WebSocket bridge for browser-to-proxy communication
|
|
1439
|
+
- Console page at /sbc/<name>/console
|
|
1440
|
+
- Full terminal emulator in browser
|
|
1441
|
+
- Added 26 proxy tests (143 total)
|
|
1442
|
+
- **Milestone 6 Complete!**
|
|
1443
|
+
- M7 Monitoring and Health (2026-01-01)
|
|
1444
|
+
- Created health check module `src/labctl/health/`
|
|
1445
|
+
- HealthChecker class with ping, serial, and power checks
|
|
1446
|
+
- CheckResult and HealthCheckSummary dataclasses
|
|
1447
|
+
- Automatic status determination based on check results
|
|
1448
|
+
- Added alerting framework
|
|
1449
|
+
- Alert and AlertLevel for structured notifications
|
|
1450
|
+
- AlertHandler ABC with pluggable handlers
|
|
1451
|
+
- LogAlertHandler for file-based alerts
|
|
1452
|
+
- ConsoleAlertHandler for terminal output
|
|
1453
|
+
- EmailAlertHandler and SlackAlertHandler stubs for future
|
|
1454
|
+
- AlertManager for dispatching to multiple handlers
|
|
1455
|
+
- Created monitoring daemon
|
|
1456
|
+
- MonitorDaemon class for periodic health checks
|
|
1457
|
+
- Configurable check interval
|
|
1458
|
+
- Automatic status updates on check results
|
|
1459
|
+
- Alert triggering on status/power changes
|
|
1460
|
+
- Added status tracking
|
|
1461
|
+
- log_status() method writes to status_log table
|
|
1462
|
+
- get_status_history() for retrieving history
|
|
1463
|
+
- cleanup_old_status_logs() for retention management
|
|
1464
|
+
- Added HealthConfig to configuration
|
|
1465
|
+
- check_interval, ping_timeout, serial_timeout
|
|
1466
|
+
- status_retention_days, alert_log_path
|
|
1467
|
+
- alert_on_offline, alert_on_power_change options
|
|
1468
|
+
- Added CLI commands
|
|
1469
|
+
- `labctl health-check` - run health checks on SBCs
|
|
1470
|
+
- --type ping|serial|power|all filter
|
|
1471
|
+
- --sbc to check single SBC
|
|
1472
|
+
- --update to update status in database
|
|
1473
|
+
- `labctl monitor` - start monitoring daemon
|
|
1474
|
+
- --interval to override check frequency
|
|
1475
|
+
- --no-update, --no-alerts options
|
|
1476
|
+
- Added web endpoints
|
|
1477
|
+
- GET /api/sbcs/<name>/history - status history
|
|
1478
|
+
- GET /api/health/check - run health checks via API
|
|
1479
|
+
- Status history page at /sbc/<name>/history
|
|
1480
|
+
- Added 28 health module tests (171 total)
|
|
1481
|
+
- **Milestone 7 Complete!**
|
|
1482
|
+
- Deferred Items Implementation (2026-01-01)
|
|
1483
|
+
- CLI Enhancements:
|
|
1484
|
+
- `labctl log <sbc>` - capture serial output to timestamped file
|
|
1485
|
+
- `--follow/-f` for continuous output
|
|
1486
|
+
- `--lines/-n` to capture N lines then exit
|
|
1487
|
+
- `--timestamp/--no-timestamp` for timestamp prefixes
|
|
1488
|
+
- `labctl status --watch` - continuous status update display
|
|
1489
|
+
- `labctl port list --unassigned` - show unassigned /dev/lab/* devices
|
|
1490
|
+
- `-q/--quiet` global flag for suppressed output
|
|
1491
|
+
- Command aliases: ls=list, rm=remove, delete=remove, show=info
|
|
1492
|
+
- Web Interface:
|
|
1493
|
+
- SBC edit form on detail page
|
|
1494
|
+
- Port/network/plug assignment forms
|
|
1495
|
+
- Settings page with configuration display
|
|
1496
|
+
- Settings link in navigation
|
|
1497
|
+
- GET /api/sbcs/<name>/console/info endpoint
|
|
1498
|
+
- GET /api/sbcs/<name>/uptime endpoint
|
|
1499
|
+
- Infrastructure:
|
|
1500
|
+
- Systemd service files (labctl-monitor.service, labctl-web.service)
|
|
1501
|
+
- Service installation script (scripts/install-services.sh)
|
|
1502
|
+
- Session log rotation with size-based rotation and compression
|
|
1503
|
+
- Old log cleanup based on retention period
|
|
1504
|
+
- Health Monitoring:
|
|
1505
|
+
- Uptime tracking with current uptime and 24h percentage
|
|
1506
|
+
- Serial probe with string/response verification
|
|
1507
|
+
- `serial_probe()` method for active serial testing
|
|
1508
|
+
|
|
1509
|
+
### Changed
|
|
1510
|
+
- Moved documentation files to docs/ folder (AGENT_RULES.md, IMPLEMENTATION.md, DECISIONS.md)
|
|
1511
|
+
- Fixed README.md and CLAUDE.md paths to reference docs/ folder
|
|
1512
|
+
|
|
1513
|
+
[Unreleased]: https://github.com/johnjezl/Embedded-Lab-Control/compare/v0.2.0...HEAD
|
|
1514
|
+
[0.2.0]: https://github.com/johnjezl/Embedded-Lab-Control/releases/tag/v0.2.0
|