architect(ET): auto-commit from architect run_id=158
All checks were successful
CI / test (push) Successful in 13s

This commit is contained in:
2026-06-06 07:57:07 +00:00
parent 4488a87404
commit 8fb59cd87f
5 changed files with 260 additions and 2 deletions

View File

@@ -0,0 +1,168 @@
# ADR-001: Token-free auth-preflight + «пустой результат = провал» в запуске агента
**Work Item:** ORCH-044
**Статус:** Accepted
**Дата:** 2026-06-06
**Автор:** Architect
> ⛔ **Scope (коррекция владельца, 06.06):** `--effort` (P2) **исключён** из ORCH-044 и
> вынесен в **ORCH-50**. Этот ADR покрывает только **P1** (preflight ловит авторизацию)
> и **P3** (пустой лог / нет result-JSON ⇒ job `failed`). Любые решения по effort,
> дефолтам `agent_effort_*` и ORCH-41 effort-докам — **вне этого ADR**.
---
## Контекст
Инцидент 05.06 (ORCH-17): аналитик-агент стартовал и мгновенно «умирал» — run-лог пустой
(0 байт), job в очереди завис в `running`. Две наложившиеся причины: (1) `claude Not logged
in` после ребилда контейнера; (2) `--effort` гасил stdout. **Системная проблема:**
preflight пропустил заведомо нерабочую задачу в работу, а пустой результат был неотличим
от успеха. Поскольку инстанс общий для всех проектов (self-hosting, общая очередь/БД),
тихое зависание блокирует конвейер **всех** проектов.
Текущее состояние слоя запуска:
- `src/preflight.py` проверяет только `os.path.exists(bin)` и `claude --version`. `--version`
отвечает успешно **даже когда claude не залогинен** (версия — локальная информация) ⇒
preflight слеп к авторизации.
- `src/agents/launcher.py::_monitor_agent` трактует `exit_code == 0` как успех **независимо
от формы stdout** (комментарий в `_spawn`, стр. 302) ⇒ пустой лог + exit 0 → `done` +
авто-advance стадии.
Ограничения (BR-1): preflight обязан быть **локальным и token-free** — никакого prompt-ping
и сетевых вызовов к API модели.
## Решение
### P1 — Preflight ловит авторизацию (комбинация проактивной и постфактум-проверок)
Реализуем **оба** подхода из TR-1.2 (a + b), проактивный — основной гейт, постфактум —
защитная сетка.
**(a) Проактивно — чтение файла учётных данных (основной гейт).**
`preflight._compute()` после успешного `--version` выполняет `_check_auth()`:
1. Резолвит путь к credentials **согласованно с HOME, под которым launcher реально спавнит
claude** (`/home/slin`), а НЕ из окружения процесса оркестратора. Реализуется зеркально
`_claude_bin()`: новый `_agent_home()` читает `AgentLauncher.AGENT_HOME` (новая константа,
значение `/home/slin`), путь = `settings.claude_credentials_path` если задан, иначе
`<AGENT_HOME>/.claude/.credentials.json`.
2. Файла нет / нечитаем / невалидный JSON ⇒ `(False, "claude not logged in: credentials …")`.
3. Нет блока `claudeAiOauth` / accessToken ⇒ `(False, "not logged in: no oauth token")`.
4. `claudeAiOauth.expiresAt` (epoch **ms**) `<= now_ms (+ skew)`
`(False, "OAuth token expired at <iso>")`.
5. accessToken есть, но `expiresAt` отсутствует/не число ⇒ **OK** (нельзя доказать истечение;
не плодим ложные срабатывания — см. Риски).
6. Иначе ⇒ `(True, "auth ok")`.
`_check_auth()` **никогда не бросает**: любое исключение → `(False, "auth check error: …")`
(fail-safe в сторону «не клеймить», BR-2 / TR-3.5).
Кеширование (TR-1.4 / AC-6): чтение файла встроено в `_compute()`, который уже кешируется
`check()` на `preflight_cache_ttl`. **Отдельный кеш не вводится** — auth-чтение происходит
только на cache-miss, как и `--version`.
Гейтинг клейма (TR-1.5 / AC-4 / BR-2): **изменений в `queue_worker._drain_once` не требуется**
— он уже не клеймит job при `ok=False`. Информативный auth-reason автоматически попадает в
`worker.last_preflight_reason` и `/queue` (без изменения схемы ответа).
**(b) Постфактум — маркер `Not logged in` в run-логе (защитная сетка).**
Если агент всё-таки стартовал при протухшей сессии (гонка: токен истёк между preflight и
спавном), `launcher` при финализации детектит auth-маркер в логе
(`preflight.is_auth_failure_text(text)`: «not logged in», «please run /login»,
«unauthorized», «401») и:
- включает маркер в `error` job;
- вызывает `preflight.reset_cache()`, чтобы **следующий тик воркера переоценил auth
проактивно** (быстрый подхват re-login ИЛИ дальнейшее гейтирование, если всё ещё битый).
Auth-провал **не** маршрутизируется как transient (это не 429) и **не** крутит брейкер —
правильный механизм гейтирования здесь preflight, а не circuit breaker.
### P3 — Пустой лог / нет result-JSON ⇒ провал job
В `_monitor_agent` для ветки `exit_code == 0` вводим **валидацию результата** перед тем как
считать job успешным. Новый защитный хелпер `_validate_result(output_path) -> (ok, reason)`:
- лог отсутствует / пустой (size 0 или только whitespace) ⇒ невалиден;
- иначе извлекаем result-JSON **тем же контрактом**, что usage-учёт
(`usage._extract_last_json_object` / `parse_usage_from_text`); нет валидного объекта ⇒
невалиден;
- хелпер обёрнут try/except и **не роняет монитор**; при собственной ошибке —
fail-safe в сторону провала (TR-3.5).
`success = (exit_code == 0 and result_ok)`. Побочные эффекты успеха выполняются **только при
`success`**:
- `_post_usage_comments(...)` (успешный status-коммент) — **не** постится при невалидном
результате (AC-12);
- `_try_advance_stage(...)`**не** вызывается при невалидном результате (AC-12);
- при `exit_code == 0 and not result_ok` шлётся Telegram-алерт о «пустом/невалидном
результате».
Финализация job (`_finalize_job` получает новый флаг `result_ok`):
- `exit_code == 0 and result_ok``done` (как раньше, AC-13 — без регрессии);
- `exit_code != 0` **ИЛИ** `result_ok == False` ⇒ путь провала:
- классификация лога `error_classifier.classify_log_file` (по умолчанию **permanent**;
transient-маркер уводит в transient-путь — TR-3.3);
- permanent: `attempts < max_attempts` ⇒ requeue (`queued`), иначе `failed` + алерт;
- `error` информативен: `empty run log / no result JSON (run_id=…)` для случая пустого
результата.
Реальный `exit_code` по-прежнему пишется в `agent_runs` без искажения; на решение
done/fail влияет отдельный флаг `result_ok`, а не подменённый код выхода.
`exit_code == 0` теперь **всегда** завершается терминально/ретраябельно (`done` |
`failed` | `queued`) — путь «быстрая смерть с exit 0 → вечный running» закрыт (AC-14, BR-3).
Watchdog ORCH-7 продолжает закрывать таймауты.
### Конфигурация (config.py)
| Настройка | Env | Default | Назначение |
|-----------|-----|---------|------------|
| `preflight_check_auth` | `ORCH_PREFLIGHT_CHECK_AUTH` | `True` | Вкл/выкл auth-проверку (аварийный тумблер) |
| `claude_credentials_path` | `ORCH_CLAUDE_CREDENTIALS_PATH` | `""` | Явный путь; пусто ⇒ `<AGENT_HOME>/.claude/.credentials.json` |
| `auth_expiry_skew_seconds` | `ORCH_AUTH_EXPIRY_SKEW_SECONDS` | `0` | Запас на рассинхрон часов при сравнении `expiresAt` |
`agent_effort_*` дефолты и `--effort` в `_spawn`**не трогаем** (scope, ORCH-50).
## Альтернативы
- **A1. Prompt-ping (ping→pong) для проверки auth.** ❌ Запрещено BR-1 (жжёт rate limit,
латентность). Отвергнуто.
- **A2. Только постфактум-маркер (чистый вариант b).** Ловит auth лишь ПОСЛЕ спавна и траты
цикла; не гейтирует клейм. Оставлен как защитная сетка, но не как основной механизм.
- **A3. Сетевая валидация токена у провайдера.** Нарушает «preflight локальный» (TR-1.6),
добавляет сетевую зависимость в горячий путь воркера. Отвергнуто.
- **A4. Подменять exit_code на ненулевой при пустом результате.** Исказило бы
`agent_runs.exit_code` и классификацию. Выбрали отдельный флаг `result_ok`.
- **A5. Отдельный кеш для auth.** Избыточно — `_compute()` уже под общим TTL.
## Последствия
**Плюсы.**
- Заведомо нерабочая (не залогинен / протухший токен) задача **не клеймится** — экономия
цикла и отсутствие тихого зависания.
- Пустая «быстрая смерть» агента теперь видима: `failed`/retry + алерт вместо ложного `done`
и движения стадии вперёд по пустому результату.
- Без изменения схемы БД, без новых QG/стадий, без новых HTTP-endpoint'ов.
- Auth-reason виден в `/queue` для диагностики.
**Минусы / ограничения.**
- **Риск ложноположительного auth-fail** (см. `10-tech-risks.md` R-1): неверно
резолвленный путь к credentials заблокирует клейм **всех** проектов (общая очередь).
Митигируется: единый источник HOME (`AGENT_HOME`), тумблер `ORCH_PREFLIGHT_CHECK_AUTH`,
обязательная проверка на staging (8501) перед прод-деплоем.
- Проверка `expiresAt` — локальная; реально отозванный, но ещё не истёкший токен ловится
только постфактум-маркером (b).
- `expiresAt`-отсутствие трактуется как OK (компромисс против ложных срабатываний).
**Self-hosting.** Изменения только в слое preflight/launch; **не** требуют рестарта/падения
прод-контейнера `orchestrator` в рамках задачи. Выкатка — через staging-гейт (AC-17).
## Связи
- BRD `01-brd.md` (P1, P3), ТЗ `02-trz.md` (TR-1.x, TR-3.x; scope-коррекция),
Acceptance `03-acceptance-criteria.md` (AC-1…AC-6, AC-10…AC-17).
- Риски: `10-tech-risks.md`. Инфра: `07-infra-requirements.md`. БД: `08-data-requirements.md`.
- Код: `src/preflight.py`, `src/agents/launcher.py` (`_monitor_agent`, `_finalize_job`),
`src/config.py`, `src/usage.py` (`_extract_last_json_object`),
`src/error_classifier.py` (`classify_log_file`), `src/queue_worker.py` (без изменений).
- ORCH-1 (очередь/resilience), ORCH-7 (watchdog), ORCH-41 (resolver — **не трогаем effort**).
- **ORCH-50** — полноценный возврат `--effort` (вынесен из этой задачи).

