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.
Files changed (114) hide show
  1. ironharness-0.2.0/.github/workflows/tests.yml +23 -0
  2. ironharness-0.2.0/.gitignore +25 -0
  3. ironharness-0.2.0/PKG-INFO +88 -0
  4. ironharness-0.2.0/README.md +74 -0
  5. ironharness-0.2.0/pyproject.toml +39 -0
  6. ironharness-0.2.0/reports/2026-09-07-qwen3.5-9b.html +30 -0
  7. ironharness-0.2.0/reports/2026-09-07-qwen3.5-9b.json +89 -0
  8. ironharness-0.2.0/src/io_core/__init__.py +46 -0
  9. ironharness-0.2.0/src/io_core/errors.py +27 -0
  10. ironharness-0.2.0/src/io_core/esp_flash.py +123 -0
  11. ironharness-0.2.0/src/io_core/faults.py +130 -0
  12. ironharness-0.2.0/src/io_core/file_sandbox.py +89 -0
  13. ironharness-0.2.0/src/io_core/journal.py +56 -0
  14. ironharness-0.2.0/src/io_core/limits.py +106 -0
  15. ironharness-0.2.0/src/io_core/mcp_server.py +187 -0
  16. ironharness-0.2.0/src/io_core/modbus_sim.py +103 -0
  17. ironharness-0.2.0/src/io_core/modbus_transport.py +87 -0
  18. ironharness-0.2.0/src/io_core/mqtt_sim.py +245 -0
  19. ironharness-0.2.0/src/io_core/mqtt_transport.py +175 -0
  20. ironharness-0.2.0/src/io_core/replay.py +71 -0
  21. ironharness-0.2.0/src/io_core/serial_transport.py +79 -0
  22. ironharness-0.2.0/src/io_core/session.py +153 -0
  23. ironharness-0.2.0/src/io_core/verify.py +51 -0
  24. ironharness-0.2.0/src/ironbench/__init__.py +3 -0
  25. ironharness-0.2.0/src/ironbench/agent.py +331 -0
  26. ironharness-0.2.0/src/ironbench/cli.py +199 -0
  27. ironharness-0.2.0/src/ironbench/plant.py +310 -0
  28. ironharness-0.2.0/src/ironbench/report.py +210 -0
  29. ironharness-0.2.0/src/ironbench/runner.py +1249 -0
  30. ironharness-0.2.0/src/ironbench/tasks/_firmware/ESP32_GENERIC-20251209-v1.27.0.bin +0 -0
  31. ironharness-0.2.0/src/ironbench/tasks/_firmware/LITEX_VEXRISCV-micropython-v1.11.0.elf +0 -0
  32. ironharness-0.2.0/src/ironbench/tasks/adc-read/diagram.json +17 -0
  33. ironharness-0.2.0/src/ironbench/tasks/adc-read/solution.py +13 -0
  34. ironharness-0.2.0/src/ironbench/tasks/adc-read/task.yaml +29 -0
  35. ironharness-0.2.0/src/ironbench/tasks/adc-read/wokwi.toml +7 -0
  36. ironharness-0.2.0/src/ironbench/tasks/blink/diagram.json +18 -0
  37. ironharness-0.2.0/src/ironbench/tasks/blink/solution.py +16 -0
  38. ironharness-0.2.0/src/ironbench/tasks/blink/task.yaml +22 -0
  39. ironharness-0.2.0/src/ironbench/tasks/blink/wokwi.toml +7 -0
  40. ironharness-0.2.0/src/ironbench/tasks/coop-scheduler/solution.py +27 -0
  41. ironharness-0.2.0/src/ironbench/tasks/coop-scheduler/task.yaml +22 -0
  42. ironharness-0.2.0/src/ironbench/tasks/debounce/diagram.json +16 -0
  43. ironharness-0.2.0/src/ironbench/tasks/debounce/solution.py +19 -0
  44. ironharness-0.2.0/src/ironbench/tasks/debounce/task.yaml +28 -0
  45. ironharness-0.2.0/src/ironbench/tasks/debounce/wokwi.toml +7 -0
  46. ironharness-0.2.0/src/ironbench/tasks/frame-corrupt/solution.py +25 -0
  47. ironharness-0.2.0/src/ironbench/tasks/frame-corrupt/task.yaml +56 -0
  48. ironharness-0.2.0/src/ironbench/tasks/median-filter/diagram.json +17 -0
  49. ironharness-0.2.0/src/ironbench/tasks/median-filter/solution.py +22 -0
  50. ironharness-0.2.0/src/ironbench/tasks/median-filter/task.yaml +32 -0
  51. ironharness-0.2.0/src/ironbench/tasks/median-filter/wokwi.toml +7 -0
  52. ironharness-0.2.0/src/ironbench/tasks/mqtt-device/solution.py +96 -0
  53. ironharness-0.2.0/src/ironbench/tasks/mqtt-device/task.yaml +39 -0
  54. ironharness-0.2.0/src/ironbench/tasks/noisy-frames/solution.py +14 -0
  55. ironharness-0.2.0/src/ironbench/tasks/noisy-frames/task.yaml +48 -0
  56. ironharness-0.2.0/src/ironbench/tasks/p-regulator/solution.py +7 -0
  57. ironharness-0.2.0/src/ironbench/tasks/p-regulator/task.yaml +37 -0
  58. ironharness-0.2.0/src/ironbench/tasks/pid-antiwindup/solution.py +19 -0
  59. ironharness-0.2.0/src/ironbench/tasks/pid-antiwindup/task.yaml +40 -0
  60. ironharness-0.2.0/src/ironbench/tasks/protocol/diagram.json +18 -0
  61. ironharness-0.2.0/src/ironbench/tasks/protocol/solution.py +22 -0
  62. ironharness-0.2.0/src/ironbench/tasks/protocol/task.yaml +28 -0
  63. ironharness-0.2.0/src/ironbench/tasks/protocol/wokwi.toml +7 -0
  64. ironharness-0.2.0/src/ironbench/tasks/protocol-retry/diagram.json +13 -0
  65. ironharness-0.2.0/src/ironbench/tasks/protocol-retry/solution.py +18 -0
  66. ironharness-0.2.0/src/ironbench/tasks/protocol-retry/task.yaml +29 -0
  67. ironharness-0.2.0/src/ironbench/tasks/protocol-retry/wokwi.toml +7 -0
  68. ironharness-0.2.0/src/ironbench/tasks/renode-hello/solution.py +5 -0
  69. ironharness-0.2.0/src/ironbench/tasks/renode-hello/task.yaml +23 -0
  70. ironharness-0.2.0/src/ironbench/tasks/sensor-poll/diagram.json +17 -0
  71. ironharness-0.2.0/src/ironbench/tasks/sensor-poll/solution.py +16 -0
  72. ironharness-0.2.0/src/ironbench/tasks/sensor-poll/task.yaml +20 -0
  73. ironharness-0.2.0/src/ironbench/tasks/sensor-poll/wokwi.toml +7 -0
  74. ironharness-0.2.0/src/ironbench/tasks/system-id/solution.py +23 -0
  75. ironharness-0.2.0/src/ironbench/tasks/system-id/task.yaml +41 -0
  76. ironharness-0.2.0/src/ironbench/tasks/threshold-hysteresis/diagram.json +17 -0
  77. ironharness-0.2.0/src/ironbench/tasks/threshold-hysteresis/solution.py +21 -0
  78. ironharness-0.2.0/src/ironbench/tasks/threshold-hysteresis/task.yaml +32 -0
  79. ironharness-0.2.0/src/ironbench/tasks/threshold-hysteresis/wokwi.toml +7 -0
  80. ironharness-0.2.0/src/ironbench/tasks/traffic-light/diagram.json +13 -0
  81. ironharness-0.2.0/src/ironbench/tasks/traffic-light/solution.py +13 -0
  82. ironharness-0.2.0/src/ironbench/tasks/traffic-light/task.yaml +17 -0
  83. ironharness-0.2.0/src/ironbench/tasks/traffic-light/wokwi.toml +7 -0
  84. ironharness-0.2.0/src/ironbench/tasks/uart-echo/diagram.json +13 -0
  85. ironharness-0.2.0/src/ironbench/tasks/uart-echo/solution.py +6 -0
  86. ironharness-0.2.0/src/ironbench/tasks/uart-echo/task.yaml +22 -0
  87. ironharness-0.2.0/src/ironbench/tasks/uart-echo/wokwi.toml +7 -0
  88. ironharness-0.2.0/src/ironbench/tasks/uart-menu/solution.py +23 -0
  89. ironharness-0.2.0/src/ironbench/tasks/uart-menu/task.yaml +45 -0
  90. ironharness-0.2.0/src/ironbench/tasks/watchdog/solution.py +17 -0
  91. ironharness-0.2.0/src/ironbench/tasks/watchdog/task.yaml +45 -0
  92. ironharness-0.2.0/src/ironbench/tasks.py +349 -0
  93. ironharness-0.2.0/tests/test_esp.py +183 -0
  94. ironharness-0.2.0/tests/test_faults.py +166 -0
  95. ironharness-0.2.0/tests/test_file_sandbox.py +88 -0
  96. ironharness-0.2.0/tests/test_ironbench_agent.py +203 -0
  97. ironharness-0.2.0/tests/test_ironbench_mqtt.py +134 -0
  98. ironharness-0.2.0/tests/test_ironbench_plant.py +227 -0
  99. ironharness-0.2.0/tests/test_ironbench_renode.py +364 -0
  100. ironharness-0.2.0/tests/test_ironbench_report.py +130 -0
  101. ironharness-0.2.0/tests/test_ironbench_runner.py +353 -0
  102. ironharness-0.2.0/tests/test_ironbench_tasks.py +192 -0
  103. ironharness-0.2.0/tests/test_ironbench_unix.py +305 -0
  104. ironharness-0.2.0/tests/test_ironbench_wokwi_integration.py +36 -0
  105. ironharness-0.2.0/tests/test_journal.py +72 -0
  106. ironharness-0.2.0/tests/test_limits.py +81 -0
  107. ironharness-0.2.0/tests/test_mcp_server.py +127 -0
  108. ironharness-0.2.0/tests/test_modbus.py +53 -0
  109. ironharness-0.2.0/tests/test_mqtt.py +314 -0
  110. ironharness-0.2.0/tests/test_mqtt_sim.py +152 -0
  111. ironharness-0.2.0/tests/test_serial.py +61 -0
  112. ironharness-0.2.0/tests/test_session.py +78 -0
  113. ironharness-0.2.0/tests/test_smoke.py +12 -0
  114. 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})"