architect(ET): auto-commit from architect run_id=158
All checks were successful
CI / test (push) Successful in 13s
All checks were successful
CI / test (push) Successful in 13s
This commit is contained in:
@@ -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` (вынесен из этой задачи).
|
||||
Reference in New Issue
Block a user