View File

@@ -0,0 +1,46 @@
# 07 — Требования к инфраструктуре
**Work Item:** ORCH-044
**Основано на:** ADR-001, ТЗ `02-trz.md`
## Топология
**Без изменений.** Новых контейнеров, портов, сервисов, очередей не вводится. Прод
`orchestrator` (8500) и staging `orchestrator-staging` (8501) остаются как есть
(`docs/operations/INFRA.md`).
## Учётные данные claude (P1)
- Launcher спавнит claude с `HOME=/home/slin` (`src/agents/launcher.py`). Preflight ДОЛЖЕН
резолвить путь к credentials от **этого же** HOME, а не от окружения процесса оркестратора.
- Ожидаемое расположение файла OAuth-токена: **`/home/slin/.claude/.credentials.json`**
(структура: `claudeAiOauth.expiresAt` — epoch **ms**).
- Файл — секрет; в гит НЕ коммитится (правило агентов №8). На хосте монтируется в контейнер
как раньше; задача его расположение **не меняет**, только начинает читать.
- ⚠️ **Проверить на staging:** реальный путь файла внутри контейнера совпадает с
резолвленным preflight. Несовпадение ⇒ ложный auth-fail и блок очереди (R-1).
## Новые переменные окружения (env-карта)
Документировать в `docs/operations/INFRA.md` и docstring `src/config.py`:
| Env | Default | Назначение |
|-----|---------|------------|
| `ORCH_PREFLIGHT_CHECK_AUTH` | `true` | Включение token-free auth-проверки в preflight. Аварийный тумблер: `false` возвращает старое поведение (только bin + `--version`). |
| `ORCH_CLAUDE_CREDENTIALS_PATH` | `""` | Явный путь к `.credentials.json`. Пусто ⇒ `<AGENT_HOME>/.claude/.credentials.json`. |
| `ORCH_AUTH_EXPIRY_SKEW_SECONDS` | `0` | Запас на рассинхрон часов при сравнении `expiresAt`. |
`--effort` env (`ORCH_AGENT_EFFORT_*`) — **вне scope**; прод-хотфикс `ORCH_AGENT_EFFORT_*=""`
в `.env` **оставить как есть** (ORCH-50).
## Эксплуатационные процедуры
- **Аварийный откат auth-гейта без редеплоя кода:** выставить `ORCH_PREFLIGHT_CHECK_AUTH=false`
в `.env` и перезапустить воркер обычной процедурой выката (НЕ в рамках этой задачи).
- **Диагностика:** auth-причина видна в `GET /queue` (`preflight_reason`) и в warning-логе
`orchestrator.preflight`.
- **Re-login:** при детекте auth-маркера в логе launcher сбрасывает preflight-кеш, поэтому
после ручного `claude /login` следующий тик воркера (≤ `preflight_cache_ttl`) подхватит
валидную сессию автоматически.
## Self-hosting / деплой (AC-17)
- Изменения только в слое preflight/launch — **не** требуют рестарта/падения прод-контейнера
в рамках задачи.
- Выкатка self-доработки ORCH — **через staging-гейт (8501)** перед прод-деплоем
(CLAUDE.md, `docs/operations/INFRA.md`, ADR-0003).

