standkit 0.10.0__tar.gz → 0.12.2__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.
- {standkit-0.10.0/standkit.egg-info → standkit-0.12.2}/PKG-INFO +1 -1
- {standkit-0.10.0 → standkit-0.12.2}/docs/BACKLOG.md +3 -2
- {standkit-0.10.0 → standkit-0.12.2}/docs/CHANGELOG.md +269 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/HOSTING.md +21 -5
- {standkit-0.10.0 → standkit-0.12.2}/docs/README.md +1 -1
- standkit-0.12.2/docs/adr/0006-hub-elevated-restart.md +130 -0
- standkit-0.12.2/docs/adr/0007-hub-exit-idle-and-plain-restart.md +223 -0
- {standkit-0.10.0 → standkit-0.12.2}/pyproject.toml +1 -1
- {standkit-0.10.0 → standkit-0.12.2}/standkit/__init__.py +1 -1
- {standkit-0.10.0 → standkit-0.12.2}/standkit/adopt.py +12 -0
- standkit-0.12.2/standkit/cli_resolve.py +198 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit/hosting.py +96 -17
- standkit-0.12.2/standkit/platform.py +637 -0
- {standkit-0.10.0 → standkit-0.12.2/standkit.egg-info}/PKG-INFO +1 -1
- {standkit-0.10.0 → standkit-0.12.2}/standkit.egg-info/SOURCES.txt +22 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/context.py +47 -57
- standkit-0.12.2/standkit_companion/cookbook.py +299 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/runner.py +44 -4
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/state.py +25 -0
- standkit-0.12.2/standkit_hub/__main__.py +670 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/agent_control.py +4 -1
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/config.py +61 -0
- standkit-0.12.2/standkit_hub/consent_api.py +377 -0
- standkit-0.12.2/standkit_hub/elevated_op.py +188 -0
- standkit-0.12.2/standkit_hub/elevation.py +509 -0
- standkit-0.12.2/standkit_hub/hub_logging.py +153 -0
- standkit-0.12.2/standkit_hub/instance.py +482 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/license_api.py +86 -55
- standkit-0.12.2/standkit_hub/mutex.py +261 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/poller.py +13 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/server.py +1466 -66
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/shortcut.py +13 -2
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/app.js +889 -15
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/cookbook.html +62 -5
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/index.html +356 -10
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/style.css +274 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_context.py +208 -5
- standkit-0.12.2/tests/test_companion_cookbook.py +329 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_runner.py +52 -3
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hosting.py +95 -0
- standkit-0.12.2/tests/test_hub_consent_api.py +774 -0
- standkit-0.12.2/tests/test_hub_elevated_op.py +715 -0
- standkit-0.12.2/tests/test_hub_elevation.py +581 -0
- standkit-0.12.2/tests/test_hub_elevation_ui.py +721 -0
- standkit-0.12.2/tests/test_hub_exit_idle_restart.py +826 -0
- standkit-0.12.2/tests/test_hub_instance.py +463 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_license_api.py +141 -0
- standkit-0.12.2/tests/test_hub_logging.py +198 -0
- standkit-0.12.2/tests/test_hub_main_elevated.py +586 -0
- standkit-0.12.2/tests/test_hub_mutex.py +236 -0
- standkit-0.12.2/tests/test_hub_settings_field_help.py +169 -0
- standkit-0.12.2/tests/test_platform_sid.py +289 -0
- standkit-0.10.0/standkit/platform.py +0 -309
- standkit-0.10.0/standkit_hub/__main__.py +0 -187
- {standkit-0.10.0 → standkit-0.12.2}/LICENSE +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/MANIFEST.in +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/README.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/SECURITY.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/ARCHITECTURE.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/COOKBOOK_LINUX.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/GAP-001-registry-fields-out-of-hub-form.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/GAP-002-http-probe-down-not-explained.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/GAP-003-redis-endpoint-in-extra.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/GAP-004-cookbook-hub-register-form.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/GAP-005-cookbook-hub-side-secrets.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/GAP-006-log-dir-case-and-separator.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/GAP-007-agent-paths-raw-traceback.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/GAP-008-agent-cert-trust-on-hub.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/GAPs/README.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/REMOTE_STANDS.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/ROADMAP.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/adr/0001-hosting-backends.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/adr/0002-k8s-backend.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/adr/0003-remote-log-browsing.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/adr/0004-iis-site-scoped-lifecycle.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/adr/0005-companion-transport-in-mit-core.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/docs/research/companion-v2-cicd-thin-agent.md +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/projects.sample.json +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/setup.cfg +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit/health.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit/lifecycle.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit/logs.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit/models.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit/registry.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit/secrets.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit.egg-info/dependency_links.txt +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit.egg-info/entry_points.txt +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit.egg-info/requires.txt +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit.egg-info/top_level.txt +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_agent/__init__.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_agent/__main__.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_agent/audit.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_agent/security.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_agent/server.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/__init__.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/__main__.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/backend.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/ed25519.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/errors.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/fsutil.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/mcp_mutex.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/patterns.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/releases.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/revocations.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_companion/signature.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/__init__.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/assets/bpmkit-icon.ico +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/assets/icon.png +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/client.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/logs_browser.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/pick_dialog.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/redis_min.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/security.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/bpmkit-logo-dark.svg +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/bpmkit-logo.svg +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/favicon.png +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/favicon.svg +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/standkit_hub/web/manifest.webmanifest +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_adopt.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_agent_security.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_agent_server_integration.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_agent_startup_preflight.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_backend.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_fsutil.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_mcp_mutex.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_patterns.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_releases.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_revocations.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_companion_signature.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_health.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_agent_control.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_client_tls.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_companion_api.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_config.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_fast_paint.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_logs_browser.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_pick_dialog.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_pwa_and_compact.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_redis_min.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_register.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_server.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_shortcut.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_hub_single_instance.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_lifecycle.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_logs.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_models.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_no_window.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_registry.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_registry_resolver.py +0 -0
- {standkit-0.10.0 → standkit-0.12.2}/tests/test_secrets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: standkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.2
|
|
4
4
|
Summary: Свободное ядро (MIT) для управления жизненным циклом локальных и удалённых стендов BPMSoft: headless start/stop/restart, health-пробы, логи, реестр стендов. Часть экосистемы BPMkit.
|
|
5
5
|
Author: standkit contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -73,8 +73,9 @@ Redis теперь объясняются словами (`details['http_reason'
|
|
|
73
73
|
| Ярлык на рабочем столе для **macOS** | осознанно не реализовано — возвращается понятный статус | `standkit_hub/shortcut.py` (докстринг) |
|
|
74
74
|
| Нативный трей (иконка со сводным состоянием, баллоны при падении) | не реализовано — потребовало бы `pystray`/`Pillow` против инварианта stdlib-only. Вместо него PWA-установка и компактный режим `?view=compact` | `standkit_hub/web/manifest.webmanifest` |
|
|
75
75
|
| Автозапуск хаба при входе в систему | не реализовано; механика ярлыка есть | `standkit_hub/shortcut.py` |
|
|
76
|
-
| Остановка хаба, когда дашборд никто не смотрит (idle-shutdown
|
|
77
|
-
| Остановка хаба из интерфейса (кнопка «Завершить работу диспетчера») |
|
|
76
|
+
| Остановка хаба, когда дашборд никто не смотрит (idle-shutdown) | **реализовано** (0.12.0, ADR-0007) — `HubConfig.idle_shutdown_min`, 30 мин по умолчанию, `0` выключает. Простой = нет открытых SSE-клиентов И нет `running`-стендов (оба условия обязательны вместе); в `--desktop` — только по стендам. Таймер сбрасывается активностью, настройка перечитывается без перезапуска | `standkit_hub/server.py::_IdleShutdownWatcher`, `standkit_hub/config.py::idle_shutdown_min` |
|
|
77
|
+
| Остановка хаба из интерфейса (кнопка «Завершить работу диспетчера») | **реализовано** (0.12.0, ADR-0007) — кнопка «Выход» в шапке и в «Настройки → О программе», `POST /api/hub/shutdown`. Освобождает мьютекс установщика сразу, чистит файл состояния, потомков (запущенные стенды, локальный агент) НЕ трогает | `standkit_hub/server.py::HubHTTPServer.request_self_shutdown`, `web/app.js::exitHubFlow` |
|
|
78
|
+
| Перезапуск хаба из интерфейса без повышения прав | **реализовано** (0.12.0, ADR-0007, GAP-276) — `POST /api/hub/restart`, переиспользует машинерию elevated-перезапуска, но порождает процесс обычным `spawn_hidden` без UAC. Кнопки — в плашке рассинхрона версий и в «О программе» | `standkit_hub/server.py::_api_hub_restart`, `web/app.js::restartHubFlow` |
|
|
78
79
|
|
|
79
80
|
## Секреты
|
|
80
81
|
|
|
@@ -5,6 +5,269 @@
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.12.2] — 2026-09-19
|
|
9
|
+
|
|
10
|
+
### Исправлено
|
|
11
|
+
|
|
12
|
+
- **Диспетчер выходил по простою при ЖИВЫХ стендах (GAP-383).** Живой инцидент
|
|
13
|
+
владельца 18.09.2026 20:56: хаб завершился штатно (`request_self_shutdown`),
|
|
14
|
+
три работающих стенда остались без управления, на экране была только ошибка
|
|
15
|
+
связи. Предикат простоя `_IdleShutdownWatcher.has_running_stands()` сравнивал
|
|
16
|
+
`process.state` со строкой `"running"`, **которой в `ProbeState` нет вообще**
|
|
17
|
+
(`ok`/`down`/`unknown`/`skipped`) — то есть на любом реальном снапшоте он
|
|
18
|
+
отвечал «стендов нет», и 30-минутный таймер срабатывал независимо от того,
|
|
19
|
+
сколько стендов работает. Набор тестов оставался зелёным, потому что его
|
|
20
|
+
двойник снапшота подставлял то самое выдуманное `"running"`; теперь двойник
|
|
21
|
+
повторяет поля настоящего `StatusSnapshot`, а отдельный тест-сторож следит,
|
|
22
|
+
что значения `"running"` в `ProbeState` по-прежнему нет.
|
|
23
|
+
Тем же гэпом закрыты два способа принять незнание за пустоту: снапшот-ОТКАЗ
|
|
24
|
+
поллера (`_safe_build`: `stands=[]`, `probed=False`, `error=...`) и
|
|
25
|
+
непробованный слепок реестра (`probed=False`, состояния `pending`) теперь
|
|
26
|
+
трактуются как «стенды есть» — ровно как обещали докстринг предиката и
|
|
27
|
+
ADR-0007 («незнание = остаёмся»).
|
|
28
|
+
- **Автовыход при заменённых под живым процессом файлах пакета (GAP-383).**
|
|
29
|
+
`pip install standkit` поверх работающего диспетчера (владелец сделал это за
|
|
30
|
+
два часа до инцидента) оставляет на диске новый код, а в памяти старый.
|
|
31
|
+
Детект рассинхрона у хаба был (ADR-0007 §4), но автовыход про него не знал.
|
|
32
|
+
Теперь при расхождении версии «с диска» и «из памяти» диспетчер **не
|
|
33
|
+
выходит** по простою: перезапуск остаётся действием человека
|
|
34
|
+
(`POST /api/hub/restart` с открытой страницы), молчаливого самоперезапуска
|
|
35
|
+
фонового процесса не вводится. Ошибка чтения версии — тихий фолбэк, она не
|
|
36
|
+
объявляет рассинхрон и не запрещает автовыход навсегда.
|
|
37
|
+
- **Усыновлённые и поднятые мимо диспетчера стенды считаются живыми
|
|
38
|
+
(GAP-385).** Условие простоя теперь «нет НИ ОДНОГО стенда, живого ПО ФАКТУ
|
|
39
|
+
пробы (`ProbeState.OK`, то есть по порту/pid)», а не «нет запущенных нами».
|
|
40
|
+
Стенд с мёртвым родителем (`standkit.adopt`, бейдж `process.external`) —
|
|
41
|
+
такой же повод остаться: потерять управление им ничуть не лучше, чем своим.
|
|
42
|
+
|
|
43
|
+
### Добавлено
|
|
44
|
+
|
|
45
|
+
- **Лог диспетчера в файл (GAP-384).** Новый модуль `standkit_hub/hub_logging.py`:
|
|
46
|
+
`resolve_log_dir()` (`%LOCALAPPDATA%\BPMkit\logs\hub.log`, на POSIX —
|
|
47
|
+
`$XDG_STATE_HOME`/`~/.local/state`) и ротация 2 МБ × 5. Причина слепоты при
|
|
48
|
+
разборе инцидента была структурной: ярлык запускает диспетчер через
|
|
49
|
+
`pythonw.exe`, у которого `sys.stderr` не подключён никуда (и может быть
|
|
50
|
+
`None`), а все объяснения — «простой N минут», «стоп-запрос», «перехват
|
|
51
|
+
порта» — писались через `print(..., file=sys.stderr)` и существовали ровно
|
|
52
|
+
на время своего вывода. Теперь в файл пишутся: старт и версия, запрос
|
|
53
|
+
штатного выхода, стоп-запрос (включая протухший) и **факты, по которым
|
|
54
|
+
принято решение об автовыходе** — `sse_client_count`, `has_running_stands`,
|
|
55
|
+
`snapshot_probed`, `snapshot_error`; отказ фонового опроса
|
|
56
|
+
(`poller._safe_build`) логируется с трейсбеком; необработанное исключение
|
|
57
|
+
процесса уходит в лог через `sys.excepthook`. Все 16 `print(..., file=sys.stderr)`
|
|
58
|
+
в `standkit_hub/__main__.py` переведены на `logging`. Неудача настройки лога
|
|
59
|
+
старт НЕ прерывает: диспетчер без лога хуже, но несравнимо лучше
|
|
60
|
+
незапустившегося.
|
|
61
|
+
|
|
62
|
+
## [0.12.1] — 2026-09-17
|
|
63
|
+
|
|
64
|
+
### Добавлено
|
|
65
|
+
|
|
66
|
+
- **Кукбук пользователя как отдельный артефакт канала (GAP-361).** Модуль
|
|
67
|
+
`standkit_companion/cookbook.py`: документ приезжает по каналу издателя
|
|
68
|
+
пассажиром на `sync_patterns`/`check_update`, ложится в
|
|
69
|
+
`%APPDATA%\BPMkit\docs\cookbook.html` атомарной заменой после проверки
|
|
70
|
+
подписи и сверки версии из `/meta`. Слот `staged` релизного канала и
|
|
71
|
+
инварианты GAP-212/279 не затронуты: у кукбука свой узкий поток.
|
|
72
|
+
Отказ попутчика не роняет несущую операцию; 404 канала — штатный пропуск
|
|
73
|
+
тика (`skipped`), пока издатель не выложил документ.
|
|
74
|
+
|
|
75
|
+
### Примечание к версии
|
|
76
|
+
|
|
77
|
+
- Выпуск PATCH поверх 0.12.0: содержимое 0.12.0 (PR #55, GAP-361) на PyPI
|
|
78
|
+
**не публиковалось** — токена не было, а установщик BPMkit 1.1.75 собран с
|
|
79
|
+
указателем `d79f3cc` (без кукбук-канала). Отдельный номер 0.12.1 нужен,
|
|
80
|
+
чтобы опубликованный пакет и собранные установщики нельзя было спутать по
|
|
81
|
+
одному и тому же номеру.
|
|
82
|
+
|
|
83
|
+
## [0.12.0] — 2026-09-17
|
|
84
|
+
|
|
85
|
+
### Добавлено
|
|
86
|
+
|
|
87
|
+
- **Кнопка «Выход» в диспетчере (Д-3).** Диспетчер живёт под `pythonw.exe` без
|
|
88
|
+
окна: закрытая вкладка НЕ завершает процесс, и он до перезагрузки держит порт
|
|
89
|
+
8770 и именованный мьютекс, из-за которого установщик BPMkit отказывается
|
|
90
|
+
работать («диспетчер запущен»). Теперь выход есть в шапке (иконка) и в
|
|
91
|
+
«Настройки → О программе», с подтверждением, которое прямо снимает главный
|
|
92
|
+
страх: «Запущенные стенды продолжат работать — выход останавливает только сам
|
|
93
|
+
диспетчер». Трея по-прежнему нет. Новый маршрут `POST /api/hub/shutdown`
|
|
94
|
+
(токен записи + локальный Origin): освобождает мьютекс СРАЗУ (ради него выход
|
|
95
|
+
чаще всего и нажимают), чистит файл состояния экземпляра, завершает сервер тем
|
|
96
|
+
же путём, что перехват порта, и НЕ трогает потомков — запущенные стенды и
|
|
97
|
+
локальный агент переживают выход.
|
|
98
|
+
- **Автовыход по простою** — `HubConfig.idle_shutdown_min`, по умолчанию 30
|
|
99
|
+
минут, `0` выключает. Условие простоя — конъюнкция: нет ни одного открытого
|
|
100
|
+
SSE-клиента (дашборд закрыт) И нет ни одного `running`-стенда (диспетчер ничем
|
|
101
|
+
не управляет). Оба слагаемых обязательны вместе: закрытая вкладка при живых
|
|
102
|
+
стендах — штатная работа, а открытая вкладка без стендов означает, что человек
|
|
103
|
+
смотрит на экран. Незнание (поллер ещё не отдал первый снапшот) трактуется в
|
|
104
|
+
пользу «остаться». В режиме `--desktop` признак «никто не смотрит»
|
|
105
|
+
недостоверен, поэтому условие сводится только к стендам. Настройка
|
|
106
|
+
перечитывается перед каждой проверкой — правка в форме применяется без
|
|
107
|
+
перезапуска. Поле — в «Настройки → Основные».
|
|
108
|
+
- **`POST /api/hub/restart` — перезапуск БЕЗ повышения прав (GAP-276 п.3).** Та
|
|
109
|
+
же машинерия, что у `restart-elevated` (`--takeover`, файл передачи сессии,
|
|
110
|
+
`result_file`, ожидание возврата вкладкой), но новый процесс порождается
|
|
111
|
+
обычным `spawn_hidden`, а не `ShellExecuteW(runas)` — окна UAC нет и права
|
|
112
|
+
остаются теми же. Чинит рассинхрон «код старый, статика новая» после
|
|
113
|
+
`pip install` поверх работающего хаба. Кнопки — в плашке рассинхрона и в
|
|
114
|
+
«О программе».
|
|
115
|
+
- **Детект рассинхрона версий страницы и сервера (GAP-276 п.2).** Статика
|
|
116
|
+
читается с диска на каждый запрос, код сервера живёт в памяти процесса:
|
|
117
|
+
обновление пакета под работающим хабом даёт новую страницу на старом сервере.
|
|
118
|
+
Сервер подставляет в `<meta name="standkit-build">` версию, прочитанную из
|
|
119
|
+
файла пакета НА ДИСКЕ (`on_disk_standkit_version`, а не `standkit.__version__`
|
|
120
|
+
из памяти — именно в этом расхождении и состоит гэп), страница сверяет её с
|
|
121
|
+
`/api/version` и при расхождении показывает плашку «Диспетчер обновлён до X,
|
|
122
|
+
сервер работает на Y — перезапустить» с кнопкой.
|
|
123
|
+
|
|
124
|
+
### Исправлено
|
|
125
|
+
|
|
126
|
+
- **Неизвестный маршрут `/api/*` отвечает человеческим текстом, а не сырым
|
|
127
|
+
`not found` (GAP-276 п.4).** Живой случай 11.09.2026: раздел «Лицензия» новой
|
|
128
|
+
страницы позвал маршрут, которого у старого сервера ещё не было, и
|
|
129
|
+
пользователь увидел `not found` как «лицензия не найдена» — сообщение,
|
|
130
|
+
уводящее в прямо противоположную от причины сторону. Теперь: «Сервер
|
|
131
|
+
диспетчера не поддерживает эту функцию — возможно, он устарел. Перезапустите
|
|
132
|
+
диспетчер и повторите», а если страница прислала свою версию сборки
|
|
133
|
+
(`X-Standkit-Build`) и она новее — обе версии называются прямо.
|
|
134
|
+
- **Тесты через `node` больше не падают на русской Windows.** `subprocess.run`
|
|
135
|
+
с `text=True` без явной кодировки декодировал UTF-8-вывод node локалью
|
|
136
|
+
процесса (cp1251), и все проверки русских сообщений падали на мусоре вместо
|
|
137
|
+
текста. Пять тестов `*_via_node` в `test_hub_elevation_ui.py` и
|
|
138
|
+
`test_hub_consent_api.py`.
|
|
139
|
+
- **`test_find_cli_returns_none_when_nothing_found` больше не зависит от того,
|
|
140
|
+
где запущен.** `extra_roots` не изолировал обход вверх от пакета: при
|
|
141
|
+
editable-установке прямо из dev-репозитория издателя четвёртый уровень вверх —
|
|
142
|
+
корень `bpmsoft-mcp` с настоящим `BPMkit/server/main.py`, и тест падал не
|
|
143
|
+
из-за кода.
|
|
144
|
+
|
|
145
|
+
## [0.11.5] — 2026-09-16
|
|
146
|
+
|
|
147
|
+
### Добавлено
|
|
148
|
+
|
|
149
|
+
- **Раздел «Данные и телеметрия» в настройках диспетчера (GAP-332).** Показывает и меняет
|
|
150
|
+
четыре согласия MCP BPMkit (обезличенная телеметрия использования, приложение журнала к
|
|
151
|
+
обращениям, отправка кандидатов-паттернов, отправка кандидатов) — тем же приёмом, каким
|
|
152
|
+
сделана лицензия: диспетчер только читает и передаёт CLI (`setup consent-info --json` /
|
|
153
|
+
`setup consent-set`), своей логики согласий у него нет. Каждый переключатель — со
|
|
154
|
+
справкой-«?», что именно уходит; строка состояния «последняя отправка / накоплено /
|
|
155
|
+
приёмник / установка», отметка о принятии соглашения, кнопка «Показать, что уйдёт» с
|
|
156
|
+
предпросмотром текста CLI. Раздел виден всегда, включая свободную редакцию и отсутствие
|
|
157
|
+
CLI рядом — тогда вместо переключателей честная подсказка с причиной и путём в
|
|
158
|
+
«Настройки → Основные».
|
|
159
|
+
- `GET /api/consent` и `POST /api/consent` — новые маршруты хаба
|
|
160
|
+
(`standkit_hub/consent_api.py`), тот же контур авторизации и CSRF-защиты, что у
|
|
161
|
+
`/api/license`.
|
|
162
|
+
|
|
163
|
+
## [0.11.4] — 2026-09-16
|
|
164
|
+
|
|
165
|
+
### Исправлено
|
|
166
|
+
|
|
167
|
+
- **После перезапуска с правами окно ожидания закрывается само.** Если новый процесс
|
|
168
|
+
диспетчера занимал порт быстрее интервала опроса, вкладка не видела обрыва связи и
|
|
169
|
+
ждала его до таймаута: оверлей висел, щит оставался красным, хотя диспетчер уже работал
|
|
170
|
+
с правами. Теперь ответ `elevated: true` (или исчезнувшее состояние ожидания) от того же
|
|
171
|
+
адреса считается новым процессом — страница перезагружается.
|
|
172
|
+
|
|
173
|
+
## [0.11.3] — 2026-09-16
|
|
174
|
+
|
|
175
|
+
### Изменено
|
|
176
|
+
|
|
177
|
+
- **Справка у полей «Настройки → Основные».** Длинные серые пояснения под полями заменены
|
|
178
|
+
маленькой иконкой «?» рядом с подписью: при наведении или фокусе — для чего поле и
|
|
179
|
+
пример значения (реестр стендов, CLI BPMkit, каталог pid-файлов, каталог логов, интервал
|
|
180
|
+
опроса). Пустые каталоги pid-файлов и логов показывают фактический каталог по умолчанию
|
|
181
|
+
вместо «не задано».
|
|
182
|
+
|
|
183
|
+
## [0.11.2] — 2026-09-16
|
|
184
|
+
|
|
185
|
+
### Изменено
|
|
186
|
+
|
|
187
|
+
- **Щит-статус прав администратора (GAP-311, решение владельца 16.09.2026,
|
|
188
|
+
пересматривает решение 15.09.2026).** На Windows значок в шапке виден теперь
|
|
189
|
+
ВСЕГДА, а не только когда прав не хватает — только иконка, без подписи:
|
|
190
|
+
зелёный (галочка) — права есть, клик открывает «О программе» и ничего не
|
|
191
|
+
перезапускает; красный (восклицательный знак) — прав нет, клик запускает
|
|
192
|
+
перезапуск с правами через UAC, как раньше; серый — определить права не
|
|
193
|
+
удалось. Кнопка — обычный квадрат 34×34, без своей рамки/фона-пилюли.
|
|
194
|
+
|
|
195
|
+
### Добавлено
|
|
196
|
+
|
|
197
|
+
- **Поле «CLI BPMkit» в «Настройки → Основные»** (рядом с «Реестр стендов») —
|
|
198
|
+
раньше жило только в «Настройки → Обновления», а тот раздел скрыт целиком в
|
|
199
|
+
свободной редакции, хотя сам CLI (проверка лицензии, версия MCP) нужен
|
|
200
|
+
независимо от неё. Под полем — строка «Сейчас используется: …» / «Сейчас:
|
|
201
|
+
CLI не найден». Сохранение настроек сбрасывает кэш сводки лицензии И кэш
|
|
202
|
+
лицензионного контекста канала обновлений, чтобы статус и версия MCP
|
|
203
|
+
обновились немедленно, а не по истечении их TTL.
|
|
204
|
+
- **`GET /api/license`: поля `cli` и `cli_source`.** `cli` — итоговая команда
|
|
205
|
+
резолва CLI строкой (`subprocess.list2cmdline`) или `null`; `cli_source` —
|
|
206
|
+
`"settings"`/`"env"`/`"auto"`/`null`. Вычисляются резолвом, без лишнего
|
|
207
|
+
запуска CLI.
|
|
208
|
+
|
|
209
|
+
### Исправлено
|
|
210
|
+
|
|
211
|
+
- **Версия MCP в UI — только фактическая.** Плейсхолдер «как в поставке»
|
|
212
|
+
убран: показывается версия из `GET /api/license` (`mcp_version`, отдаёт CLI
|
|
213
|
+
`setup license-info --json`) либо, если её нет, `current_version` канала
|
|
214
|
+
обновлений; если версия неизвестна вовсе — честно «неизвестна» (с уточнением
|
|
215
|
+
«не найден CLI BPMkit», если резолв не нашёл CLI). Строка состояния внизу
|
|
216
|
+
(`#sl-mcp`) больше не прячет сегмент «MCP» при неизвестной версии — она
|
|
217
|
+
видна с подсказкой, что поправить в «Настройки → Основные».
|
|
218
|
+
|
|
219
|
+
## [0.11.1] — 2026-09-16
|
|
220
|
+
|
|
221
|
+
### Исправлено
|
|
222
|
+
|
|
223
|
+
- **Остановленная служба W3SVC больше не выдаётся за «обрыв канала управления IIS».**
|
|
224
|
+
Живьём на iis19: `appcmd start site` при остановленной «Службе веб-публикаций»
|
|
225
|
+
отвечает `0x80010006` — тем же текстом, что транзиентный сбой RPC, и диспетчер
|
|
226
|
+
советовал просто повторить. Теперь при таком ответе сверяется состояние служб WAS/W3SVC,
|
|
227
|
+
и ошибка называет остановленную службу и команду запуска. Проверка прав администратора
|
|
228
|
+
идёт раньше классификации RPC: без прав appcmd отказывает под любым текстом.
|
|
229
|
+
- **Щит «без прав администратора» не ломает шапку**: подпись переносилась в две строки
|
|
230
|
+
внутри квадратной кнопки 34×34.
|
|
231
|
+
|
|
232
|
+
## [0.11.0] — 2026-09-15
|
|
233
|
+
|
|
234
|
+
### Добавлено
|
|
235
|
+
|
|
236
|
+
- **Права администратора для IIS прямо из диспетчера (GAP-311).** Разбор —
|
|
237
|
+
[ADR-0006](adr/0006-hub-elevated-restart.md).
|
|
238
|
+
- **Щит «без прав администратора» в шапке** — виден только когда диспетчер работает
|
|
239
|
+
без прав (`GET /api/hub/elevation`). По клику, после подтверждения, диспетчер
|
|
240
|
+
перезапускается через окно Windows (UAC) на том же адресе, открытая вкладка
|
|
241
|
+
переподключается сама: сессия переезжает одноразовым файлом, не аргументом
|
|
242
|
+
командной строки.
|
|
243
|
+
- **Кнопки в ошибке IIS.** Ответ действия со стендом при нехватке прав теперь несёт
|
|
244
|
+
`elevation_required: true`, и в сообщении есть «Перезапустить с правами
|
|
245
|
+
администратора» и «Только эту операцию».
|
|
246
|
+
- **Однократная операция с правами** (`POST /api/hub/elevated-op`, режим
|
|
247
|
+
`--elevated-op start|stop|restart --stand`) — одна операция над IIS-стендом через
|
|
248
|
+
окно Windows, сам диспетчер остаётся без прав. Также доступна в «Настройки → О
|
|
249
|
+
программе», где видно и текущее состояние прав.
|
|
250
|
+
- **Отказ при подтверждении под другой учётной записью** — новый процесс сверяет SID
|
|
251
|
+
и ничего не делает, старый диспетчер показывает пояснение и продолжает работать.
|
|
252
|
+
|
|
253
|
+
### Исправлено
|
|
254
|
+
|
|
255
|
+
- **Запуск ярлыка «от имени администратора» больше не выглядит как «ничего не
|
|
256
|
+
произошло».** Раньше новый процесс видел работающий диспетчер на порту 8770, открывал
|
|
257
|
+
браузер на СТАРОМ, без прав, и выходил. Теперь он перехватывает порт у неэлевированного
|
|
258
|
+
экземпляра (файл состояния `standkit_hub/instance.py`).
|
|
259
|
+
- **Перехват порта не убивает запущенные стенды.** Старый диспетчер останавливается по
|
|
260
|
+
запросу (файл stop-request в `run_dir`) и сам закрывает сокет; эскалация, если он
|
|
261
|
+
завис, завершает только его pid (`taskkill` без `/T`) и только после подтверждения у
|
|
262
|
+
ОС, что это действительно диспетчер. Новый процесс занимает строго тот же порт.
|
|
263
|
+
- **`appcmd.exe` у 32-битного Python** резолвится через `Sysnative` (раньше
|
|
264
|
+
WOW64-редирект давал «файл не найден», похожий на отказ по правам).
|
|
265
|
+
- **Мьютекс диспетчера, созданный с правами администратора, виден установщику BPMkit**
|
|
266
|
+
без прав — явный security descriptor.
|
|
267
|
+
- Проверка «процесс с правами администратора» переехала в
|
|
268
|
+
`standkit.platform.is_elevated()`; добавлены `current_user_sid()`,
|
|
269
|
+
`current_user_name()`, `process_create_time()`.
|
|
270
|
+
|
|
8
271
|
## [0.10.0] — 2026-09-08
|
|
9
272
|
|
|
10
273
|
### Добавлено
|
|
@@ -792,6 +1055,12 @@
|
|
|
792
1055
|
- Первый публичный релиз ядра `standkit` + агента `standkit_agent` + веб-дашборда
|
|
793
1056
|
`standkit_hub` на PyPI; фронт-README, `docs/REMOTE_STANDS.md`, скриншоты.
|
|
794
1057
|
|
|
1058
|
+
[0.11.5]: https://pypi.org/project/standkit/0.11.5/
|
|
1059
|
+
[0.11.4]: https://pypi.org/project/standkit/0.11.4/
|
|
1060
|
+
[0.11.3]: https://pypi.org/project/standkit/0.11.3/
|
|
1061
|
+
[0.11.2]: https://pypi.org/project/standkit/0.11.2/
|
|
1062
|
+
[0.11.1]: https://pypi.org/project/standkit/0.11.1/
|
|
1063
|
+
[0.11.0]: https://pypi.org/project/standkit/0.11.0/
|
|
795
1064
|
[0.10.0]: https://pypi.org/project/standkit/0.10.0/
|
|
796
1065
|
[0.9.0]: https://pypi.org/project/standkit/0.9.0/
|
|
797
1066
|
[0.8.0]: https://pypi.org/project/standkit/0.8.0/
|
|
@@ -162,11 +162,27 @@ Health-проба «процесс» (`standkit.health.check_stand`) для `iis
|
|
|
162
162
|
что требует прав администратора; без elevation любая IIS-операция падает с
|
|
163
163
|
ошибкой прав (например, `код 1168` / «не удалось открыть файл конфигурации
|
|
164
164
|
из-за отсутствия необходимых разрешений»). Членства в группе `IIS_IUSRS`
|
|
165
|
-
**недостаточно**. Как запускать elevated:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
**недостаточно**. Как запускать elevated:
|
|
166
|
+
|
|
167
|
+
- **из самого дашборда** — щит «без прав администратора» в шапке (виден, только
|
|
168
|
+
когда прав действительно нет) или кнопка «Перезапустить с правами администратора»
|
|
169
|
+
в сообщении об ошибке: подтвердите окно Windows, и диспетчер поднимется заново на
|
|
170
|
+
том же адресе, вкладка переподключится сама. Запущенные стенды перезапуск не
|
|
171
|
+
трогает. Если окно подтвердили под другой учётной записью — диспетчер откажет с
|
|
172
|
+
пояснением и продолжит работать без прав;
|
|
173
|
+
- **только одну операцию** — «Только эту операцию» в сообщении об ошибке или блок
|
|
174
|
+
«Однократная операция с правами администратора» в «Настройки → О программе»:
|
|
175
|
+
старт/стоп/перезапуск IIS-стенда с запросом прав, сам диспетчер остаётся без них.
|
|
176
|
+
Разбор обоих путей — [ADR-0006](adr/0006-hub-elevated-restart.md);
|
|
177
|
+
- ярлык/консоль → правый клик → «Запуск от имени администратора» (elevated
|
|
178
|
+
экземпляр заменит собой уже работающий обычный; до 0.11.0 он молча открывал
|
|
179
|
+
браузер на старом, и казалось, что запуск не сработал);
|
|
180
|
+
- раз и навсегда — свойства ярлыка → «Дополнительно» → галка «Запуск от
|
|
181
|
+
имени администратора»; либо `standkit-hub` из уже поднятой
|
|
182
|
+
административной консоли.
|
|
183
|
+
|
|
184
|
+
Диспетчер в такой ситуации отдаёт понятную подсказку «запустите от имени
|
|
185
|
+
администратора» в тексте ошибки. Живая приёмка — на стенде с реальным IIS.
|
|
170
186
|
- **Docker**: установленный Docker Engine (`docker` в PATH процесса
|
|
171
187
|
standkit); для compose-режима — плагин `docker compose` (Compose V2).
|
|
172
188
|
- **Kubernetes**: установленный `kubectl` в PATH процесса standkit и
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
| Документ | О чём |
|
|
39
39
|
|---|---|
|
|
40
40
|
| [ARCHITECTURE.md](ARCHITECTURE.md) | Краткое зеркало архитектуры: что за что отвечает, границы модулей |
|
|
41
|
-
| [adr/](adr/) | Архитектурные решения с разбором альтернатив — [0001 хостинг-бэкенды](adr/0001-hosting-backends.md) · [0002 k8s](adr/0002-k8s-backend.md) · [0003 логи удалённых стендов](adr/0003-remote-log-browsing.md) · [0004 IIS по Site](adr/0004-iis-site-scoped-lifecycle.md) · [0005 транспорт канала обновлений в MIT-ядре](adr/0005-companion-transport-in-mit-core.md) |
|
|
41
|
+
| [adr/](adr/) | Архитектурные решения с разбором альтернатив — [0001 хостинг-бэкенды](adr/0001-hosting-backends.md) · [0002 k8s](adr/0002-k8s-backend.md) · [0003 логи удалённых стендов](adr/0003-remote-log-browsing.md) · [0004 IIS по Site](adr/0004-iis-site-scoped-lifecycle.md) · [0005 транспорт канала обновлений в MIT-ядре](adr/0005-companion-transport-in-mit-core.md) · [0006 права администратора для IIS](adr/0006-hub-elevated-restart.md) |
|
|
42
42
|
| [CHANGELOG.md](CHANGELOG.md) | История изменений по версиям (Keep a Changelog + SemVer) |
|
|
43
43
|
| [research/](research/) | Проработки без принятых решений — [companion v2: CI/CD через тонкого агента](research/companion-v2-cicd-thin-agent.md) |
|
|
44
44
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# ADR-0006 — Права администратора для IIS: перезапуск диспетчера через UAC и однократная операция
|
|
2
|
+
|
|
3
|
+
- Статус: **Принято / реализовано** (v0.11.0, GAP-311, 15.09.2026). Первая версия
|
|
4
|
+
перезапуска написана 19.08.2026 в ветке `feat/hub-elevated-restart` (тогда под номером
|
|
5
|
+
ADR-0005, который к мержу оказался занят ADR о канале обновлений), перенесена в
|
|
6
|
+
`main` с доработками по двум раундам ревью.
|
|
7
|
+
- Контекст-владелец: Владимир Терновский
|
|
8
|
+
- Связано: [ADR-0004](0004-iis-site-scoped-lifecycle.md) (диагноз «нехватка прав» у `appcmd`),
|
|
9
|
+
[HOSTING.md](../HOSTING.md); в dev-репо BPMkit — GAP-282 (путь без прав: перезапуск
|
|
10
|
+
приложения сменой `LastWriteTime` у `web.config`) и GAP-311.
|
|
11
|
+
|
|
12
|
+
## Контекст и проблема
|
|
13
|
+
|
|
14
|
+
Управление стендами IIS идёт через `appcmd.exe`, а он без прав администратора не работает
|
|
15
|
+
в принципе: не читает даже `…\inetsrv\config\redirection.config`. Диспетчер честно называл
|
|
16
|
+
причину и советовал «запустите standkit-hub с правами администратора», но выполнить совет
|
|
17
|
+
было нельзя:
|
|
18
|
+
|
|
19
|
+
1. Ошибка остановки IIS-стенда с подсказкой про права.
|
|
20
|
+
2. Пользователь запускает ярлык «от имени администратора».
|
|
21
|
+
3. Ничего не меняется: новый процесс видит на порту 8770 работающий диспетчер
|
|
22
|
+
(single-instance, `HubAlreadyRunning`), открывает браузер на СТАРОМ — без прав — и выходит.
|
|
23
|
+
|
|
24
|
+
Живьём воспроизведено дважды: 19.08.2026 и 15.09.2026 (`TokenIsElevated=0` у работающего
|
|
25
|
+
`pythonw -m standkit_hub`, хотя пользователь уверен, что запускал от администратора).
|
|
26
|
+
|
|
27
|
+
## Решение
|
|
28
|
+
|
|
29
|
+
### 1. Видно, что прав нет
|
|
30
|
+
|
|
31
|
+
`GET /api/hub/elevation` → `{supported, elevated, can_restart, reason, user, restart}`.
|
|
32
|
+
Щит «без прав администратора» в шапке показывается **только когда прав нет** (решение
|
|
33
|
+
владельца). С правами и на не-Windows щита нет; состояние прав всегда видно в
|
|
34
|
+
«Настройки → О программе». Ошибка действия со стендом при нехватке прав приходит с
|
|
35
|
+
признаком `elevation_required: true`, и в сообщении две кнопки — «Перезапустить с правами
|
|
36
|
+
администратора» и «Только эту операцию».
|
|
37
|
+
|
|
38
|
+
### 2. Перезапуск диспетчера с правами (П1)
|
|
39
|
+
|
|
40
|
+
`POST /api/hub/restart-elevated`:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
старый хаб новый хаб (runas, --takeover)
|
|
44
|
+
────────── ─────────────────────────────
|
|
45
|
+
restart=requesting (под lock)
|
|
46
|
+
handoff-файл с токеном сессии
|
|
47
|
+
ShellExecuteW runas ──UAC «Нет»──> 409 cancelled, restart=null, handoff удалён
|
|
48
|
+
│ «Да»
|
|
49
|
+
202, restart=pending, 1. SID != --initiator-sid → result: refused, выход
|
|
50
|
+
наблюдатель result-файла 2. читает handoff (одноразово)
|
|
51
|
+
│ 3. пишет stop-request (pid старого)
|
|
52
|
+
stop-request для моего pid ───────────────── 4. ждёт освобождения порта
|
|
53
|
+
штатный shutdown, выход 5. bind СТРОГО на тот же порт (повторы)
|
|
54
|
+
(дочерние стенды и агент живут) 6. result: serving | failed (+причина)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Старый процесс завершается **только** по stop-request — не «через секунду после 202».
|
|
58
|
+
Отказ под другой учётной записью, сбой перехвата или таймаут оставляют старый
|
|
59
|
+
диспетчер работать, а причина видна в `restart.status` = `refused` / `failed`.
|
|
60
|
+
- Если stop-request проигнорирован (зависший хаб), эскалация завершает **только сам pid**
|
|
61
|
+
(`taskkill` без `/T`): стенды Kestrel и локальный агент — дочерние процессы хаба и не
|
|
62
|
+
должны умирать вместе с ним. Перед эскалацией личность процесса подтверждается у ОС
|
|
63
|
+
(время создания процесса против записанного в файле состояния; маркер в командной
|
|
64
|
+
строке — только когда время создания получить нельзя). Не подтверждено — не убиваем,
|
|
65
|
+
отдаём «остановите вручную».
|
|
66
|
+
- Сессия переезжает одноразовым файлом передачи (TTL 3 минуты, удаляется при чтении),
|
|
67
|
+
не аргументом командной строки. Открытая вкладка переподключается сама.
|
|
68
|
+
- Запуск ярлыка «от имени администратора» при живом неэлевированном экземпляре идёт тем
|
|
69
|
+
же путём перехвата (stop-request, строгий bind).
|
|
70
|
+
- Перезапуск учитывает поставку: во frozen-сборке (`BPMkit-hub.exe`) запускается
|
|
71
|
+
`sys.executable`, в режиме `--desktop` новый процесс открывает своё окно, а старое
|
|
72
|
+
закрывается по stop-request. Передаются `--host`/`--insecure`, абсолютный `--config`.
|
|
73
|
+
- Права держатся до выхода диспетчера; обратного перехвата (elevated уступает обычному) нет.
|
|
74
|
+
|
|
75
|
+
### 3. Однократная операция с правами (П2)
|
|
76
|
+
|
|
77
|
+
`POST /api/hub/elevated-op {stand, action}` запускает через UAC тот же исполняемый файл в
|
|
78
|
+
режиме `--elevated-op start|stop|restart --stand <имя> --result-file <путь>`: одна операция
|
|
79
|
+
над стендом `host_kind=iis`, без HTTP-сервера, мьютекса и файла состояния, результат в файл,
|
|
80
|
+
выход. Диспетчер остаётся без прав. `GET /api/hub/elevated-op/<op_id>` —
|
|
81
|
+
`pending` / `ok` / `error` / `refused` / `expired`. Одна активная операция на стенд.
|
|
82
|
+
|
|
83
|
+
### 4. Повышение под другой учётной записью — отказ с пояснением
|
|
84
|
+
|
|
85
|
+
Если окно UAC подтвердил другой пользователь (over-the-shoulder), у нового процесса другой
|
|
86
|
+
профиль: другой `projects.json`, конфиг, ключи. Работать от его имени — значит молча
|
|
87
|
+
показать пустой реестр или чужие секреты. Новый процесс сравнивает SID своего токена с
|
|
88
|
+
`--initiator-sid` и при расхождении ничего не делает, записывая текст: «Права подтверждены
|
|
89
|
+
другой учётной записью (…). Диспетчер работает с реестром стендов и ключами текущего
|
|
90
|
+
пользователя Windows, поэтому повышение под другой учётной записью не поддерживается. …»
|
|
91
|
+
|
|
92
|
+
### 5. Мьютекс диспетчера
|
|
93
|
+
|
|
94
|
+
Именованный мьютекс (GAP-229/284) берётся после bind. Созданный elevated-процессом объект
|
|
95
|
+
получил бы DACL «только администраторы», и неэлевированный установщик BPMkit не увидел бы
|
|
96
|
+
работающий диспетчер. Поэтому мьютекс создаётся с явным security descriptor
|
|
97
|
+
(SYSTEM, Администраторы, текущий пользователь — полный доступ; Everyone — SYNCHRONIZE).
|
|
98
|
+
|
|
99
|
+
## Модель угроз
|
|
100
|
+
|
|
101
|
+
- Сторонний сайт не может запросить UAC или операцию: все новые мутации — `POST` с
|
|
102
|
+
CSRF-заголовком, локальным `Origin` и сессионным токеном; `stand`/`action`/`op_id`
|
|
103
|
+
валидируются белыми списками и регулярными выражениями.
|
|
104
|
+
- **UAC — не граница безопасности** (позиция Microsoft). Процесс той же учётной записи
|
|
105
|
+
без прав может править конфиг и реестр стендов, которые затем прочитает elevated-процесс.
|
|
106
|
+
Мы это принимаем: согласие в окне UAC даётся на диспетчер, работающий с данными этого
|
|
107
|
+
пользователя. Что сделано против дешёвых приёмов: elevated-процесс не пишет в `run_dir`
|
|
108
|
+
и не читает оттуда, если путь или файл — reparse point/symlink; временные файлы — со
|
|
109
|
+
случайным именем и `O_EXCL`; однократная операция повторно проверяет `host_kind=iis`.
|
|
110
|
+
- Права `0o600` на Windows ACL не задают — защиту файла передачи обеспечивает ACL профиля.
|
|
111
|
+
`run_dir` вне профиля пользователя даёт предупреждение в логе.
|
|
112
|
+
|
|
113
|
+
## Последствия
|
|
114
|
+
|
|
115
|
+
- Ошибка прав для IIS решается кнопкой в два клика и видна до первой неудачной операции.
|
|
116
|
+
- Отказ в UAC, чужая учётка, зависший старый процесс — у каждого свой понятный исход,
|
|
117
|
+
пользователь не остаётся без диспетчера.
|
|
118
|
+
- Живая приёмка возможна только на Windows с IIS и настоящим окном UAC; юнит-тесты
|
|
119
|
+
покрывают протокол, но не WinAPI.
|
|
120
|
+
|
|
121
|
+
## Альтернативы, которые не выбрали
|
|
122
|
+
|
|
123
|
+
- **Только однократная операция на каждое действие.** Минимальные права, но окно UAC на
|
|
124
|
+
каждый старт/стоп в ежедневной работе неприемлемо — оставлена как дополнительный путь (П2).
|
|
125
|
+
- **Галка «Запуск от имени администратора» в свойствах ярлыка.** Лечит только будущие
|
|
126
|
+
запуски, не ситуацию «диспетчер уже открыт». Осталась рекомендацией в HOSTING.md.
|
|
127
|
+
- **Задача планировщика «с наивысшими правами» из установщика.** Отклонена владельцем
|
|
128
|
+
14.09.2026 как избыточная.
|
|
129
|
+
- **Служба Windows.** Другой класс задачи (автозапуск, своя учётка); для удалённых
|
|
130
|
+
IIS-стендов вопрос остаётся открытым в dev-репо (GAP-282, §3.1 записки).
|