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/__init__.py +11 -0
- standkit/health.py +163 -0
- standkit/lifecycle.py +191 -0
- standkit/logs.py +221 -0
- standkit/models.py +211 -0
- standkit/platform.py +174 -0
- standkit/registry.py +223 -0
- standkit/secrets.py +144 -0
- standkit-0.3.7.dist-info/METADATA +155 -0
- standkit-0.3.7.dist-info/RECORD +38 -0
- standkit-0.3.7.dist-info/WHEEL +5 -0
- standkit-0.3.7.dist-info/entry_points.txt +4 -0
- standkit-0.3.7.dist-info/licenses/LICENSE +21 -0
- standkit-0.3.7.dist-info/top_level.txt +3 -0
- standkit_agent/__init__.py +9 -0
- standkit_agent/__main__.py +192 -0
- standkit_agent/audit.py +93 -0
- standkit_agent/security.py +300 -0
- standkit_agent/server.py +386 -0
- standkit_hub/__init__.py +13 -0
- standkit_hub/__main__.py +131 -0
- standkit_hub/agent_control.py +216 -0
- standkit_hub/assets/bpmkit-icon.ico +0 -0
- standkit_hub/assets/icon.png +0 -0
- standkit_hub/client.py +159 -0
- standkit_hub/config.py +156 -0
- standkit_hub/logs_browser.py +174 -0
- standkit_hub/redis_min.py +326 -0
- standkit_hub/security.py +146 -0
- standkit_hub/server.py +881 -0
- standkit_hub/shortcut.py +264 -0
- standkit_hub/web/app.js +803 -0
- standkit_hub/web/bpmkit-logo-dark.svg +5 -0
- standkit_hub/web/bpmkit-logo.svg +5 -0
- standkit_hub/web/favicon.png +0 -0
- standkit_hub/web/favicon.svg +4 -0
- standkit_hub/web/index.html +267 -0
- standkit_hub/web/style.css +843 -0
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)
|
|
36
|
+
[](https://www.python.org/)
|
|
37
|
+
[]()
|
|
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
|
+
|  |  |
|
|
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,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,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())
|