View File

@@ -0,0 +1,23 @@
# 08 — Требования к схеме БД
**Work Item:** ORCH-044
**Основано на:** ADR-001, ТЗ `02-trz.md` §4
## Вердикт: изменений схемы НЕ требуется
Новых таблиц, колонок, индексов, миграций — **нет**.
P1 (auth-preflight) и P3 (пустой результат ⇒ провал) работают на **существующих** структурах:
- **`jobs`** — повторно используются существующие колонки для пути провала:
`status` (`queued`/`running`/`done`/`failed`), `error`, `attempts`, `max_attempts`,
`transient_attempts`, `available_at`, `run_id`. Пустой/невалидный результат идёт тем же
путём, что и обычный permanent/transient провал (`mark_job` / `mark_job_transient`).
- **`agent_runs`** — `exit_code` пишется без искажения (реальный код выхода процесса).
Решение done/fail принимается по отдельному in-memory флагу `result_ok` в `_monitor_agent`,
а не по колонке.
## Состояние данных
- Никаких бэкофиллов / data-migration.
- Auth-проверка читает **файл** `.credentials.json` (вне БД), результат кешируется in-memory
(`preflight._cache`), не персистится.

View File

@@ -0,0 +1,20 @@
# 10 — Технические риски
**Work Item:** ORCH-044
**Основано на:** ADR-001
| ID | Риск | Вероятн. | Влияние | Митигация |
|----|------|----------|---------|-----------|
| R-1 | **Ложноположительный auth-fail.** Неверно резолвленный путь к `.credentials.json` (иной HOME/маунт) ⇒ preflight всегда FAIL ⇒ **не клеймится ни одна job всех проектов** (общая очередь, self-hosting). | Средняя | **Высокое** | Единый источник HOME (`AgentLauncher.AGENT_HOME`, зеркально `_claude_bin()`); тумблер `ORCH_PREFLIGHT_CHECK_AUTH=false`; **обязательная проверка на staging** (реальный путь == резолвленный) перед прод-деплоем; информативный reason в `/queue` + warning-лог. |
| R-2 | **Fail-safe-провал на легитимном пустом выводе.** Агент легитимно завершился `exit 0` с непустым логом, но `_validate_result` ошибочно счёл результат невалидным ⇒ ложный `failed`/requeue (регрессия AC-13). | Низкая | Среднее | Контракт извлечения JSON — тот же, что у работающего usage-учёта (`_extract_last_json_object`); регресс-тест TC-15 (валидный лог ⇒ `done`); валидатор не трогает успешный путь, кроме булева флага. |
| R-3 | **`expiresAt` без сетевой валидации.** Реально отозванный, но ещё не истёкший по времени токен пройдёт проактивную проверку (a). | Средняя | Среднее | Защитная сетка постфактум (b): маркер `Not logged in` в логе ⇒ `error` + `preflight.reset_cache()` ⇒ следующий тик переоценивает auth; полная сетевая валидация — вне scope (BR-1). |
| R-4 | **`expiresAt` отсутствует/нечисловой** в файле (иная версия CLI / иной формат) ⇒ проверка трактует как OK и пропускает. | Низкая | Низкое | Осознанный компромисс против ложных срабатываний (см. ADR §P1.5); отсутствие токена/accessToken по-прежнему ⇒ FAIL; постфактум-маркер ловит реальный «не залогинен». |
| R-5 | **Часовой рассинхрон** контейнер vs токен ⇒ валидный токен сочтён истёкшим. | Низкая | Среднее | `ORCH_AUTH_EXPIRY_SKEW_SECONDS` (default 0) для запаса; контейнеры на одном хосте (mva154) — рассинхрон маловероятен. |
| R-6 | **Транзиентный auth (битый JSON в момент записи re-login).** Чтение файла во время атомарной перезаписи ⇒ временный FAIL. | Низкая | Низкое | Кеш TTL сглаживает; следующий тик перечитает; fail-safe в сторону «подождать» (job остаётся `queued`, не теряется). |
| R-7 | **Конфликт test-plan с коррекцией scope.** `04-test-plan.yaml` TC-09/TC-10/TC-11 проверяют `--effort` (variant B: «`--effort` не формируется»), но владелец **исключил** effort из ORCH-044 и оставил дефолты `agent_effort_*` = `high`. При дефолтной тест-конфигурации `_spawn` сформирует `--effort high` ⇒ TC-09 (ожидающий отсутствие флага) **упадёт**. | **Высокая** | Среднее | Developer/Tester: **адаптировать TC-09/10/11** под «effort не трогаем» (assert успешной сборки cmd без требования удаления флага, либо пометить как deferred→ORCH-50). Артефакт `04-test-plan.yaml` — чужой этап (правило №3), архитектор его НЕ редактирует, только фиксирует расхождение здесь. AC-7/AC-8/AC-9 не применяются (см. `03-acceptance-criteria.md` §P2). |
| R-8 | **Постфактум auth-сброс кеша зацикливает.** Повторные auth-провалы ⇒ повторные `reset_cache()`. | Низкая | Низкое | `reset_cache()` лишь форсирует один пересчёт; следующий `check()` снова закеширует на TTL; цикла «горячего» чтения нет; job не клеймится при FAIL. |
## Сводно
Доминирующий риск — **R-1** (блок очереди ложным auth-fail при неверном пути) и
организационный **R-7** (test-plan vs scope). Оба закрываются: R-1 — staging-проверкой +
тумблером, R-7 — правкой effort-тестов разработчиком/тестером согласно коррекции владельца.