standkit 0.3.7__py3-none-any.whl

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.
standkit/secrets.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Secret-first доступ к секретам стенда (пароли БД, токены агентов и т.п.).
3
+
4
+ Контракт разрешения секрета (по духу совпадает с secretstore из BPMkit, но без
5
+ зависимости от него — ядро standkit самодостаточно):
6
+
7
+ 1. Переменная окружения ``STANDKIT_SECRET__<REF_UPPER>`` (двойное подчёркивание
8
+ как разделитель, символы, недопустимые в имени переменной окружения,
9
+ заменяются на "_");
10
+ 2. системный keyring (опциональная зависимость — импортируется в try/except,
11
+ ядро НЕ требует пакет ``keyring`` для работы);
12
+ 3. явный фолбэк, переданный вызывающей стороной (например, открытое поле
13
+ ``db_password`` из записи реестра — сознательно менее приоритетно, чем
14
+ секрет);
15
+ 4. если ничего не найдено — SecretError.
16
+
17
+ Секрет никогда не логируется и не попадает в текстовое представление ошибки
18
+ целиком (в сообщении об ошибке фигурирует только ref, не значение).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import re
25
+ from typing import Optional
26
+
27
+ try:
28
+ import keyring # type: ignore
29
+
30
+ _HAS_KEYRING = True
31
+ except ImportError: # keyring — опциональная зависимость
32
+ keyring = None # type: ignore
33
+ _HAS_KEYRING = False
34
+
35
+ _KEYRING_SERVICE = "standkit"
36
+ _ENV_PREFIX = "STANDKIT_SECRET__"
37
+
38
+
39
+ class SecretError(Exception):
40
+ """Секрет не найден ни в одном из источников."""
41
+
42
+
43
+ def _env_var_name(ref: str) -> str:
44
+ safe = re.sub(r"[^A-Za-z0-9]", "_", ref).upper()
45
+ return f"{_ENV_PREFIX}{safe}"
46
+
47
+
48
+ def has_secret(ref: str) -> bool:
49
+ """Быстрая проверка наличия секрета (без исключений) — env либо keyring."""
50
+ if os.environ.get(_env_var_name(ref)):
51
+ return True
52
+ if _HAS_KEYRING:
53
+ try:
54
+ return keyring.get_password(_KEYRING_SERVICE, ref) is not None
55
+ except Exception:
56
+ # Бэкенд keyring недоступен на этой машине (нет DBus/Credential
57
+ # Manager и т.п.) — не считаем это фатальной ошибкой на этапе has_secret.
58
+ return False
59
+ return False
60
+
61
+
62
+ def set_secret(ref: str, value: str) -> None:
63
+ """
64
+ Сохраняет значение секрета под ссылкой ``ref`` в системном keyring.
65
+
66
+ Симметрично ``get_secret``/``has_secret`` — тот же backend (keyring,
67
+ сервис ``standkit``). Требует опциональную зависимость ``keyring``
68
+ (extra ``standkit[secrets]``/``standkit[gui]``); при её отсутствии или
69
+ сбое backend'а бросает понятную ``SecretError`` (без падения импорта
70
+ модуля — см. try/except вокруг ``import keyring`` выше).
71
+
72
+ Значение секрета никогда не логируется и не попадает в текст ошибки.
73
+ """
74
+ if not _HAS_KEYRING:
75
+ raise SecretError(
76
+ f"Невозможно задать секрет '{ref}': пакет keyring не установлен. "
77
+ "Установите опциональную зависимость: pip install standkit[secrets]"
78
+ )
79
+ try:
80
+ keyring.set_password(_KEYRING_SERVICE, ref, value)
81
+ except Exception as exc:
82
+ # Значение секрета намеренно не попадает в текст исключения — только ref.
83
+ raise SecretError(f"Не удалось сохранить секрет '{ref}' в keyring: {exc}") from exc
84
+
85
+
86
+ def delete_secret(ref: str) -> None:
87
+ """
88
+ Удаляет секрет по ссылке ``ref`` из системного keyring.
89
+
90
+ Идемпотентно по духу с остальным контрактом: отсутствие backend'а —
91
+ SecretError; отсутствие самого секрета в keyring backend обычно тоже
92
+ трактует как ошибку (PasswordDeleteError) — она также оборачивается в
93
+ SecretError, вызывающая сторона может считать "секрета и так не было"
94
+ нормальным исходом при необходимости (проверить через has_secret до
95
+ удаления).
96
+ """
97
+ if not _HAS_KEYRING:
98
+ raise SecretError(
99
+ f"Невозможно удалить секрет '{ref}': пакет keyring не установлен. "
100
+ "Установите опциональную зависимость: pip install standkit[secrets]"
101
+ )
102
+ try:
103
+ keyring.delete_password(_KEYRING_SERVICE, ref)
104
+ except Exception as exc:
105
+ raise SecretError(f"Не удалось удалить секрет '{ref}' из keyring: {exc}") from exc
106
+
107
+
108
+ def get_secret(ref: str, *, fallback: Optional[str] = None) -> str:
109
+ """
110
+ Возвращает значение секрета по ссылке ``ref`` согласно Secret-first контракту.
111
+
112
+ ``fallback`` — необязательное значение "последней надежды" (например,
113
+ открытое поле реестра) — используется только если ни env, ни keyring не
114
+ дали ответа. Если и фолбэка нет — SecretError.
115
+ """
116
+ env_val = os.environ.get(_env_var_name(ref))
117
+ if env_val:
118
+ return env_val
119
+
120
+ if _HAS_KEYRING:
121
+ try:
122
+ val = keyring.get_password(_KEYRING_SERVICE, ref)
123
+ except Exception:
124
+ val = None
125
+ if val:
126
+ return val
127
+
128
+ if fallback:
129
+ return fallback
130
+
131
+ raise SecretError(
132
+ f"Секрет '{ref}' не найден ни в переменной окружения {_env_var_name(ref)}, "
133
+ f"ни в keyring, ни в переданном fallback"
134
+ )
135
+
136
+
137
+ # --- TODO(следующая итерация) ---
138
+ # - CLI-обёртка (set/get/status/rotate/delete/list) по аналогии с
139
+ # BPMkit/server/secretstore.py, но для сервиса "standkit" (set_secret/
140
+ # delete_secret уже есть как программное API — CLI поверх них тривиален);
141
+ # - файловый фолбэк secrets.enc с мастер-ключом из переменной окружения — для
142
+ # машин без доступного системного keyring (headless Linux без dbus/libsecret);
143
+ # - явный `status()` с диагностикой источника (env/keyring/fallback/not found)
144
+ # для онбординга новой машины без раскрытия самого значения.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: standkit
3
+ Version: 0.3.7
4
+ Summary: Свободное ядро (MIT) для управления жизненным циклом локальных и удалённых стендов BPMSoft: headless start/stop/restart, health-пробы, логи, реестр стендов. Часть экосистемы BPMkit.
5
+ Author: standkit contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/thinkquattro/BPMkitStand
8
+ Project-URL: Экосистема BPMkit, https://github.com/thinkquattro/BPMkit-dev
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Topic :: System :: Systems Administration
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: secrets
19
+ Requires-Dist: keyring>=24; extra == "secrets"
20
+ Provides-Extra: desktop
21
+ Requires-Dist: pywebview>=5; extra == "desktop"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ <div align="center">
27
+
28
+ <img src="https://raw.githubusercontent.com/thinkquattro/BPMkitStand/main/standkit_hub/web/bpmkit-logo.svg" alt="BPMkit" width="300"/>
29
+
30
+ # BPMkitStand
31
+
32
+ **Свободный диспетчер стендов BPMSoft.**
33
+ Локальный веб-дашборд для запуска, остановки и мониторинга ваших стендов — в один клик, без консоли.
34
+
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-f9763d.svg)](LICENSE)
36
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/)
37
+ [![Platform](https://img.shields.io/badge/OS-Windows%20%7C%20Linux-lightgrey.svg)]()
38
+
39
+ [Сайт](https://bpmkit.pro) · [Telegram](https://t.me/quattrolife) · [Companion-версия](https://bpmkit.pro)
40
+
41
+ </div>
42
+
43
+ ---
44
+
45
+ ## Что это
46
+
47
+ **BPMkitStand** — бесплатный инструмент экосистемы [BPMkit](https://bpmkit.pro) для управления
48
+ локальными и удалёнными стендами BPMSoft. Открывает лёгкий локальный веб-дашборд в браузере:
49
+ видно состояние каждого стенда, его можно запустить/остановить/перезапустить, посмотреть логи
50
+ текущей сессии и очистить Redis — всё без ручных команд в PowerShell.
51
+
52
+ Ядро (`standkit`) написано на чистой стандартной библиотеке Python — без тяжёлых зависимостей,
53
+ работает на Windows и Linux.
54
+
55
+ ## Возможности
56
+
57
+ - **Дашборд стендов** — список из общего реестра BPMkit (`projects.json`), состояние в реальном времени.
58
+ - **Жизненный цикл** — старт / стоп / рестарт стенда с честной обратной связью (спиннер до готовности, без ложных «не поднялся» на прогреве).
59
+ - **Логи** — просмотр логов текущей сессии стенда, открытие папки логов стенда и папки BPMkit-проекта.
60
+ - **Redis** — очистка кэша стенда (номер БД берётся из конфигурации стенда).
61
+ - **Тёмная и светлая тема** с переключателем.
62
+ - **Безопасность по умолчанию** — дашборд слушает только `127.0.0.1`, сессионный токен, защита мутаций (CSRF + проверка Origin).
63
+ - **Удалённые стенды** *(в развитии)* — через лёгкие кроссплатформенные headless-агенты с TLS/mTLS.
64
+
65
+ ## Скриншоты
66
+
67
+ | Дашборд (светлая тема) | Дашборд (тёмная тема) |
68
+ |---|---|
69
+ | ![Дашборд, светлая тема](https://raw.githubusercontent.com/thinkquattro/BPMkitStand/main/docs/img/dashboard-light.png) | ![Дашборд, тёмная тема](https://raw.githubusercontent.com/thinkquattro/BPMkitStand/main/docs/img/dashboard-dark.png) |
70
+
71
+ <div align="center">
72
+ <img src="https://raw.githubusercontent.com/thinkquattro/BPMkitStand/main/docs/img/about.png" alt="Модальное окно «О программе»" width="360"/>
73
+ </div>
74
+
75
+ ## Как это работает
76
+
77
+ BPMkitStand состоит из **ядра и двух оболочек**:
78
+
79
+ - **`standkit`** — ядро (MIT) на чистой стандартной библиотеке Python: движок жизненного цикла
80
+ над реестром `projects.json` (старт/стоп/рестарт процесса стенда), health-пробы (процесс / HTTP
81
+ / порт БД / порт Redis), tail лога, Secret-first доступ к секретам. Без веб-слоя и сетевых
82
+ зависимостей — можно использовать и как библиотеку/CLI.
83
+ - **`standkit_hub`** — локальный веб-дашборд (то, что видно на скриншотах). Сам себя отдаёт по
84
+ HTTP через `stdlib http.server` (vanilla JS/CSS, без CDN и сборки) и открывается в системном
85
+ браузере; опционально — в нативном окне (`standkit[desktop]`, `--desktop`). Устанавливается
86
+ только на машину оператора.
87
+ - **`standkit_agent`** — лёгкий headless-демон на хосте удалённого стенда (см. ниже).
88
+
89
+ Дашборд — **федеративный клиент**: он собирает в один список и локальные стенды (управляет ими
90
+ напрямую через ядро), и удалённые (ходит к их агентам по HTTP). В таблице колонка «Транспорт»
91
+ показывает, как дашборд дотягивается до стенда: `local` или `agent`.
92
+
93
+ Реестр стендов — **единый с MCP BPMkit**: один `projects.json` (стенды под ключом `projects`),
94
+ путь резолвится через `BPMSOFT_PROJECTS_FILE` → `%APPDATA%\BPMkit\projects.json`
95
+ (`~/.config/BPMkit/...` на Linux) → `./projects.json`. Секреты (пароли БД, токены агентов) в
96
+ реестре не хранятся — только ссылки на них (Secret-first).
97
+
98
+ Запуск стенда честный: дашборд поднимает `dotnet <stand_dll>` и держит спиннер до реального
99
+ ответа web-хоста по HTTP, а не рапортует «запущено» по факту создания процесса.
100
+
101
+ ## Удалённые стенды
102
+
103
+ Стенды на других хостах (виртуалки, серверы, контуры заказчика) управляются через **федерацию
104
+ лёгких кроссплатформенных агентов** (`standkit_agent`, Windows/Linux, только stdlib). На хосте
105
+ стенда поднимается агент, дашборд оператора ходит к нему по HTTPS с Bearer-токеном; стенд
106
+ объявляется удалённым одним полем `transport: "agent"` в реестре. Агент — RCE-поверхность по
107
+ дизайну, поэтому защищён secure-defaults: loopback по умолчанию, fail-closed на non-loopback без
108
+ TLS, TLS 1.2+/mTLS, скоупы control/readonly, lockout по IP, аудит.
109
+
110
+ **Полное описание, установка агента, TLS/mTLS, служба и траблшутинг — в
111
+ [docs/REMOTE_STANDS.md](docs/REMOTE_STANDS.md).**
112
+
113
+ ## Установка
114
+
115
+ Нужен Python 3.10+.
116
+
117
+ ```bash
118
+ pip install "git+https://github.com/thinkquattro/BPMkitStand.git"
119
+ ```
120
+
121
+ Запуск дашборда:
122
+
123
+ ```bash
124
+ standkit-hub
125
+ ```
126
+
127
+ Откроется браузер с локальным дашбордом. Реестр стендов берётся из
128
+ `%APPDATA%\BPMkit\projects.json` (или из переменной окружения `BPMSOFT_PROJECTS_FILE`).
129
+
130
+ ## Реестр стендов
131
+
132
+ BPMkitStand использует тот же реестр, что и MCP BPMkit — единый `projects.json`.
133
+ Образец формата — [`projects.sample.json`](projects.sample.json).
134
+
135
+ ## Безопасность
136
+
137
+ Дашборд и агент проектировались с расчётом на прод-контур. Модель угроз, харденинг и чек-лист —
138
+ в [SECURITY.md](SECURITY.md). Кратко: fail-closed bind на loopback, TLS 1.2+/mTLS для агента,
139
+ скоупы токена, per-IP lockout, аудит без утечки секретов.
140
+
141
+ ## BPMkitStand и Companion
142
+
143
+ Бесплатная версия — полноценный диспетчер стендов. **Companion-версия** дополнительно даёт
144
+ автообновление MCP BPMkit и контроль лицензии; поставляется в составе установщика MCP-клиента.
145
+ Подробнее — на [bpmkit.pro](https://bpmkit.pro).
146
+
147
+ ## Лицензия
148
+
149
+ [MIT](LICENSE) © Владимир Терновский
150
+
151
+ ---
152
+
153
+ <div align="center">
154
+ <sub>Часть экосистемы <a href="https://bpmkit.pro">BPMkit</a> — AI-ассистента для разработки на BPMSoft.</sub>
155
+ </div>
@@ -0,0 +1,38 @@
1
+ standkit/__init__.py,sha256=zmlBZ6BtkoH1mvInZBnAWFWXiNhRXlbY0xWAIThVELM,515
2
+ standkit/health.py,sha256=rjfluQrks6WdN764oZxmFze_pF4X9jZ9QN6zBD57Qj8,7351
3
+ standkit/lifecycle.py,sha256=notNgz3W_aele4Jt2cDQNSQO_LhS3f2NTadsI6WxxT0,9525
4
+ standkit/logs.py,sha256=eGTjlZtX_6HId_E9-IJfeS7Eoy_Kkg0NnINnfm0McfQ,12458
5
+ standkit/models.py,sha256=sUOwkVKWyERvQVG6qjIWrkVfdH-XBEXoQAtVP7EXkKk,8895
6
+ standkit/platform.py,sha256=ER-0YJ1Q0ydpHTQAuE2Brh-hX3tfVNYwQEtC1F1MMU4,7598
7
+ standkit/registry.py,sha256=97goPJQv0DR3pQqyngeIEDXVR257IoHItpgvEOxrRcE,11043
8
+ standkit/secrets.py,sha256=80nEVTEli7Qb4N1WLu6ErfJCLFcwoCfFRyNfu31vd3E,7267
9
+ standkit-0.3.7.dist-info/licenses/LICENSE,sha256=rszyODf-5JGU50GROQX7tP2ULMpvxVveFnvI5kuK1XE,1078
10
+ standkit_agent/__init__.py,sha256=yWsYdZ1i-3BKYmnhnW2Yxs2STd1006yxwE3e267kN3s,569
11
+ standkit_agent/__main__.py,sha256=Xdc_PXsWFmTJKWzlD0FgwFPTxMa-Pv2b2tY7ozNN4TY,8922
12
+ standkit_agent/audit.py,sha256=Xa7FmarL537AOrd8N6GmvYjtoAP7tgpO8wiFOlKrr8k,3716
13
+ standkit_agent/security.py,sha256=QqcDT9_BrX2B-QwNu2jgxRBmN3mRicxl0gCClAR4I64,14545
14
+ standkit_agent/server.py,sha256=KjttdAZ8dLxjLC921DnV_ty-KItoa-xugZWE12eHzJY,18478
15
+ standkit_hub/__init__.py,sha256=4aE2g17dRVRnlBzY2CImMakVr5hmnIqxuOexdXoBaK8,830
16
+ standkit_hub/__main__.py,sha256=zH1IFVTNiwAlDW_uDmVbE2G1mCei4fGt3az6iG2XkFs,5475
17
+ standkit_hub/agent_control.py,sha256=3l-iQROAAlBmUioID0WruqNBlN2JPZhs5m-JFYI6kt0,9193
18
+ standkit_hub/client.py,sha256=zXKMc_PA0oidfLY76cYB5dpJx0ZW-4ifPqFJVxb_moE,7860
19
+ standkit_hub/config.py,sha256=RrPzHlCZuxjV6RbPxj3pgWGR79zMtwZNvPW9DEWecwQ,7016
20
+ standkit_hub/logs_browser.py,sha256=VMUtJ15sUnMPG5B5_O857HIHuUpyakgFnGIuU_1nnpM,9078
21
+ standkit_hub/redis_min.py,sha256=l537Y7oOoRp7KJdGtjOBCggKNq5Lgd4-o0ekMUOzPEc,15152
22
+ standkit_hub/security.py,sha256=NnRxXFobFsbI5GnpFGPzzVDSOVooNkwphBmy8y6SPlQ,6934
23
+ standkit_hub/server.py,sha256=hYuo_r2GNuwP6cXFQCWJ2OuJ7ovtk2PkEAvriNTvgtY,42706
24
+ standkit_hub/shortcut.py,sha256=FlBcgDC4pm0Hv6ixfI_NIHG9RcIT6Rus1jPhHXy37mM,10649
25
+ standkit_hub/assets/bpmkit-icon.ico,sha256=3d6Ymsk29tGp2k3ODf2jpbW1BXGsjcG3rLBqLSLRsc4,19265
26
+ standkit_hub/assets/icon.png,sha256=0qGNU9UoRSyEuGJG-xeZh19zfr8DiV_XsEjYgN5vjqU,16386
27
+ standkit_hub/web/app.js,sha256=PwwJDh-gvaFUVNPyW4JJQCvgD9h2gQ3UXRJzi3clrzc,34842
28
+ standkit_hub/web/bpmkit-logo-dark.svg,sha256=0DRtZU3jdro5PvndG14w02hqVxxu8invnBKccueqZ4w,530
29
+ standkit_hub/web/bpmkit-logo.svg,sha256=ukeUGKk6jEtJVyeKfMh1Ti_qBU76TdNB16GzVpYtMO8,491
30
+ standkit_hub/web/favicon.png,sha256=0qGNU9UoRSyEuGJG-xeZh19zfr8DiV_XsEjYgN5vjqU,16386
31
+ standkit_hub/web/favicon.svg,sha256=hq4Y_4MPPd6vvAgHWZrk3-MRwMcy3zdGYjrBYh5p2dE,314
32
+ standkit_hub/web/index.html,sha256=ERtm6fpuPGLYUcbSs70BDhX57QX9N-3pOffldBt4U14,14866
33
+ standkit_hub/web/style.css,sha256=ux48bEaryiX6ytuSwua9vCtgLVkwYlRkglH7hy8EyNE,17247
34
+ standkit-0.3.7.dist-info/METADATA,sha256=TkK7LsQ8tBvE2uR0GAowdXxpNXZz1O5GVMbNJ_u66Zk,9840
35
+ standkit-0.3.7.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
36
+ standkit-0.3.7.dist-info/entry_points.txt,sha256=8ROYOwvYAzY-od3CEidY_e-RcZfI7_6pTA-kUxpvgDs,148
37
+ standkit-0.3.7.dist-info/top_level.txt,sha256=nuG5K9GO1qU22M369LXDcB6u10IiuiNo5rWzPrQD9AU,37
38
+ standkit-0.3.7.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ standkit-agent = standkit_agent.__main__:main
3
+ standkit-gui = standkit_hub.__main__:main
4
+ standkit-hub = standkit_hub.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 standkit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ standkit
2
+ standkit_agent
3
+ standkit_hub
@@ -0,0 +1,9 @@
1
+ """
2
+ standkit_agent — лёгкий headless-демон на хосте стенда: ядро standkit +
3
+ крошечный HTTP/RPC сервер (stdlib-only: http.server, subprocess, socket,
4
+ urllib). Кроссплатформенный (Windows/Linux). Без Qt и без сторонних
5
+ зависимостей — специально, чтобы агент можно было развернуть на "голом"
6
+ хосте стенда без сборки колёс под конкретную ОС/архитектуру.
7
+ """
8
+
9
+ __all__: list[str] = []
@@ -0,0 +1,192 @@
1
+ """
2
+ Точка входа агента: ``python -m standkit_agent`` (или консольный скрипт
3
+ ``standkit-agent`` после установки пакета).
4
+
5
+ БЕЗОПАСНОСТЬ (см. также standkit_agent/security.py, README.md → раздел
6
+ "Безопасность"): агент управляет жизненным циклом процессов на хосте стенда
7
+ (start/stop/restart) — это RCE-поверхность по дизайну. Secure-defaults:
8
+ - ``--host`` по умолчанию ``127.0.0.1`` (НЕ 0.0.0.0);
9
+ - non-loopback host без TLS ОТКАЗЫВАЕТСЯ стартовать (fail-closed),
10
+ если явно не передан ``--insecure`` (только dev/тест, НЕ прод);
11
+ - Bearer-токен сравнивается через hmac.compare_digest (защита от
12
+ timing-атак);
13
+ - lockout по source-IP после серии неудачных аутентификаций;
14
+ - append-only JSON-lines аудит-лог всех запросов (без токенов).
15
+
16
+ Примеры:
17
+ # dev, loopback, без TLS (secure default при отсутствии удалённого доступа)
18
+ python -m standkit_agent --registry ./projects.json \\
19
+ --token-ref standkit:my-stand:agent-token
20
+
21
+ # прод, удалённый доступ, TLS + mTLS
22
+ python -m standkit_agent --host 0.0.0.0 --port 8765 \\
23
+ --registry /opt/standkit/projects.json \\
24
+ --token-ref standkit:my-stand:agent-token \\
25
+ --readonly-token-ref standkit:my-stand:agent-readonly-token \\
26
+ --tls-cert /etc/standkit/agent.crt --tls-key /etc/standkit/agent.key \\
27
+ --tls-client-ca /etc/standkit/clients-ca.crt
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ from standkit.registry import Registry, default_registry_path
37
+ from standkit.secrets import SecretError, get_secret
38
+ from standkit_agent.security import (
39
+ Authenticator,
40
+ InsecureBindError,
41
+ LockoutTracker,
42
+ DEFAULT_LOCKOUT_MAX_FAILURES,
43
+ DEFAULT_LOCKOUT_WINDOW_SECONDS,
44
+ validate_bind_security,
45
+ )
46
+ from standkit_agent.server import run_server
47
+
48
+
49
+ def _resolve_token(token_ref: str, *, label: str) -> str:
50
+ try:
51
+ return get_secret(token_ref)
52
+ except SecretError as exc:
53
+ print(f"[standkit-agent] ОШИБКА: не удалось получить секрет {label} ({token_ref!r}): {exc}", file=sys.stderr)
54
+ sys.exit(1)
55
+
56
+
57
+ def main(argv: list[str] | None = None) -> int:
58
+ parser = argparse.ArgumentParser(
59
+ prog="standkit-agent",
60
+ description=(
61
+ "Headless-агент standkit для хоста стенда. RCE-поверхность по дизайну "
62
+ "(start/stop/restart процессов) — secure-defaults: loopback-only без TLS, "
63
+ "fail-closed на non-loopback без TLS. См. README.md → раздел «Безопасность»."
64
+ ),
65
+ )
66
+ parser.add_argument(
67
+ "--host",
68
+ default="127.0.0.1",
69
+ help="адрес, на котором слушать (по умолчанию 127.0.0.1 — loopback-only, secure default)",
70
+ )
71
+ parser.add_argument("--port", type=int, default=8765, help="порт (по умолчанию 8765)")
72
+ parser.add_argument(
73
+ "--registry",
74
+ default=None,
75
+ help=(
76
+ "путь к реестру стендов, которыми управляет этот агент (по умолчанию — "
77
+ "тот же реестр, что резолвит BPMkit MCP: env BPMSOFT_PROJECTS_FILE, "
78
+ "иначе %%APPDATA%%\\BPMkit\\projects.json / ~/.config/BPMkit/projects.json, "
79
+ "иначе ./projects.json; см. standkit.registry.default_registry_path)"
80
+ ),
81
+ )
82
+ parser.add_argument(
83
+ "--token-ref",
84
+ required=True,
85
+ help="ссылка на секрет control-токена (start/stop/restart + read), Secret-first (см. standkit.secrets)",
86
+ )
87
+ parser.add_argument(
88
+ "--readonly-token-ref",
89
+ default=None,
90
+ help="ссылка на секрет readonly-токена (только GET /stands, /status, /logs) — опционально",
91
+ )
92
+ parser.add_argument("--run-dir", default=None, help="каталог pid-файлов (по умолчанию ~/.standkit/run)")
93
+ parser.add_argument("--log-dir", default=None, help="каталог лог-файлов (по умолчанию ~/.standkit/logs)")
94
+ parser.add_argument(
95
+ "--audit-log",
96
+ default=None,
97
+ help="путь к JSON-lines аудит-логу (по умолчанию ~/.standkit/audit.log)",
98
+ )
99
+ parser.add_argument(
100
+ "--tls-cert",
101
+ default=None,
102
+ help="путь к серверному TLS-сертификату (PEM). Требуется вместе с --tls-key для включения TLS",
103
+ )
104
+ parser.add_argument(
105
+ "--tls-key",
106
+ default=None,
107
+ help="путь к приватному ключу серверного TLS-сертификата (PEM)",
108
+ )
109
+ parser.add_argument(
110
+ "--tls-client-ca",
111
+ default=None,
112
+ help="путь к CA (PEM) для проверки клиентских сертификатов — включает mTLS "
113
+ "(CERT_REQUIRED); без этого флага TLS работает без проверки клиента",
114
+ )
115
+ parser.add_argument(
116
+ "--insecure",
117
+ action="store_true",
118
+ help="ОСОЗНАННЫЙ обход fail-closed проверки: разрешить открытый HTTP на "
119
+ "non-loopback адресе без TLS. ТОЛЬКО для dev/тестовых сценариев за "
120
+ "изолированным периметром — НЕ для прода/недоверенной сети. Выводит "
121
+ "громкое предупреждение в stderr",
122
+ )
123
+ parser.add_argument(
124
+ "--lockout-max-failures",
125
+ type=int,
126
+ default=DEFAULT_LOCKOUT_MAX_FAILURES,
127
+ help=f"порог неудачных аутентификаций с одного IP до блокировки (по умолчанию {DEFAULT_LOCKOUT_MAX_FAILURES})",
128
+ )
129
+ parser.add_argument(
130
+ "--lockout-window",
131
+ type=float,
132
+ default=DEFAULT_LOCKOUT_WINDOW_SECONDS,
133
+ help=f"окно (сек) для подсчёта неудачных аутентификаций (по умолчанию {DEFAULT_LOCKOUT_WINDOW_SECONDS:.0f})",
134
+ )
135
+ args = parser.parse_args(argv)
136
+
137
+ registry_path = Path(args.registry) if args.registry else default_registry_path()
138
+ registry = Registry.load(registry_path)
139
+
140
+ control_token = _resolve_token(args.token_ref, label="control-токена агента")
141
+ readonly_token = (
142
+ _resolve_token(args.readonly_token_ref, label="readonly-токена агента")
143
+ if args.readonly_token_ref
144
+ else None
145
+ )
146
+ authenticator = Authenticator(control_token, readonly_token)
147
+ lockout = LockoutTracker(max_failures=args.lockout_max_failures, window_seconds=args.lockout_window)
148
+
149
+ run_dir = Path(args.run_dir) if args.run_dir else None
150
+ log_dir = Path(args.log_dir) if args.log_dir else None
151
+ audit_log_path = Path(args.audit_log) if args.audit_log else None
152
+
153
+ tls_enabled = bool(args.tls_cert and args.tls_key)
154
+
155
+ # Fail-closed bind-проверка ДО любого вывода "слушаю ..." — если конфигурация
156
+ # небезопасна, агент не должен даже создавать впечатление, что он стартовал.
157
+ try:
158
+ validate_bind_security(args.host, tls_enabled=tls_enabled, insecure=args.insecure)
159
+ except InsecureBindError as exc:
160
+ print(f"[standkit-agent] {exc}", file=sys.stderr)
161
+ return 1
162
+
163
+ print(
164
+ f"[standkit-agent] слушаю {args.host}:{args.port} "
165
+ f"(tls={'on' if tls_enabled else 'off'}"
166
+ f"{'+mtls' if tls_enabled and args.tls_client_ca else ''}), "
167
+ f"реестр={registry_path}, стендов={len(registry)}, readonly-токен={'да' if readonly_token else 'нет'}"
168
+ )
169
+
170
+ try:
171
+ run_server(
172
+ registry,
173
+ authenticator,
174
+ host=args.host,
175
+ port=args.port,
176
+ run_dir=run_dir,
177
+ log_dir=log_dir,
178
+ tls_cert=args.tls_cert,
179
+ tls_key=args.tls_key,
180
+ tls_client_ca=args.tls_client_ca,
181
+ insecure=args.insecure,
182
+ lockout=lockout,
183
+ audit_log_path=audit_log_path,
184
+ )
185
+ except InsecureBindError as exc:
186
+ print(f"[standkit-agent] {exc}", file=sys.stderr)
187
+ return 1
188
+ return 0
189
+
190
+
191
+ if __name__ == "__main__":
192
+ raise SystemExit(main())