feat(preflight): catch logged-out auth and treat empty result as failure
ORCH-044 closes two blind spots that let a single de-authenticated agent
stall the shared queue for all projects:
P1 — preflight auth gate. `claude --version` answers even when logged out,
so version-only preflight was blind to auth. Adds a token-free, network-free
check of <AGENT_HOME>/.claude/.credentials.json: missing/unreadable/no-oauth
or an expired `claudeAiOauth.expiresAt` (epoch ms, vs now + skew) => preflight
FAIL; absent expiry => OK (no false positives). Result is cached on the same
preflight_cache_ttl. Post-factum safety net: launcher detects auth markers
("not logged in" / "/login" / "unauthorized" / 401) in the run log and resets
the preflight cache so the next tick re-evaluates auth. Auth failure is a gate,
not a transient — it does not spin the circuit breaker. Emergency toggle
ORCH_PREFLIGHT_CHECK_AUTH=false restores version-only behaviour.
P3 — empty log / no result-JSON => job failed. exit_code==0 with an empty or
JSON-less run log no longer counts as success: a separate result_ok flag gates
stage advance + usage comments, fires a Telegram alert, and routes the job
through the normal transient/permanent failure path (exit_code integrity in
agent_runs preserved).
Scope: P2 (--effort) is intentionally excluded and tracked in ORCH-50.
New settings: ORCH_PREFLIGHT_CHECK_AUTH, ORCH_CLAUDE_CREDENTIALS_PATH,
ORCH_AUTH_EXPIRY_SKEW_SECONDS. Docs updated (INFRA.md, internals.md, CHANGELOG).
Refs: ORCH-044
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -88,7 +88,16 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
|
||||
1. Записывает run в DB (agent_runs)
|
||||
2. Запускает subprocess. **stdout/stderr перенаправляются СРАЗУ в файл `/app/data/runs/{id}.log` на уровне ОС** (Popen `stdout=log_fh`). Никакого PIPE в памяти оркестратора → нет PIPE-deadlock, нет потока-читателя, нет зомби (B-2).
|
||||
3. Стартует **watchdog thread** (timeout 30 мин → SIGKILL по pid)
|
||||
4. Стартует **monitor thread**: `proc.wait()` (гарантированный reap → реальный exit_code в БД) → закрывает log_fh → git commit/push → auto-advance
|
||||
4. Стартует **monitor thread**: `proc.wait()` (гарантированный reap → реальный exit_code в БД) → закрывает log_fh → **валидация результата (ORCH-044)** → git commit/push → auto-advance
|
||||
|
||||
**Валидация результата (ORCH-044, P3).** `exit_code==0` сам по себе НЕ считается успехом: claude может «быстро умереть» (разлогинен / флаг гасит stdout), оставив пустой или JSON-less лог, но выйдя с кодом 0 — раньше это было неотличимо от успеха (`done` + auto-advance по пустому результату). Теперь `_monitor_agent` вызывает `_validate_result(output_path)`:
|
||||
- лог отсутствует / пустой (0 байт или только whitespace) ⇒ невалиден;
|
||||
- нет парсящегося trailing result-JSON (тот же контракт, что usage-учёт — `usage._extract_last_json_object`) ⇒ невалиден;
|
||||
- хелпер защитный (never-raise); при собственной ошибке — fail-safe в сторону провала.
|
||||
|
||||
`success = (exit_code==0 AND result_ok)`. Реальный `exit_code` пишется в `agent_runs` без искажения; на решение done/fail влияет отдельный флаг `result_ok` (не подменённый код выхода). Только при `success`: постится «успешный» status-коммент и вызывается `_try_advance_stage`. При `exit_code==0 AND not result_ok`: шлётся Telegram-алерт о пустом/невалидном результате, стадия НЕ двигается, а `_finalize_job(result_ok=False)` маршрутизирует job в провал (`empty run log / no result JSON`): по умолчанию permanent (`attempts<max` ⇒ requeue, иначе `failed`+алерт), transient-маркер в логе уводит в transient-путь. Итог: `exit_code==0` всегда завершается терминально/ретраябельно (`done`|`failed`|`queued`) — путь «быстрая смерть с exit 0 → вечный running» закрыт.
|
||||
|
||||
**Постфактум auth-детекция (ORCH-044, P1b).** В пути провала `_handle_auth_marker(log)` ищет маркер разлогина (`not logged in` / `please run /login` / `unauthorized` / `401`) и при совпадении сбрасывает preflight-кеш (`preflight.reset_cache()`), чтобы следующий тик воркера переоценил auth проактивно. Auth-провал НЕ transient и НЕ крутит circuit breaker.
|
||||
|
||||
### 5. Auto-advance (`launcher._try_advance_stage`)
|
||||
|
||||
@@ -216,6 +225,8 @@ services:
|
||||
| Max retries | Developer: max 3 попытки, затем эскалация |
|
||||
| Zombie-free | stdout идёт сразу в файл + monitor `proc.wait()` → процесс всегда reap'нут (B-2) |
|
||||
| Orphan recovery | При старте: orphan-run'ы (finished_at IS NULL, старше 35 мин) помечаются exit=-1 с per-run warning + Telegram-уведомлением «нужна ручная проверка» (M-1) |
|
||||
| Preflight auth-гейт (ORCH-044) | Перед клеймом: `os.path.exists(bin)` + `claude --version` + **token-free auth** (чтение `.credentials.json` + `expiresAt`); разлогинен / протух ⇒ job не клеймится. Постфактум-маркер `not logged in` сбрасывает кеш. Тумблер `ORCH_PREFLIGHT_CHECK_AUTH`. Детали — INFRA.md |
|
||||
| Пустой результат = провал (ORCH-044) | `exit 0` с пустым/JSON-less логом ⇒ `failed`/retry + алерт, без auto-advance (см. §4 «Валидация результата») |
|
||||
|
||||
## Агенты
|
||||
|
||||
@@ -292,12 +303,15 @@ webhook (plane/gitea) background thread (queue_worker)
|
||||
_monitor_agent (proc.wait, commit/push,
|
||||
│ advance stage)
|
||||
│
|
||||
_finalize_job:
|
||||
exit 0 -> mark_job done
|
||||
exit !=0 & attempts<max -> requeue (queued)
|
||||
exit !=0 & attempts>=max -> failed + Telegram
|
||||
_finalize_job(result_ok):
|
||||
exit 0 & result_ok -> mark_job done
|
||||
else (exit!=0 ИЛИ пустой результат):
|
||||
attempts<max -> requeue (queued)
|
||||
attempts>=max -> failed + Telegram
|
||||
```
|
||||
|
||||
> ORCH-044 (P3): `result_ok` отражает валидность run-лога (непустой + есть result-JSON). `exit 0` с пустым/невалидным результатом идёт в ветку провала, НЕ в `done` (см. §4 «Валидация результата»).
|
||||
|
||||
### Таблица `jobs`
|
||||
|
||||
| Колонка | Назначение |
|
||||
|
||||
Reference in New Issue
Block a user