← к элементу · к схеме
Гайд Архитектора h2_shared
| Продукт | h2_shared |
| Контур | dev |
| Тип документа | Гайд Архитектора |
| Координаты чтения | {"op": "get_document", "env": "dev", "product": "h2_shared", "doc_type": "Гайд Архитектора", "latest": true} |
| Версия | v1.2 |
| SHA-256 | 7beeed52bdc781473b88f73f5f18148c9301ecae24f75c8805dc4676146d7a45
сверено
|
| Размер | 37925 байт |
Полный текст
# H2_SHARED ARCHITECT GUIDE v1.2
> **Scope.** Гайд для ИИ-архитектора H2, который принимает новый сервис Ring 3.
> Тема — **как правильно построить listener + MSSN в каноне h2_shared**, чтобы
> сервис был чёрным ящиком для рантайма и получил бесплатно все инварианты платформы:
> observability, evidence, secrets, wheel binding, request/reply, MSSN spawn/reap.
>
> Владелец нормы: Онтология h2_shared §5.3 — ADB `{"op":"get_document","env":"dev","product":"h2_shared","doc_type":"Онтология","latest":true}`;
> Гайд Пользователя h2_shared §4.8, §9 — ADB `{"op":"get_document","env":"prod","product":"h2_shared","doc_type":"Гайд Пользователя","latest":true}`.
> Настоящий документ **не переопределяет** эти каноны, а
> собирает их в цельный операционный маршрут для роли «архитектора сервиса»
> (Ring 3 boundary), давая единый источник правды на вопрос «а как это сделать
> руками, шаг за шагом, если у меня чистый лист».
---
## 0. Резюме на 30 секунд
- **Не пишите свой listener и не пишите свой MSSN.** Оба уже есть в `h2_shared`.
- Ваш сервис — это **6 строк**: `class MyRuntime(AgentRuntime): AGENT_NAME = "<agent>"` и `def main(): MyRuntime().main()` (имена `AGENT_NAME`/`<agent>` — канон кода, не переименовываются).
- Всё остальное — упаковка (`pyproject.toml`), деплой (`bootstrap.ps1` из §4.8),
установка `h2_shared` из локального `inbox` wheel и регистрация NSSM-сервиса
с 5-переменным whitelist.
- Проверка = **не** «сервис запустился», а «через `transport.send_request` пришёл
финальный ответ MSSN, и `list_caller_events` показал следы `@observable`».
Если вы поймали себя на том, что пишете `nats.aio.client.Client`,
`subprocess.Popen`, свой `NATSClient`, свой `MssnRunner`, свой цикл
`accepted → progress → final` — **остановитесь**. Всё это уже в `h2_shared.runtime`.
Ring 3 не имеет права воспроизводить эту механику руками.
## 0a. Оркестрация и место сервиса в платформе (NEW v1.1)
Сервис Ring 3 не оркестрируется ядром. Его запускают внешние ИИ-агенты-
оркестраторы — ПерпКомп (Perplexity Computer, роль ИИ-Архитектора) и Roo Code
(VS Code, роль ИИ-Кодера) — через шлюз ACO (ai_connect_orchestrator) как
готовую тулзу. Контракт запуска — карточка микротулы: `run_mode=plain_run`,
`orchestrator_wrapper=false` (Онтология ACO §7). Reasoning-цикл ядра
(`Orchestrator.run`) для сервисов Ring 3 запрещён — регрессия 2026-09-12
(L1 Концепт ACO §0: цикл возвращал константу `"echo"` вместо ответа).
Этот гайд описывает устройство самого сервиса; постановку задач и планирование
выполняет оркестратор за ACO.
---
## 0b. Подготовка нового узла H2: зоны ответственности (NEW v1.2)
Когда Оператор ставит продукты h2_shared на новый узел, подготовку узла ведёт
ИИ-Архитектор h2_shared. Его зона — всё, кроме VS Code, его расширений и
установки самих мостовых продуктов:
1. **Структура каталогов** — `C:\H2` (wheels, deps, venv, bridge_work,
incoming, nssm, evidence) по зеркалу узла-донора. Переносы между узлами —
двумя последовательными PSSession без двойного хода, с SHA-сверкой на
обоих концах.
2. **Установка ядра и инструментов** — offline из локальных wheel-байтов,
non-editable, по ЖЦ h2_shared (prod-запись wheel → prod deploy → UEPR
узла). В prod-контур ставятся только инструменты, входящие в prod-контур
донора (например, `test_guide` для node-port Live Scenario). Валидаторы
(`ai_validation`, `ai_h2_shared_validation`) в prod не ставятся: их
контур — валидация dev-релизов.
3. **NSSM-бинарь** — перенос с узла-донора, SHA-сверка, размещение в
`C:\H2\nssm`.
4. **node.json** — создание по канону Онтологии §K.4: собственные node_id,
config_id, config_revision и integrity-хэш (канонический payload SHA-256
вычисляется публичным API установленного пакета), атомарная публикация в
`C:\ProgramData\H2\config\node.json`, проверка
`load_node_config`/`validate_node_config`. Копирование файла другого узла
и шаблоны с готовыми значениями запрещены.
5. **Секреты** — проверка наличия (имена файлов/ключей, без чтения значений)
и фиксация инструкции по заведению недостающих. Значения создаёт и
размещает только Оператор; они не проходят через агентов и не печатаются.
Вне зоны ИИ-Архитектора h2_shared: VS Code и его расширения (Roo Code,
локальные bridge-расширения), установка самих мостовых продуктов
(`owui_roo_bridge`, `owui_perl_bridge`) — архитекторы мостов; значения
секретов — Оператор.
---
## 1. Модель runtime: почему listener + MSSN, а не «один процесс»
### 1.1 Разделение ролей
H2 разделяет **приёмку** и **исполнение** на два разных процесса:
- **Listener** — долгоживущий NSSM-сервис `h2-<agent>`. Единственная его задача:
подписаться на канонический subject, валидировать входящее сообщение,
спавнить MSSN, дождаться его ответа, вернуть ответ клиенту. Listener **не
исполняет бизнес-логику** — он оркеструет.
- **MSSN** — короткоживущий NSSM-сервис `H2_<AGENT>_dyn_<hash>`, который делает
ровно один запрос и завершается. Env, аргументы, лог-роутинг — всё
инъектится spawner'ом. MSSN **не подписывается на публичный subject**, он
публикует ровно один ответ на приватный `H2_INTERNAL_REPLY` subject.
### 1.2 Почему именно так
- **Изоляция сбоев.** Падение MSSN не убивает listener; listener рестартует
под nssm-supervision независимо.
- **Конкурентность без ГИЛ-накладок.** Listener шлёт `asyncio.create_task` на
каждый запрос и параллельно спавнит N MSSN — они реально работают в
разных процессах.
- **Boundary для observability.** MSSN живёт ровно один цикл — evidence,
`h2_mssn_runs`, `h2_logs` собираются атомарно, без «мусора» от предыдущих
запросов.
- **Fail-fast с evidence.** Если MSSN упал, `AppExit=Exit` фиксирует падение
в `nssm_stderr.log`, а reaper listener'а зачищает сервис через
`stop_poll / remove_poll / verify_absent`.
### 1.3 Что делает `AgentRuntime` за вас
`h2_shared.runtime.agent_runtime.AgentRuntime` — единственный класс, который
вы наследуете. Он реализует:
1. **Bootstrap listener'а** (`--mode=listener`): подключение к NATS, чтение
`HOST_REGISTRY.yaml` из пакета, регистрация в `agents_registry`,
`nc.subscribe(resolve_request_subject(agent), cb=_on_request)`.
**Без queue-group** — по канону на один хост подписан ровно один listener.
2. **Concurrent обработка**: `_on_request` → `asyncio.create_task(_process_one_request(msg))`.
3. **Request lifecycle**: валидация через `AppRouter.validate`, подписка на
`mssn_reply_subject`, `router.create_reply_future(corr_id, timeout=1800)`,
`MssnSpawner.spawn(...)`, `await reply_fut.wait()`, `nc.publish(reply_subject, response_json)`.
4. **Bootstrap MSSN** (`--mode=mssn`): чтение `H2_PAYLOAD_JSON` из env,
исполнение микротулзы через `AppRouter.dispatch`, публикация ответа на
`H2_INTERNAL_REPLY`, `sys.exit(0)`.
5. **Dry-run режимы**: `--mode=dry-run-full` (полный цикл без NSSM,
inline-транспорт), `--mode=dry-run-light` (только регистрация без реального
NATS).
Вы **не пишете ни одну из этих функций**. Вы только объявляете имя сервиса
и (опционально) реестр микротулз.
---
## 2. Канонический скелет сервиса
### 2.1 Структура файлов
```
D:\<agent>\
├─ bootstrap.ps1 ← точная копия §4.8 template
├─ <agent>_code\
│ ├─ pyproject.toml ← 12 строк (см. §2.3)
│ ├─ inbox\
│ │ └─ h2_shared-0.56.79-py3-none-any.whl ← ставится offline
│ ├─ bootstrap_venv\ ← создаётся bootstrap.ps1
│ └─ src\<agent>\
│ ├─ __init__.py ← пустой
│ ├─ __version__.py ← __version__ = "0.2.0"
│ └─ main.py ← 6 строк (см. §2.2)
└─ <agent>_data\
├─ evidence\ ← H2_EVIDENCE_DIR
└─ logs\ ← nssm_stdout.log, nssm_stderr.log
```
**Важно.** Не создавайте вложенную копию `h2_shared` внутри `src/<agent>/`.
Норма §2.1 STANDARD_WORKSPACE_HYGIENE v3.26: «установленный wheel-пакет;
исходники внутрь сервиса не копируются» (R-NO-VENDOR-IN-CODE).
### 2.2 `src/<agent>/main.py` — весь сервис (путь — канон кода)
```python
"""Ring 3 canonical agent skeleton. §9 H2_SHARED_AGENT_CODER_GUIDE v1.7.7."""
from h2_shared import AgentRuntime
class MyRuntime(AgentRuntime):
AGENT_NAME = "<agent_name_lower_snake>"
def main() -> None:
MyRuntime().main()
if __name__ == "__main__":
main()
```
Всё. Если хотите зарегистрировать микротулзы — добавьте класс-реестр по §5:
```python
from h2_shared import AgentRuntime, MicrotoolBase, MicrotoolResult
class Ping(MicrotoolBase):
NAME = "ping"
async def run(self, payload: dict) -> MicrotoolResult:
return MicrotoolResult.ok({"echo": payload.get("text", "")})
class MyRuntime(AgentRuntime):
AGENT_NAME = "my_agent"
MICROTOOLS = [Ping]
```
Никакого явного `AppRouter`, никакого явного `NATSClient` — runtime
подписывает `MICROTOOLS` в свой `AppRouter` автоматически.
### 2.3 `pyproject.toml`
```toml
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "<agent>"
version = "0.2.0"
requires-python = ">=3.11"
dependencies = ["h2_shared>=0.56.2,<1.0"]
[project.scripts]
<agent> = "<agent>.main:main"
[tool.setuptools.packages.find]
where = ["src"]
```
Никаких `nats-py`, `mariadb`, `pyyaml`, `httpx` в `dependencies` — они
транзитивно приходят с `h2_shared`. Если добавляете свои — только те, что
не покрыты рантаймом.
---
## 3. Bootstrap NSSM: точный маршрут установки
### 3.1 Копирование wheel в inbox
`h2_shared` **устанавливается только из локального inbox**, никогда не из
интернета/private PyPI. Wheel лежит в `<agent>\<agent>_code\inbox\`.
- Копирование wheel — задача deploy-скрипта (например, `roo_deploy_<agent>.ps1`).
- Источник wheel — `D:\<reference_agent>\<reference_agent>_code\inbox\` любого
уже работающего Ring 3 сервиса на этом хосте (например, `test_guide`,
`ai_validation` — спец-исполнители). Все сервисы на одном хосте живут на одной версии
`h2_shared`.
- Sha256 сверяется с manifest'ом, зарегистрированным в ADB (`list_wheels`).
### 3.2 `bootstrap.ps1` — точная копия §4.8
```powershell
# D:\<agent>\bootstrap.ps1
# Ring 3 canonical bootstrap. Требуется UAC-подъём (нужно оператору).
param(
[string]$Agent = "<agent>"
)
$ErrorActionPreference = 'Stop'
$SVC = "h2-$Agent"
$BASE = "D:\$Agent"
$VENV = "$BASE\${Agent}_code\bootstrap_venv"
$PY = "$VENV\Scripts\python.exe"
$APPPARAMS = "-m h2_shared.runtime.agent_runtime --mode=listener --agent=$Agent"
$LOGDIR = "$BASE\${Agent}_data\logs"
$STDOUT_LOG = "$LOGDIR\nssm_stdout.log"
$STDERR_LOG = "$LOGDIR\nssm_stderr.log"
# Ring 3 zero-knowledge про инфру (§S п.5, §F.N+4).
# NATS_SERVERS, H2_DOMAIN, MariaDB DSN — НЕ в env, НЕ в bootstrap.
# h2_shared.runtime.AgentRuntime сам читает HOST_REGISTRY.yaml из пакета (§K.2).
$AGENT_ROOT = $BASE
$AGENT_CODE = "$BASE\${Agent}_code"
$EVIDENCE_DIR = "$BASE\${Agent}_data\evidence"
$APPENV = ":H2_AGENT=$Agent PYTHONPATH=$AGENT_CODE AGENT_ROOT=$AGENT_ROOT H2_EVIDENCE_DIR=$EVIDENCE_DIR H2_SECRETS_FILE=$env:H2_SECRETS_FILE"
# Prep
New-Item -ItemType Directory -Path $LOGDIR -Force | Out-Null
New-Item -ItemType Directory -Path $EVIDENCE_DIR -Force | Out-Null
# venv + install (offline из inbox)
if (-not (Test-Path "$PY")) {
& python -m venv $VENV
}
& $PY -m pip install --upgrade pip
& $PY -m pip install --no-deps --no-index --find-links="$AGENT_CODE\inbox" h2_shared
& $PY -m pip install nats-py>=2.7 mariadb pydantic>=2.0 pyyaml tzdata httpx
& $PY -m pip install -e $AGENT_CODE
# NSSM install/config
$nssm = "C:\Windows\System32\nssm.exe"
& $nssm install $SVC $PY
& $nssm set $SVC AppParameters $APPPARAMS
& $nssm set $SVC AppDirectory "$VENV\Scripts"
& $nssm set $SVC ObjectName LocalSystem
& $nssm set $SVC AppExit Default Exit # fail-fast (§5.3 п.10 онтологии)
& $nssm set $SVC AppRestartDelay 5000
& $nssm set $SVC AppStdout $STDOUT_LOG
& $nssm set $SVC AppStderr $STDERR_LOG
& $nssm set $SVC AppEnvironmentExtra $APPENV # replace-mode ":" — whitelist 5 переменных
& $nssm start $SVC
# Verify
& $nssm status $SVC
```
### 3.3 Инварианты, которые НЕЛЬЗЯ нарушать
1. **Имя сервиса — kebab-case** `h2-<agent>`. Никаких `H2_HEAVY02_<AGENT>_SVC`,
никакого upper-case, никаких дефисов внутри имени сервиса. Владелец нормы:
§5.3 п.5 Онтологии.
2. **Whitelist ровно 5 переменных**: `H2_AGENT`, `PYTHONPATH`, `AGENT_ROOT`,
`H2_EVIDENCE_DIR`, `H2_SECRETS_FILE`. Всё остальное — `REJECT_FRAUD`.
Владелец нормы: §7.3 v1.7.1.
3. **NATS_URL, MariaDB DSN, H2_DOMAIN в env НЕТ.** `h2_shared` читает
`HOST_REGISTRY.yaml` из своего же пакета (`_resolve_nats_url()`).
Ring 3 не знает адрес брокера. Владелец нормы: §S п.5.
4. **`AppEnvironmentExtra` — одна строка**, ведущее `:` (replace-mode),
пробельные разделители пар. Никаких `+KEY=`, никаких переносов, никакого
`REG_MULTI_SZ` в argv (иначе секреты не попадают в env — B10, ai-validation).
5. **`AppExit=Exit`** — fail-fast. Не `Restart`. Acceptance должен увидеть
один stderr, не бесконечный цикл.
6. **`AppDirectory = venv\Scripts`** — иначе Windows не находит DLL зависимостей.
7. **`H2_SECRETS_FILE` — только имя переменной.** Значение, путь, содержимое
файла — забота Ring 0 (`H2_SHARED_INTERNALS_OPERATIONS.md`). Ring 3 знает
только имя переменной.
---
## 4. Как runtime обрабатывает запрос: полный цикл
Понимание цикла нужно, чтобы вы могли отладить сервис, не заглядывая в
`agent_runtime.py`. Ниже — модель поведения, которую вы наблюдаете снаружи.
### 4.1 Listener boot
```
nssm start h2-<agent>
→ python -m h2_shared.runtime.agent_runtime --mode=listener --agent=<agent>
→ AgentRuntime().main()
→ _resolve_nats_url() из HOST_REGISTRY.yaml
→ nc.connect(...)
→ upsert_agent_registry(agent, card, live_scenario_path, is_active=True)
→ subject = resolve_request_subject("<agent>") # e.g. "h2.<agent>.request"
→ nc.subscribe(subject, cb=_on_request) # БЕЗ queue-group
→ await forever
```
Признак «listener жив»: `nssm status h2-<agent> == SERVICE_RUNNING` +
успешный `python -m h2_shared.probe readiness --agent <agent> --timeout 30 --json`.
Не «сервис запустился» — `probe readiness` реально шлёт NATS-запрос и ждёт
ответ.
### 4.2 Приём запроса
```
клиент: transport.send_request(target_agent="<agent>", payload={...}, timeout=1800)
→ nc.request("h2.<agent>.request", payload_json, timeout=1800)
listener._on_request(msg):
→ asyncio.create_task(_process_one_request(msg)) # немедленный ACK транспорту
→ return # NEXT запрос принимается сразу, конкурентная обработка
_process_one_request(msg):
→ AppRouter.validate(payload) # microtool exists? schema ok?
→ corr_id = payload["correlation_id"]
→ reply_subject = msg.reply
→ mssn_reply_subject = f"h2.mssn.{corr_id}.reply"
→ nc.subscribe(mssn_reply_subject, cb=router.on_mssn_reply)
→ reply_fut = router.create_reply_future(corr_id, timeout=1800)
→ MssnSpawner.spawn(agent="<agent>", corr_id=corr_id, payload=payload,
reply_subject=mssn_reply_subject)
→ response = await reply_fut.wait() # блокируется на этом task-е
→ nc.publish(reply_subject, response_json)
→ nc.unsubscribe(mssn_reply_subject)
```
**Здесь НЕТ отдельного `accepted` пакета.** Если вы видели у себя в коде
«сначала publish accepted, потом final» — это самопал, канон так не делает.
Финальный ответ приходит клиенту один раз, когда MSSN отработал.
### 4.3 MssnSpawner: как поднимается MSSN
```
MssnSpawner.spawn(agent, corr_id, payload, reply_subject):
→ mssn_id = f"H2_{agent}_dyn_{sha256(corr_id)[:16]}"
→ env для MSSN — whitelist:
H2_AGENT = <agent>
H2_CORR_ID = <corr_id>
H2_REPLY_SUBJECT = <mssn_reply_subject>
H2_PAYLOAD_JSON = base64(payload_json)
H2_INTERNAL_REPLY = <mssn_reply_subject> # дублируется для совместимости
H2_MSSN_ID = <mssn_id>
PYTHONUNBUFFERED = 1
+ inherit: H2_SECRETS_FILE, PYTHONPATH, AGENT_ROOT, H2_EVIDENCE_DIR
→ NSSM install <mssn_id> python -m h2_shared.runtime.agent_runtime --mode=mssn
→ NSSM set AppEnvironmentExtra <whitelist из 9 переменных>
→ NSSM set AppExit=Exit
→ NSSM start <mssn_id>
```
**НИКАКОГО `subprocess.Popen`.** Норма R-SPAWN-05: все MSSN — через `nssm install`
+ `nssm start`. Если вы видите у себя `Popen` — это регрессия, MSSN не запишется
в SCM и reaper не сможет его прибрать.
### 4.4 MSSN lifecycle
```
NSSM запускает: python -m h2_shared.runtime.agent_runtime --mode=mssn
_run_mssn():
→ payload = base64_decode(os.environ["H2_PAYLOAD_JSON"])
→ agent = os.environ["H2_AGENT"]
→ nc.connect(...) # берёт HOST_REGISTRY из пакета
→ response = await AppRouter.dispatch(agent, payload)
→ nc.publish(os.environ["H2_INTERNAL_REPLY"], response_json)
→ await nc.flush()
→ sys.exit(0)
listener видит publish → reply_fut.set_result(response)
→ _process_one_request продолжается, отвечает клиенту
reaper (внутри listener, отдельная корутина):
→ каждые N секунд: for mssn in list_dyn_mssn(agent):
→ if not is_running(mssn):
→ nssm remove <mssn> confirm
→ verify_absent(<mssn>, timeout=10)
```
Признак «MSSN отработал корректно»:
- строка в `h2_mssn_runs` со `status=OK`, `corr_id=<corr_id>`, `exit_code=0`
- запись в `h2_logs` с `caller_product=<agent>`, `caller_event=mssn_completed`
- файл `reply.json` в `H2_EVIDENCE_DIR/<mssn_id>/`
- сервис `H2_<AGENT>_dyn_<hash>` **отсутствует** в SCM (reaper убрал)
---
## 5. Микротулзы: контракт
### 5.1 Минимальный класс
```python
from h2_shared import MicrotoolBase, MicrotoolResult
class ProbeMicrotool(MicrotoolBase):
NAME = "system.ping" # публичное имя в payload["op"]
INPUT_SCHEMA = { # JSON Schema (draft 2020-12)
"type": "object",
"properties": {"echo": {"type": "string"}},
"required": ["echo"],
}
OUTPUT_SCHEMA = {
"type": "object",
"properties": {"echo": {"type": "string"}, "server_time": {"type": "string"}},
"required": ["echo", "server_time"],
}
async def run(self, payload: dict) -> MicrotoolResult:
import datetime as dt
return MicrotoolResult.ok({
"echo": payload["echo"],
"server_time": dt.datetime.utcnow().isoformat(timespec="seconds") + "Z",
})
```
### 5.2 Что даёт `@observable` (автоматически)
`MicrotoolBase.run` уже обёрнут в `h2_shared.observability.observable_async`.
Каждый вызов пишет:
- `h2_logs`: `caller_product=<agent>`, `caller_event=microtool.<NAME>.start` / `.end`
- `h2_mssn_runs`: `mssn_id`, `corr_id`, `microtool=<NAME>`, `latency_ms`, `status`
- `h2_findings`: только при `MicrotoolResult.error(...)` или необработанном исключении
Проверка «observability работает»: `list_caller_events(caller_product="<agent>", last=10)`
возвращает N событий. Если пусто — либо `h2_shared` установлен неправильно
(например, копия внутри `src/<agent>/`), либо микротулза вызывается в обход
`MicrotoolBase.run`.
### 5.3 Секреты внутри микротулзы
Не читайте `os.environ` напрямую. Не читайте `H2_SECRETS_FILE` напрямую.
Используйте:
```python
from h2_shared import secrets
api_key = await secrets.get("DEEPSEEK_API_KEY") # резолвит из H2_SECRETS_FILE
```
`secrets` умеет: file-based (`H2_SECRETS_FILE`), CredStore (Ring 0 только),
in-memory fallback для dry-run. Ring 3 знает только имя ключа, не путь и не
формат хранилища.
---
## 6. Live-run приёмка: обязательный §9 шаблон
Владелец нормы: §9.1 `H2_SHARED_AGENT_CODER_GUIDE v1.7.7`. Ниже — операционный
чек-лист, который выполняется **после** deploy и до заявки VERDICT=GREEN.
### 6.1 Preflight P-0..P-9
Один PS-скрипт, 10 проверок, результат — `preflight.txt`.
| # | Что проверяет | Механика |
|---|---------------|----------|
| P-0 | 7 python-deps | `pip show nats-py aiohttp mariadb cryptography msgpack orjson pynacl` |
| P-1 | python 3.11+ + PS >= 5 | `$PSVersionTable.PSVersion.Major -ge 5` |
| P-2 | h2_shared wheel sha256 | `pip show h2_shared` + сравнение с MANIFEST.json в inbox |
| P-3 | wheel bundle цел | `Test-Path inbox\h2_shared-*.whl` + `Get-FileHash` |
| P-4 | mssn baseline | SQL: `SELECT COUNT(*) FROM h2_mssn_runs WHERE agent=<a>` |
| P-5 | nssm.exe в PATH, сервиса нет | `& nssm status h2-<a>` == error (до install) |
| P-6 | CLI help работает | `python -m h2_shared.runtime.agent_runtime --help` |
| P-7 | secrets резолвятся | `python -m h2_shared.cli.evidence query --agent <a> --last 1 --json` |
| P-8 | NATS reachable | `python -m h2_shared.cli.probe nats --timeout 3` |
| P-9 | evidence-dir пуст | нет stray-файлов от прошлых прогонов |
Все — exit 0. Результат: `preflight.txt` со строками `P-N: PASS — <факт>` и
финальной строкой `RESULT: 10/10 GREEN`.
### 6.2 NSSM install (уже описан в §3.2)
### 6.3 Listener readiness (обязательный gate)
```powershell
$readiness = & $VenvPy -m h2_shared.probe readiness --agent $Agent --timeout 30 --json
$readiness | Tee-Object "$Evidence\listener_check.txt"
if ($LASTEXITCODE -ne 0) { throw "Listener not ready: $readiness" }
```
`h2_shared.probe readiness` не смотрит на `nssm status` — он реально шлёт NATS-
запрос `probe.ping` и ждёт ответ. Это единственный корректный сигнал
«listener жив и подписан на канонический subject».
### 6.4 Живой request/reply
```powershell
$requestJson = @{
request_text = "full run"
correlation_id = "live-run-$(Get-Date -Format 'yyyyMMddHHmmss')"
instance_id = "live-run-$(Get-Date -Format 'yyyyMMddHHmmss')"
} | ConvertTo-Json -Compress
$reply = & $VenvPy -c @"
import asyncio, json
from h2_shared.runtime.transport import send_request
async def main():
payload = json.loads(r'$requestJson')
result = await send_request(target_agent='$Agent', payload=payload, timeout=1800)
print(json.dumps(result, indent=2, ensure_ascii=False))
asyncio.run(main())
"@
$reply | Set-Content -Encoding UTF8 "$Evidence\nats_req_capture.txt"
```
Использовать **только** `h2_shared.runtime.transport.send_request` — это
канонический Ring 3 инжект. `nats.aio.client.Client` напрямую — REJECT_FRAUD.
### 6.5 Evidence check
- `h2_mssn_runs`: минимум 1 строка со `status=OK` для нашего `corr_id`.
- `h2_logs`: `list_caller_events(caller_product="<agent>", last=20)` содержит
события `microtool.*.start` / `.end`.
- `H2_EVIDENCE_DIR/<mssn_id>/reply.json` существует.
- `nssm_stderr.log` пустой (или содержит только ожидаемые warnings).
### 6.6 Teardown (обязательно, чтобы приёмка была повторяемой)
```powershell
& $nssm stop $SVC
& $nssm remove $SVC confirm
Remove-Item -Recurse -Force $VENV
```
`register_validation_run` в ADB закрывает live-run с полным манифестом
evidence-файлов и sha256 wheel.
---
## 7. Dry-run режимы (быстрая обратная связь без NSSM)
С `h2_shared 0.56.13` `AgentRuntime` поддерживает два dry-run режима.
### 7.1 `--mode=dry-run-full`
Полный цикл `_serve_one_request` без NSSM: тот же код listener+MSSN, но
транспорт inline (без NATS-hop). Годится для:
- быстрой проверки микротулз;
- CI-подобных прогонов;
- диагностики: если dry-run-full GREEN, а NSSM listener RED — проблема в
NSSM-конфиге (env whitelist, AppExit, log routing), не в коде сервиса.
```powershell
& $VenvPy -m h2_shared.runtime.agent_runtime `
--mode=dry-run-full `
--agent=$Agent `
--json 2>&1 | Tee-Object "$Evidence\dry_run_full.json"
```
Не заменяет §9.1–§9.5 для Acceptance.
### 7.2 `--mode=dry-run-light`
Только регистрация в `agents_registry` без реального NATS-соединения. Для
самой первой проверки «код запускается вообще».
---
## 8. Типовые ошибки Ring 3 и как их не сделать
| Симптом | Причина | Что делать |
|---------|---------|-----------|
| `list_caller_events` пустой при живом listener | `h2_shared` скопирован в `src/<agent>/vendor/`, реальный import идёт из копии, а `@observable` из копии не пишет в audit-DB | Убрать копию, ставить только из inbox wheel |
| MSSN не стартует, в `nssm_stderr.log` `KeyError: 'H2_PAYLOAD_JSON'` | В `AppEnvironmentExtra` попал `REG_MULTI_SZ` (пять argv вместо одной строки) | Использовать формат из §3.2: одна строка, ведущее `:`, пробелы |
| Listener принимает запрос, ответа нет 1800s | Своя `nc.request(...)` в микротулзе с тем же subject, что и listener — эхо-цикл | Микротулза не должна шлать на `h2.<agent>.request`; для side-effect calls использовать `transport.send_request` с другим `target_agent` |
| `probe readiness` OK, `send_request` timeout | Микротулза не зарегистрирована в `MICROTOOLS` или её `NAME` не совпадает с `payload["op"]` | Проверить `AppRouter.list_microtools(<agent>)` через `python -m h2_shared.cli.probe list-microtools --agent <a>` |
| MSSN зависают в SCM, копятся сотни `H2_<A>_dyn_*` | Reaper упал (обычно из-за исключения в цикле) или listener не рестартнули | `Get-Service H2_<A>_dyn_* | Remove-Service`, рестартнуть `h2-<a>` |
| `h2_shared_available=false` в собственных логах сервиса | Своя проверка через `importlib.util.find_spec("h2_shared")` в venv, где wheel не установлен | Не проверяйте вручную — если процесс поднялся с `AgentRuntime`, значит h2_shared доступен |
| Наличие своего `NATSClient` / `MssnRunner` / `Listener` | Ring 3 воспроизвёл runtime руками | Выкинуть, наследовать `AgentRuntime` |
| `NATS_URL` в `AppEnvironmentExtra` | v1.7.1 whitelist нарушен | Убрать; `_resolve_nats_url()` читает `HOST_REGISTRY.yaml` из пакета |
---
## 9. Deploy на удалённый хост через Roo (референс)
Ring 3 работает под LocalSystem через NSSM. Оператор не имеет прямого доступа
к учётке. Канонический deploy-скрипт:
1. `scp <agent>_v0.2.tar.gz <node>:D:\_deploy\`
2. Roo dispatch на `<node>` с prompt:
- остановить и удалить старый сервис (если был);
- распаковать tarball в `D:\<agent>\<agent>_code\`;
- скопировать `h2_shared-<ver>.whl` из `D:\<reference>\<reference>_code\inbox\`
в `D:\<agent>\<agent>_code\inbox\`;
- запустить `bootstrap.ps1 -Agent <agent>` (self-elevate);
- вернуть `nssm status h2-<agent>`.
3. Проверить через ADB `register_deploy` и `product_readiness_report`.
Референс-скрипты для копирования: `roo_deploy_test_guide.ps1`,
`roo_deploy_ai_validation.ps1`. Ключевые различия между сервисами — только
имя `$Agent`.
---
## 10. Что должно быть в MR/PR ревью нового сервиса
Ревьюер проверяет по чек-листу:
- [ ] `src/<agent>/main.py` — 6–10 строк, наследует `AgentRuntime`.
- [ ] Нет своего listener'а/spawner'а/NATS-клиента.
- [ ] Нет копии `h2_shared` в `src/`.
- [ ] `pyproject.toml`: только `h2_shared` в `dependencies` (плюс явные бизнес-зависимости).
- [ ] `bootstrap.ps1` — байт-в-байт §4.8, разница только в `$Agent`.
- [ ] Whitelist ровно 5 переменных.
- [ ] `AppExit=Exit`, не `Restart`.
- [ ] Имя сервиса `h2-<agent>` (kebab).
- [ ] `inbox\` содержит один `h2_shared-*.whl`, sha256 совпадает с
зарегистрированным в ADB (`list_wheels`).
- [ ] Live-run отчёт §9 с `probe readiness` OK + `send_request` OK +
`list_caller_events` > 0 + `h2_mssn_runs` OK.
- [ ] `register_validation_run` в ADB с полным манифестом.
Если хотя бы один пункт не выполнен — MR не принимается.
---
## 11. Ссылки на нормативные документы
- `H2_SHARED_AGENT_CODER_GUIDE v1.7.7` (id=13) — Ring 3 boundary contract.
- `H2_SHARED_ONTOLOGY v1.7.9` (id=14) — §5.3 сервисный контракт, §9.3 NSSM canon.
- `LIVE_SCENARIO_H2_SHARED_STAGE_2B3 v1` (id=15) — 10-стадий приёмка runtime.
- `ROADMAP_H2_SHARED_ROLE_CORRECTED v15` (id=16) — дорожная карта.
- `STANDARD_WORKSPACE_HYGIENE v3.26` — §2.1 R-NO-VENDOR-IN-CODE.
- `LIVE_SCENARIO_AI_VALIDATION v4.0.1` — референс §9 LIVE-RUN для валидирующего Ring 3.
---
## 12. Changelog
- **v1.2 (2026-09-29)** — по решению Оператора добавлен §0b «Подготовка нового
узла H2: зоны ответственности»: провижн узла (каталоги, колёса, offline-установка,
NSSM, node.json, проверка секретов) — зона ИИ-Архитектора h2_shared; VS Code,
расширения и установка мостовых продуктов — архитекторы мостов; значения
секретов — Оператор. Основание — подготовка узла heavy01 (deploy 205) и
коррекция Оператора от 29.09.2026 о запрете валидаторов в prod-контуре.
- **v1.1 (2026-09-25)** — патч легаси по директиве Оператора: терминология
сервисного слоя («агент» → «сервис Ring 3» в прозе, 16 мест; имена кода —
`AGENT_NAME`, `<agent>`, пути — не тронуты); добавлен §0a «Оркестрация и место
сервиса» (внешние оркестраторы ПерпКомп/Roo Code за шлюзом ACO,
`run_mode=plain_run`, запрет reasoning-цикла для сервисов — регрессия
2026-09-12); владелец нормы — координаты ADB вместо файловых версий.
- **v1.0 (2026-09-12)** — первая версия. Собран цельный операционный маршрут
для роли ИИ-архитектора при принятии нового Ring 3 агента: модель
runtime (§1), скелет агента (§2), bootstrap NSSM (§3), полный цикл
request/reply (§4), контракт микротулз (§5), live-run приёмка (§6),
dry-run режимы (§7), типовые ошибки (§8), deploy маршрут (§9), MR ревью
чек-лист (§10). Основание — Coder Guide v1.7.7 §4.8/§9, Ontology v1.7.9
§5.3, разбор канонического `test_guide` на LAPTOP (session
`01a09689-1183-735e-ba7d-18376f7bd6e3`).