ironharness 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.
- ironharness-0.2.0/.github/workflows/tests.yml +23 -0
- ironharness-0.2.0/.gitignore +25 -0
- ironharness-0.2.0/PKG-INFO +88 -0
- ironharness-0.2.0/README.md +74 -0
- ironharness-0.2.0/pyproject.toml +39 -0
- ironharness-0.2.0/reports/2026-09-07-qwen3.5-9b.html +30 -0
- ironharness-0.2.0/reports/2026-09-07-qwen3.5-9b.json +89 -0
- ironharness-0.2.0/src/io_core/__init__.py +46 -0
- ironharness-0.2.0/src/io_core/errors.py +27 -0
- ironharness-0.2.0/src/io_core/esp_flash.py +123 -0
- ironharness-0.2.0/src/io_core/faults.py +130 -0
- ironharness-0.2.0/src/io_core/file_sandbox.py +89 -0
- ironharness-0.2.0/src/io_core/journal.py +56 -0
- ironharness-0.2.0/src/io_core/limits.py +106 -0
- ironharness-0.2.0/src/io_core/mcp_server.py +187 -0
- ironharness-0.2.0/src/io_core/modbus_sim.py +103 -0
- ironharness-0.2.0/src/io_core/modbus_transport.py +87 -0
- ironharness-0.2.0/src/io_core/mqtt_sim.py +245 -0
- ironharness-0.2.0/src/io_core/mqtt_transport.py +175 -0
- ironharness-0.2.0/src/io_core/replay.py +71 -0
- ironharness-0.2.0/src/io_core/serial_transport.py +79 -0
- ironharness-0.2.0/src/io_core/session.py +153 -0
- ironharness-0.2.0/src/io_core/verify.py +51 -0
- ironharness-0.2.0/src/ironbench/__init__.py +3 -0
- ironharness-0.2.0/src/ironbench/agent.py +331 -0
- ironharness-0.2.0/src/ironbench/cli.py +199 -0
- ironharness-0.2.0/src/ironbench/plant.py +310 -0
- ironharness-0.2.0/src/ironbench/report.py +210 -0
- ironharness-0.2.0/src/ironbench/runner.py +1249 -0
- ironharness-0.2.0/src/ironbench/tasks/_firmware/ESP32_GENERIC-20251209-v1.27.0.bin +0 -0
- ironharness-0.2.0/src/ironbench/tasks/_firmware/LITEX_VEXRISCV-micropython-v1.11.0.elf +0 -0
- ironharness-0.2.0/src/ironbench/tasks/adc-read/diagram.json +17 -0
- ironharness-0.2.0/src/ironbench/tasks/adc-read/solution.py +13 -0
- ironharness-0.2.0/src/ironbench/tasks/adc-read/task.yaml +29 -0
- ironharness-0.2.0/src/ironbench/tasks/adc-read/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/blink/diagram.json +18 -0
- ironharness-0.2.0/src/ironbench/tasks/blink/solution.py +16 -0
- ironharness-0.2.0/src/ironbench/tasks/blink/task.yaml +22 -0
- ironharness-0.2.0/src/ironbench/tasks/blink/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/coop-scheduler/solution.py +27 -0
- ironharness-0.2.0/src/ironbench/tasks/coop-scheduler/task.yaml +22 -0
- ironharness-0.2.0/src/ironbench/tasks/debounce/diagram.json +16 -0
- ironharness-0.2.0/src/ironbench/tasks/debounce/solution.py +19 -0
- ironharness-0.2.0/src/ironbench/tasks/debounce/task.yaml +28 -0
- ironharness-0.2.0/src/ironbench/tasks/debounce/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/frame-corrupt/solution.py +25 -0
- ironharness-0.2.0/src/ironbench/tasks/frame-corrupt/task.yaml +56 -0
- ironharness-0.2.0/src/ironbench/tasks/median-filter/diagram.json +17 -0
- ironharness-0.2.0/src/ironbench/tasks/median-filter/solution.py +22 -0
- ironharness-0.2.0/src/ironbench/tasks/median-filter/task.yaml +32 -0
- ironharness-0.2.0/src/ironbench/tasks/median-filter/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/mqtt-device/solution.py +96 -0
- ironharness-0.2.0/src/ironbench/tasks/mqtt-device/task.yaml +39 -0
- ironharness-0.2.0/src/ironbench/tasks/noisy-frames/solution.py +14 -0
- ironharness-0.2.0/src/ironbench/tasks/noisy-frames/task.yaml +48 -0
- ironharness-0.2.0/src/ironbench/tasks/p-regulator/solution.py +7 -0
- ironharness-0.2.0/src/ironbench/tasks/p-regulator/task.yaml +37 -0
- ironharness-0.2.0/src/ironbench/tasks/pid-antiwindup/solution.py +19 -0
- ironharness-0.2.0/src/ironbench/tasks/pid-antiwindup/task.yaml +40 -0
- ironharness-0.2.0/src/ironbench/tasks/protocol/diagram.json +18 -0
- ironharness-0.2.0/src/ironbench/tasks/protocol/solution.py +22 -0
- ironharness-0.2.0/src/ironbench/tasks/protocol/task.yaml +28 -0
- ironharness-0.2.0/src/ironbench/tasks/protocol/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/protocol-retry/diagram.json +13 -0
- ironharness-0.2.0/src/ironbench/tasks/protocol-retry/solution.py +18 -0
- ironharness-0.2.0/src/ironbench/tasks/protocol-retry/task.yaml +29 -0
- ironharness-0.2.0/src/ironbench/tasks/protocol-retry/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/renode-hello/solution.py +5 -0
- ironharness-0.2.0/src/ironbench/tasks/renode-hello/task.yaml +23 -0
- ironharness-0.2.0/src/ironbench/tasks/sensor-poll/diagram.json +17 -0
- ironharness-0.2.0/src/ironbench/tasks/sensor-poll/solution.py +16 -0
- ironharness-0.2.0/src/ironbench/tasks/sensor-poll/task.yaml +20 -0
- ironharness-0.2.0/src/ironbench/tasks/sensor-poll/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/system-id/solution.py +23 -0
- ironharness-0.2.0/src/ironbench/tasks/system-id/task.yaml +41 -0
- ironharness-0.2.0/src/ironbench/tasks/threshold-hysteresis/diagram.json +17 -0
- ironharness-0.2.0/src/ironbench/tasks/threshold-hysteresis/solution.py +21 -0
- ironharness-0.2.0/src/ironbench/tasks/threshold-hysteresis/task.yaml +32 -0
- ironharness-0.2.0/src/ironbench/tasks/threshold-hysteresis/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/traffic-light/diagram.json +13 -0
- ironharness-0.2.0/src/ironbench/tasks/traffic-light/solution.py +13 -0
- ironharness-0.2.0/src/ironbench/tasks/traffic-light/task.yaml +17 -0
- ironharness-0.2.0/src/ironbench/tasks/traffic-light/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/uart-echo/diagram.json +13 -0
- ironharness-0.2.0/src/ironbench/tasks/uart-echo/solution.py +6 -0
- ironharness-0.2.0/src/ironbench/tasks/uart-echo/task.yaml +22 -0
- ironharness-0.2.0/src/ironbench/tasks/uart-echo/wokwi.toml +7 -0
- ironharness-0.2.0/src/ironbench/tasks/uart-menu/solution.py +23 -0
- ironharness-0.2.0/src/ironbench/tasks/uart-menu/task.yaml +45 -0
- ironharness-0.2.0/src/ironbench/tasks/watchdog/solution.py +17 -0
- ironharness-0.2.0/src/ironbench/tasks/watchdog/task.yaml +45 -0
- ironharness-0.2.0/src/ironbench/tasks.py +349 -0
- ironharness-0.2.0/tests/test_esp.py +183 -0
- ironharness-0.2.0/tests/test_faults.py +166 -0
- ironharness-0.2.0/tests/test_file_sandbox.py +88 -0
- ironharness-0.2.0/tests/test_ironbench_agent.py +203 -0
- ironharness-0.2.0/tests/test_ironbench_mqtt.py +134 -0
- ironharness-0.2.0/tests/test_ironbench_plant.py +227 -0
- ironharness-0.2.0/tests/test_ironbench_renode.py +364 -0
- ironharness-0.2.0/tests/test_ironbench_report.py +130 -0
- ironharness-0.2.0/tests/test_ironbench_runner.py +353 -0
- ironharness-0.2.0/tests/test_ironbench_tasks.py +192 -0
- ironharness-0.2.0/tests/test_ironbench_unix.py +305 -0
- ironharness-0.2.0/tests/test_ironbench_wokwi_integration.py +36 -0
- ironharness-0.2.0/tests/test_journal.py +72 -0
- ironharness-0.2.0/tests/test_limits.py +81 -0
- ironharness-0.2.0/tests/test_mcp_server.py +127 -0
- ironharness-0.2.0/tests/test_modbus.py +53 -0
- ironharness-0.2.0/tests/test_mqtt.py +314 -0
- ironharness-0.2.0/tests/test_mqtt_sim.py +152 -0
- ironharness-0.2.0/tests/test_serial.py +61 -0
- ironharness-0.2.0/tests/test_session.py +78 -0
- ironharness-0.2.0/tests/test_smoke.py +12 -0
- ironharness-0.2.0/uv.lock +1177 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ${{ matrix.os }}
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
os: [ubuntu-latest, windows-latest]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- name: Зависимости
|
|
19
|
+
run: uv sync
|
|
20
|
+
- name: Линтер
|
|
21
|
+
run: uv run ruff check .
|
|
22
|
+
- name: Тесты
|
|
23
|
+
run: uv run pytest -q
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
venv/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
|
|
14
|
+
# Локальная документация — не пушится (правило владельца)
|
|
15
|
+
AGENTS.md
|
|
16
|
+
PLAN.md
|
|
17
|
+
|
|
18
|
+
# Секреты
|
|
19
|
+
.env
|
|
20
|
+
|
|
21
|
+
# Прогоны ironbench (serial-логи, журналы)
|
|
22
|
+
.ironbench*/
|
|
23
|
+
|
|
24
|
+
# Локальные временные файлы
|
|
25
|
+
tmp/
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ironharness
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: io-core — безопасный I/O-слой для агентов (MCP); ironbench — бенчмарк для firmware-агентов
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: esptool>=5
|
|
8
|
+
Requires-Dist: mcp>=1.2.0
|
|
9
|
+
Requires-Dist: paho-mqtt>=2.0
|
|
10
|
+
Requires-Dist: pymodbus>=3.7
|
|
11
|
+
Requires-Dist: pyserial>=3.5
|
|
12
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# ironharness
|
|
16
|
+
|
|
17
|
+
> harness — «упряжь»: впрягаем LLM-агентов в железо.
|
|
18
|
+
|
|
19
|
+
Агентский харнесс для I/O и прошивок. Два модуля:
|
|
20
|
+
|
|
21
|
+
- **io-core** — безопасный I/O-слой для агентов: транспорты (serial, Modbus TCP, MQTT,
|
|
22
|
+
файловая песочница), симулятор Modbus, инструменты прошивки ESP32 (esptool: разбор образа
|
|
23
|
+
офлайн, flash/erase на живой плате), JSONL-журнал всех операций, реплеер, лимиты
|
|
24
|
+
(rate-limit, дедлайны), верификация эффектов (`expect_read`), MCP-сервер (19 инструментов).
|
|
25
|
+
- **ironbench** — бенчмарк для firmware-агентов: золотые задачи в симуляторах
|
|
26
|
+
(Wokwi ESP32/MicroPython, далее Renode), агентский цикл поверх LLM API, отчёты pass@k.
|
|
27
|
+
|
|
28
|
+
## Быстрый старт
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv sync # зависимости (+ сам проект editable)
|
|
32
|
+
uv run pytest # тесты (без железа: loop:// и симуляторы)
|
|
33
|
+
uv run ruff check . # линтер
|
|
34
|
+
uv run ironharness-mcp # MCP-сервер (stdio; или: python -m io_core.mcp_server)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Инструменты агента (MCP)
|
|
38
|
+
|
|
39
|
+
`echo` · `serial_open/write/read/read_line` · `modbus_open/read/write` ·
|
|
40
|
+
`mqtt_open/publish/subscribe/read` · `esp_image_info/flash/erase` · `file_write/read/list/delete`
|
|
41
|
+
|
|
42
|
+
Все операции автоматически пишутся в JSONL-журнал (`$IRONHARNESS_HOME/journal.jsonl`,
|
|
43
|
+
по умолчанию `~/.ironharness/`); файловые операции изолированы песочницей
|
|
44
|
+
(`$IRONHARNESS_SANDBOX`, по умолчанию `~/.ironharness/sandbox`).
|
|
45
|
+
|
|
46
|
+
## Подключение внешнего агента
|
|
47
|
+
|
|
48
|
+
Любой MCP-совместимый агент (Claude Code, Codex, Cursor, OpenCode…) получает все
|
|
49
|
+
инструменты io-core одной записью в конфиг — свой цикл агент приносит с собой,
|
|
50
|
+
ironharness даёт «руки»: транспорты, песочницу, журнал, верификацию.
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"ironharness": {
|
|
56
|
+
"command": "uv",
|
|
57
|
+
"args": ["run", "--directory", "/path/to/ironharness", "ironharness-mcp"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
После публикации пакета на PyPI то же самое одной строкой: `"command": "uvx", "args": ["ironharness-mcp"]`.
|
|
64
|
+
|
|
65
|
+
Для испытаний на ненадёжных линиях есть `io_core.faults.FaultyTransport` — сценарные
|
|
66
|
+
сбои (обрыв, задержка, порча и потеря байтов) поверх любого транспорта, и
|
|
67
|
+
`io_core.mqtt_sim.MqttSimBroker` — мини-брокер MQTT (подмножество) для офлайн-прогонов.
|
|
68
|
+
|
|
69
|
+
## ironbench — бенчмарк firmware-агентов
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv run ironbench list # каталог золотых задач
|
|
73
|
+
uv run ironbench run --all # эталонные прогоны (нужен WOKWI_CLI_TOKEN)
|
|
74
|
+
uv run ironbench solve --task blink --attempts 3 # LLM-агент решает задачу
|
|
75
|
+
uv run ironbench report # pass@k: report.json + report.html
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Задачи — ESP32/MicroPython в Wokwi (headless `wokwi-cli`), Renode, MicroPython unix-port
|
|
79
|
+
(бесплатные локальные прогоны) и plant-мишень (замкнутая петля «объект + регулятор» в
|
|
80
|
+
Python, оценка по метрикам переходной характеристики: p-regulator, pid-antiwindup,
|
|
81
|
+
system-id). У каждой задачи класс (io/data/protocol/fsm/control/resilience) и уровень
|
|
82
|
+
1–5; `ironbench report` показывает профиль модели по классам, а не одно число.
|
|
83
|
+
LLM-конфиг — переменные окружения: `LLM_BASE_URL` (по умолчанию локальный LM Studio),
|
|
84
|
+
`LLM_MODEL`, `LLM_API_KEY`, `LLM_TIMEOUT`.
|
|
85
|
+
|
|
86
|
+
Статус: этапы 0–2 завершены (17 золотых задач на 4 мишенях, агентский цикл, таксономия,
|
|
87
|
+
отчёты pass@k + профиль по классам), далее — этап 3 (реальное железо, плата заказана).
|
|
88
|
+
План — `PLAN.md` (локально).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# ironharness
|
|
2
|
+
|
|
3
|
+
> harness — «упряжь»: впрягаем LLM-агентов в железо.
|
|
4
|
+
|
|
5
|
+
Агентский харнесс для I/O и прошивок. Два модуля:
|
|
6
|
+
|
|
7
|
+
- **io-core** — безопасный I/O-слой для агентов: транспорты (serial, Modbus TCP, MQTT,
|
|
8
|
+
файловая песочница), симулятор Modbus, инструменты прошивки ESP32 (esptool: разбор образа
|
|
9
|
+
офлайн, flash/erase на живой плате), JSONL-журнал всех операций, реплеер, лимиты
|
|
10
|
+
(rate-limit, дедлайны), верификация эффектов (`expect_read`), MCP-сервер (19 инструментов).
|
|
11
|
+
- **ironbench** — бенчмарк для firmware-агентов: золотые задачи в симуляторах
|
|
12
|
+
(Wokwi ESP32/MicroPython, далее Renode), агентский цикл поверх LLM API, отчёты pass@k.
|
|
13
|
+
|
|
14
|
+
## Быстрый старт
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv sync # зависимости (+ сам проект editable)
|
|
18
|
+
uv run pytest # тесты (без железа: loop:// и симуляторы)
|
|
19
|
+
uv run ruff check . # линтер
|
|
20
|
+
uv run ironharness-mcp # MCP-сервер (stdio; или: python -m io_core.mcp_server)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Инструменты агента (MCP)
|
|
24
|
+
|
|
25
|
+
`echo` · `serial_open/write/read/read_line` · `modbus_open/read/write` ·
|
|
26
|
+
`mqtt_open/publish/subscribe/read` · `esp_image_info/flash/erase` · `file_write/read/list/delete`
|
|
27
|
+
|
|
28
|
+
Все операции автоматически пишутся в JSONL-журнал (`$IRONHARNESS_HOME/journal.jsonl`,
|
|
29
|
+
по умолчанию `~/.ironharness/`); файловые операции изолированы песочницей
|
|
30
|
+
(`$IRONHARNESS_SANDBOX`, по умолчанию `~/.ironharness/sandbox`).
|
|
31
|
+
|
|
32
|
+
## Подключение внешнего агента
|
|
33
|
+
|
|
34
|
+
Любой MCP-совместимый агент (Claude Code, Codex, Cursor, OpenCode…) получает все
|
|
35
|
+
инструменты io-core одной записью в конфиг — свой цикл агент приносит с собой,
|
|
36
|
+
ironharness даёт «руки»: транспорты, песочницу, журнал, верификацию.
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"ironharness": {
|
|
42
|
+
"command": "uv",
|
|
43
|
+
"args": ["run", "--directory", "/path/to/ironharness", "ironharness-mcp"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
После публикации пакета на PyPI то же самое одной строкой: `"command": "uvx", "args": ["ironharness-mcp"]`.
|
|
50
|
+
|
|
51
|
+
Для испытаний на ненадёжных линиях есть `io_core.faults.FaultyTransport` — сценарные
|
|
52
|
+
сбои (обрыв, задержка, порча и потеря байтов) поверх любого транспорта, и
|
|
53
|
+
`io_core.mqtt_sim.MqttSimBroker` — мини-брокер MQTT (подмножество) для офлайн-прогонов.
|
|
54
|
+
|
|
55
|
+
## ironbench — бенчмарк firmware-агентов
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv run ironbench list # каталог золотых задач
|
|
59
|
+
uv run ironbench run --all # эталонные прогоны (нужен WOKWI_CLI_TOKEN)
|
|
60
|
+
uv run ironbench solve --task blink --attempts 3 # LLM-агент решает задачу
|
|
61
|
+
uv run ironbench report # pass@k: report.json + report.html
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Задачи — ESP32/MicroPython в Wokwi (headless `wokwi-cli`), Renode, MicroPython unix-port
|
|
65
|
+
(бесплатные локальные прогоны) и plant-мишень (замкнутая петля «объект + регулятор» в
|
|
66
|
+
Python, оценка по метрикам переходной характеристики: p-regulator, pid-antiwindup,
|
|
67
|
+
system-id). У каждой задачи класс (io/data/protocol/fsm/control/resilience) и уровень
|
|
68
|
+
1–5; `ironbench report` показывает профиль модели по классам, а не одно число.
|
|
69
|
+
LLM-конфиг — переменные окружения: `LLM_BASE_URL` (по умолчанию локальный LM Studio),
|
|
70
|
+
`LLM_MODEL`, `LLM_API_KEY`, `LLM_TIMEOUT`.
|
|
71
|
+
|
|
72
|
+
Статус: этапы 0–2 завершены (17 золотых задач на 4 мишенях, агентский цикл, таксономия,
|
|
73
|
+
отчёты pass@k + профиль по классам), далее — этап 3 (реальное железо, плата заказана).
|
|
74
|
+
План — `PLAN.md` (локально).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ironharness"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "io-core — безопасный I/O-слой для агентов (MCP); ironbench — бенчмарк для firmware-агентов"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
"esptool>=5",
|
|
10
|
+
"mcp>=1.2.0",
|
|
11
|
+
"paho-mqtt>=2.0",
|
|
12
|
+
"pymodbus>=3.7",
|
|
13
|
+
"pyserial>=3.5",
|
|
14
|
+
"pyyaml>=6.0.3",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.0",
|
|
20
|
+
"ruff>=0.6",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
ironbench = "ironbench.cli:main"
|
|
25
|
+
ironharness-mcp = "io_core.mcp_server:main"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/io_core", "src/ironbench"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
line-length = 100
|
|
39
|
+
src = ["src", "tests"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ru">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>ironbench — отчёт</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: -apple-system, "Segoe UI", sans-serif; margin: 2rem; color: #222; }
|
|
8
|
+
h1 { font-size: 1.3rem; }
|
|
9
|
+
table { border-collapse: collapse; margin-top: 1rem; }
|
|
10
|
+
th, td { border: 1px solid #ccc; padding: 6px 14px; text-align: left; }
|
|
11
|
+
th { background: #f4f4f4; }
|
|
12
|
+
.pass { color: #0a7d24; font-weight: 600; }
|
|
13
|
+
.fail { color: #b3261e; font-weight: 600; }
|
|
14
|
+
footer { margin-top: 1.5rem; color: #777; font-size: 0.85rem; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<h1>ironbench — отчёт solve</h1>
|
|
19
|
+
<table>
|
|
20
|
+
<tr><th>Модель</th><th>Задача</th><th>Попыток</th><th>Решено</th><th>Success rate</th><th>Средних итераций</th><th>Среднее время, с</th></tr>
|
|
21
|
+
<tr><td>qwen3.5-9b</td><td>blink</td><td>3</td><td>3</td><td class="pass">100%</td><td>1.0</td><td>318.4</td></tr>
|
|
22
|
+
<tr><td>qwen3.5-9b</td><td>debounce</td><td>3</td><td>0</td><td class="fail">0%</td><td>2.3</td><td>1052.5</td></tr>
|
|
23
|
+
<tr><td>qwen3.5-9b</td><td>protocol</td><td>3</td><td>0</td><td class="fail">0%</td><td>1.7</td><td>938.5</td></tr>
|
|
24
|
+
<tr><td>qwen3.5-9b</td><td>sensor-poll</td><td>3</td><td>1</td><td class="pass">33%</td><td>2.3</td><td>651.0</td></tr>
|
|
25
|
+
<tr><td>qwen3.5-9b</td><td>traffic-light</td><td>3</td><td>0</td><td class="fail">0%</td><td>3.0</td><td>854.9</td></tr>
|
|
26
|
+
<tr><td>qwen3.5-9b</td><td>uart-echo</td><td>3</td><td>3</td><td class="pass">100%</td><td>1.3</td><td>456.2</td></tr>
|
|
27
|
+
</table>
|
|
28
|
+
<footer>Сгенерировано ironbench · pass@k = доля пар, решённых хотя бы одной из k попыток: 50%</footer>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"campaign": "solve",
|
|
3
|
+
"models": [
|
|
4
|
+
"qwen3.5-9b"
|
|
5
|
+
],
|
|
6
|
+
"tasks": [
|
|
7
|
+
"blink",
|
|
8
|
+
"debounce",
|
|
9
|
+
"protocol",
|
|
10
|
+
"sensor-poll",
|
|
11
|
+
"traffic-light",
|
|
12
|
+
"uart-echo"
|
|
13
|
+
],
|
|
14
|
+
"pass_at_k": 0.5,
|
|
15
|
+
"groups": [
|
|
16
|
+
{
|
|
17
|
+
"model": "qwen3.5-9b",
|
|
18
|
+
"task": "blink",
|
|
19
|
+
"attempts": 3,
|
|
20
|
+
"solved": 3,
|
|
21
|
+
"total_iterations": 3,
|
|
22
|
+
"total_duration": 955.07,
|
|
23
|
+
"success_rate": 1.0,
|
|
24
|
+
"avg_iterations": 1.0,
|
|
25
|
+
"avg_duration": 318.4,
|
|
26
|
+
"passed": true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"model": "qwen3.5-9b",
|
|
30
|
+
"task": "debounce",
|
|
31
|
+
"attempts": 3,
|
|
32
|
+
"solved": 0,
|
|
33
|
+
"total_iterations": 7,
|
|
34
|
+
"total_duration": 3157.5,
|
|
35
|
+
"success_rate": 0.0,
|
|
36
|
+
"avg_iterations": 2.3,
|
|
37
|
+
"avg_duration": 1052.5,
|
|
38
|
+
"passed": false
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"model": "qwen3.5-9b",
|
|
42
|
+
"task": "protocol",
|
|
43
|
+
"attempts": 3,
|
|
44
|
+
"solved": 0,
|
|
45
|
+
"total_iterations": 5,
|
|
46
|
+
"total_duration": 2815.46,
|
|
47
|
+
"success_rate": 0.0,
|
|
48
|
+
"avg_iterations": 1.7,
|
|
49
|
+
"avg_duration": 938.5,
|
|
50
|
+
"passed": false
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"model": "qwen3.5-9b",
|
|
54
|
+
"task": "sensor-poll",
|
|
55
|
+
"attempts": 3,
|
|
56
|
+
"solved": 1,
|
|
57
|
+
"total_iterations": 7,
|
|
58
|
+
"total_duration": 1952.8999999999999,
|
|
59
|
+
"success_rate": 0.33,
|
|
60
|
+
"avg_iterations": 2.3,
|
|
61
|
+
"avg_duration": 651.0,
|
|
62
|
+
"passed": true
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"model": "qwen3.5-9b",
|
|
66
|
+
"task": "traffic-light",
|
|
67
|
+
"attempts": 3,
|
|
68
|
+
"solved": 0,
|
|
69
|
+
"total_iterations": 9,
|
|
70
|
+
"total_duration": 2564.8,
|
|
71
|
+
"success_rate": 0.0,
|
|
72
|
+
"avg_iterations": 3.0,
|
|
73
|
+
"avg_duration": 854.9,
|
|
74
|
+
"passed": false
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"model": "qwen3.5-9b",
|
|
78
|
+
"task": "uart-echo",
|
|
79
|
+
"attempts": 3,
|
|
80
|
+
"solved": 3,
|
|
81
|
+
"total_iterations": 4,
|
|
82
|
+
"total_duration": 1368.48,
|
|
83
|
+
"success_rate": 1.0,
|
|
84
|
+
"avg_iterations": 1.3,
|
|
85
|
+
"avg_duration": 456.2,
|
|
86
|
+
"passed": true
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""io-core: безопасный I/O-слой для агентов (транспорты, журнал, политики, MCP)."""
|
|
2
|
+
|
|
3
|
+
from io_core.errors import (
|
|
4
|
+
OperationTimeout,
|
|
5
|
+
QuotaExceeded,
|
|
6
|
+
RateLimitExceeded,
|
|
7
|
+
SandboxViolation,
|
|
8
|
+
VerificationError,
|
|
9
|
+
)
|
|
10
|
+
from io_core.esp_flash import EspFlasher
|
|
11
|
+
from io_core.file_sandbox import FileSandbox
|
|
12
|
+
from io_core.journal import JsonlJournal, read_events
|
|
13
|
+
from io_core.limits import DeadlineTransport, RateLimitedTransport, RateLimiter
|
|
14
|
+
from io_core.modbus_sim import ModbusSimServer
|
|
15
|
+
from io_core.modbus_transport import ModbusTransport
|
|
16
|
+
from io_core.mqtt_transport import MqttTransport
|
|
17
|
+
from io_core.replay import ReplayMismatch, ReplayTransport
|
|
18
|
+
from io_core.serial_transport import SerialTransport
|
|
19
|
+
from io_core.session import Session
|
|
20
|
+
from io_core.verify import expect_read, write_and_expect
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DeadlineTransport",
|
|
26
|
+
"EspFlasher",
|
|
27
|
+
"FileSandbox",
|
|
28
|
+
"JsonlJournal",
|
|
29
|
+
"ModbusSimServer",
|
|
30
|
+
"ModbusTransport",
|
|
31
|
+
"MqttTransport",
|
|
32
|
+
"OperationTimeout",
|
|
33
|
+
"QuotaExceeded",
|
|
34
|
+
"RateLimitExceeded",
|
|
35
|
+
"RateLimitedTransport",
|
|
36
|
+
"RateLimiter",
|
|
37
|
+
"ReplayMismatch",
|
|
38
|
+
"ReplayTransport",
|
|
39
|
+
"SandboxViolation",
|
|
40
|
+
"SerialTransport",
|
|
41
|
+
"Session",
|
|
42
|
+
"VerificationError",
|
|
43
|
+
"expect_read",
|
|
44
|
+
"read_events",
|
|
45
|
+
"write_and_expect",
|
|
46
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Ошибки политик io-core. Единые имена для всех транспортов и обёрток."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SandboxViolation(Exception):
|
|
7
|
+
"""Попытка обратиться за пределы песочницы."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QuotaExceeded(Exception):
|
|
11
|
+
"""Превышена квота песочницы (байты или число файлов)."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RateLimitExceeded(Exception):
|
|
15
|
+
"""Превышен лимит операций в окне."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OperationTimeout(Exception):
|
|
19
|
+
"""Истёк дедлайн операции/сессии."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VerificationError(Exception):
|
|
23
|
+
"""Проверка эффекта не прошла: ожидание не подтвердилось за отведённое время."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConnectionLost(Exception):
|
|
27
|
+
"""Соединение с устройством потеряно (в том числе по сценарию сбоев)."""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Инструменты прошивки ESP32 через esptool (план 3.3; офлайн-часть этапа 3).
|
|
2
|
+
|
|
3
|
+
image_info() разбирает .bin-образ без железа (проверка агентом перед прошивкой);
|
|
4
|
+
flash()/erase() требуют живую плату — в тестах функции esptool подменяются моками.
|
|
5
|
+
Последовательность flash/erase — каноническая для esptool CLI (см. докстринг
|
|
6
|
+
connect_esp): connect на 115200 → run_stub → change_baud → attach_flash → операция.
|
|
7
|
+
Порт закрывается контекст-менеджером ESPLoader даже при ошибке операции.
|
|
8
|
+
|
|
9
|
+
Внимание: без платы connect_esp делает 7 попыток синхронизации — вызов
|
|
10
|
+
flash/erase блокируется до ~30 секунд, потом ConnectionError.
|
|
11
|
+
|
|
12
|
+
Классический ESP32: один образ (MicroPython) шьётся по адресу 0x1000.
|
|
13
|
+
Пути к .bin не ограничены файловой песочницей (образы лежат вне её корня),
|
|
14
|
+
но каждая операция журналируется с полным путём, включая неудачные попытки.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from esptool.cmds import (
|
|
24
|
+
CHIP_DEFS,
|
|
25
|
+
FLASH_MODES,
|
|
26
|
+
FatalError,
|
|
27
|
+
LoadFirmwareImage,
|
|
28
|
+
attach_flash,
|
|
29
|
+
connect_esp,
|
|
30
|
+
erase_flash,
|
|
31
|
+
run_stub,
|
|
32
|
+
write_flash,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
EventHook = Callable[[str, dict[str, Any]], None]
|
|
36
|
+
|
|
37
|
+
DEFAULT_BOOTLOADER_OFFSET = 0x1000 # классический ESP32
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _reverse_lookup(table: dict, value: int, fallback: str) -> str:
|
|
41
|
+
return next((name for name, v in table.items() if v == value), fallback)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EspFlasher:
|
|
45
|
+
def __init__(self, *, chip: str = "esp32", on_event: EventHook | None = None) -> None:
|
|
46
|
+
self._chip = chip
|
|
47
|
+
self._on_event = on_event
|
|
48
|
+
|
|
49
|
+
def _emit(self, event: str, data: dict[str, Any]) -> None:
|
|
50
|
+
if self._on_event is not None:
|
|
51
|
+
self._on_event(event, data)
|
|
52
|
+
|
|
53
|
+
# --- офлайн: разбор образа без платы ---
|
|
54
|
+
|
|
55
|
+
def image_info(self, firmware_path: str | Path) -> dict[str, Any]:
|
|
56
|
+
path = Path(firmware_path)
|
|
57
|
+
if not path.is_file():
|
|
58
|
+
raise FileNotFoundError(f"образ не найден: {path}")
|
|
59
|
+
img = LoadFirmwareImage(self._chip, str(path))
|
|
60
|
+
segments = [{"addr": hex(s.addr), "size": len(s.data)} for s in img.segments]
|
|
61
|
+
# flash_size_freq: старший ниббл — размер, младший — частота; таблицы — в классе таргета
|
|
62
|
+
size_nibble = (img.flash_size_freq >> 4) & 0xF
|
|
63
|
+
freq_nibble = img.flash_size_freq & 0xF
|
|
64
|
+
target = CHIP_DEFS[self._chip]
|
|
65
|
+
info = {
|
|
66
|
+
"chip": self._chip,
|
|
67
|
+
"path": str(path),
|
|
68
|
+
"size": path.stat().st_size,
|
|
69
|
+
"entrypoint": hex(img.entrypoint),
|
|
70
|
+
"segments": segments,
|
|
71
|
+
"flash_mode": _reverse_lookup(FLASH_MODES, img.flash_mode, str(img.flash_mode)),
|
|
72
|
+
"flash_size": _reverse_lookup(
|
|
73
|
+
{name: (v >> 4) & 0xF for name, v in target.FLASH_SIZES.items()},
|
|
74
|
+
size_nibble,
|
|
75
|
+
hex(size_nibble),
|
|
76
|
+
),
|
|
77
|
+
"flash_freq": _reverse_lookup(
|
|
78
|
+
dict(target.FLASH_FREQUENCY), freq_nibble, hex(freq_nibble)
|
|
79
|
+
),
|
|
80
|
+
}
|
|
81
|
+
self._emit("esp_image_info", info)
|
|
82
|
+
return info
|
|
83
|
+
|
|
84
|
+
# --- живая плата ---
|
|
85
|
+
|
|
86
|
+
def flash(
|
|
87
|
+
self,
|
|
88
|
+
port: str,
|
|
89
|
+
firmware_path: str | Path,
|
|
90
|
+
*,
|
|
91
|
+
addr: int = DEFAULT_BOOTLOADER_OFFSET,
|
|
92
|
+
baud: int = 921600,
|
|
93
|
+
) -> str:
|
|
94
|
+
path = Path(firmware_path)
|
|
95
|
+
if not path.is_file():
|
|
96
|
+
raise FileNotFoundError(f"образ не найден: {path}")
|
|
97
|
+
try:
|
|
98
|
+
with connect_esp(port=port, chip=self._chip) as esp:
|
|
99
|
+
esp = run_stub(esp)
|
|
100
|
+
esp.change_baud(baud)
|
|
101
|
+
attach_flash(esp)
|
|
102
|
+
write_flash(esp, [(addr, str(path))])
|
|
103
|
+
except (OSError, FatalError) as e:
|
|
104
|
+
self._emit(
|
|
105
|
+
"esp_flash_failed",
|
|
106
|
+
{"port": port, "addr": addr, "baud": baud, "path": str(path), "error": str(e)},
|
|
107
|
+
)
|
|
108
|
+
raise ConnectionError(f"esp {port}: не удалось прошить ({e})") from None
|
|
109
|
+
self._emit("esp_flash", {"port": port, "addr": addr, "baud": baud, "path": str(path)})
|
|
110
|
+
return f"ok: {path.name} прошит по 0x{addr:x} ({port})"
|
|
111
|
+
|
|
112
|
+
def erase(self, port: str, *, baud: int = 921600) -> str:
|
|
113
|
+
try:
|
|
114
|
+
with connect_esp(port=port, chip=self._chip) as esp:
|
|
115
|
+
esp = run_stub(esp)
|
|
116
|
+
esp.change_baud(baud)
|
|
117
|
+
attach_flash(esp)
|
|
118
|
+
erase_flash(esp)
|
|
119
|
+
except (OSError, FatalError) as e:
|
|
120
|
+
self._emit("esp_erase_failed", {"port": port, "baud": baud, "error": str(e)})
|
|
121
|
+
raise ConnectionError(f"esp {port}: не удалось стереть ({e})") from None
|
|
122
|
+
self._emit("esp_erase", {"port": port, "baud": baud})
|
|
123
|
+
return f"ok: флеш стёрт ({port})"
|