장르: 세션·장애 기록(postmortem). 실행 가이드가 아니라 "무엇을 관측했고 어떻게 원인에 도달했는가"의 기록입니다. 재현 절차만 필요하면 맨 아래 부록 A. 재발 시 진단 순서를 보세요.
- 일시: 2026-07-21 (KST 12:32 최초 발현 ~ 13:26 봇 응답 회복)
- 대상: Ubuntu 서버
your-server, 컨테이너hermes, 프로필default/sales/finance/dev/qna(+ 타인 테스트용guest)- 영향: Mattermost 채널 봇 전체 무응답. 사용자에게는 경고 문구만 노출됨
1. 요약
| 항목 | 내용 |
|---|---|
| 증상 1 | ⚠️ The model provider failed after retries. ... check gateway logs for diagnostics. |
| 증상 2 (조치 중 발현) | ⚠️ Provider authentication failed. Check the configured credentials; ... |
| 근본 원인 1 | 사용 중이던 무료 모델 tencent/hy3:free가 Nous 추론 API에서 사라짐 → HTTP 404. 계정 크레딧이 0이라 유료 모델도 사용 불가 |
| 근본 원인 2 | /opt/data/auth.json을 hermes 사용자가 읽지 못함(Permission denied) → 게이트웨이가 "로그인 안 된 상태"로 기동 → HTTP 401 |
| 조치 1 | 프로필 model.default → stepfun/step-3.7-flash:free (호출 가능한 유일한 무료 모델). 자사 5개 + 승인 후 guest까지 총 6개 |
| 조치 2 | /opt/data 소유권 복구(chown -R hermes:hermes) + 게이트웨이 재기동 |
| 결과 | 13:26 KST @agents-bot 정상 응답 확인 |
두 원인은 독립 사건입니다. 404를 고치는 과정에서 hermes CLI를 root로 실행해 2차 사고(401)가 유발되었습니다.
2. 타임라인
| 시각 (KST) | 시각 (UTC) | 사건 |
|---|---|---|
| 12:32 | 03:31:56 ~ 03:32:04 | 모델 호출 3회 재시도 전부 404 → 사용자에게 경고 문구 노출 |
| ~12:50 | ~03:50 | 로그 위치 추적 시작. "로그가 최신이 아니다"는 오인(→ TZ 문제) |
| ~13:05 | ~04:05 | errors.log에서 404 원문 확보 → 원인 조사 본격화 |
| ~13:10 | ~04:10 | 모델별 호출 테스트로 404의 두 종류를 분리 → 원인 1 확정 |
| ~13:15 | ~04:15 | default 프로필 모델 교체. 이 과정에서 root 실행으로 auth.json 권한 파손 |
| 13:16 | 04:16:38 | 증상이 404 → 401로 변경 (Permission denied, starting with empty store) |
| ~13:20 | ~04:20 | 대체 모델의 tool calling·context 검증 통과 |
| ~13:25 | ~04:25 | 소유권 복구 + 나머지 4개 프로필 모델 교체 |
| 13:26 | 04:26 | 봇 정상 응답 회복 |
3. 진단 과정 — 세운 가설과 그 운명
이 절이 이 글의 핵심입니다. 틀린 가설을 어떤 증거로 죽였는지가 재발 시 가장 큰 시간 절약이 됩니다.
| # | 가설 | 결과 | 반증/확증한 증거 |
|---|---|---|---|
| 1 | 로그가 오래됨 = 게이트웨이가 죽었거나 다른 곳에 기록 | ❌ 기각 | 컨테이너는 UTC, Mattermost는 KST. 03:32가 곧 12:32였음 |
| 2 | 무료 티어 rate limit(429) | ❌ 기각 | 로그는 404 NotFoundError. 429는 한 번도 없음 |
| 3 | API 키 만료/잔액 부족(401/402) | ❌ 기각 | 로그가 404. 401은 2차 사고 때 별개로 발생 |
| 4 | 모델이 카탈로그에서 삭제됨 | ❌ 기각 | /v1/models(449KB) grep 결과 hy3:free 문자열이 존재 |
| 5 | 키가 무효(직접 curl에서 401) | ❌ 기각 | config.yaml에 api_key가 없어서 빈 문자열을 보낸 무효 테스트였음. 인증은 auth.json(OAuth) 기반 |
| 6 | OAuth 토큰 만료/갱신 경합 | ❌ 기각 | 서버가 "계정 잔액이 부족하다"고 응답 → 토큰을 읽고 계정까지 식별했다는 뜻 |
| 7 | 모델은 카탈로그에 이름만 남고 라우팅 불가 | ✅ 확증 | hy3:free→"Couldn't find that, sorry." / 유료 모델→"requires available credits". 404 메시지가 서로 다름 |
| 8 | 401의 원인은 토큰 만료 | ❌ 기각 | 같은 시각 파일의 agent_key로 curl하면 HTTP 200. 파일은 멀쩡했음 |
| 9 | 게이트웨이가 auth.json을 읽지 못함 | ✅ 확증 | failed to parse /opt/data/auth.json ([Errno 13] Permission denied) — starting with empty store |
결정적 관측 — "404 두 종류"
tencent/hy3:free → {"status":404,"message":"Couldn't find that, sorry."}
tencent/hy3 → {"status":404,"message":"Model 'tencent/hy3' requires available credits. ..."}
meituan/longcat-2.0 → {"status":404,"message":"... requires available credits. ..."}
같은 404라도 메시지가 다르면 원인이 다릅니다. 유료 모델은 "크레딧 부족"이라 명시하는데 hy3:free만 "그런 것 없음"이라 답했습니다. 여기서 원인 1이 확정되었고, 동시에 인증은 정상임이 증명되었습니다(계정 잔액을 판정하려면 먼저 계정을 식별해야 하므로).
4. 근본 원인 1 — 무료 모델 소멸
상태
- Nous 추론 API는 OpenRouter형 모델 중개 게이트웨이로 동작 (
canonical_slug,pricing,aliases,expiration_date필드 존재 = 모델이 수시로 교체·만료되는 구조) - 인증 방식: 정적 API 키가 아니라 OAuth device_code
/opt/data/auth.json,client_id=hermes-cli,scope=inference:invokeagent_key/access_token수명 3599초(1시간),refresh_token으로 갱신config show의 API Keys는 전부(not set)— OpenRouter 등 대체 provider 없음
- 계정 크레딧 0 → 유료 모델 전부 차단
검증된 사실
카탈로그(/v1/models)의 :free 항목 : stepfun/step-3.7-flash:free, tencent/hy3:free (2개)
실제 호출 성공 : stepfun/step-3.7-flash:free (1개, HTTP 200)
⚠️ 카탈로그에 있다 ≠ 호출할 수 있다.
/v1/models는 메뉴판일 뿐이고 서빙 여부는/v1/chat/completions를 직접 때려봐야만 알 수 있습니다. 이 프로젝트에서 가설 4가 죽은 이유이자, 앞으로도 모델 교체 시 반드시 지켜야 할 원칙입니다.
대체 모델 검증 (교체 전 필수 확인)
stepfun/step-3.7-flash:free:
| 항목 | 값 | 판정 |
|---|---|---|
context_length |
256,000 | ✅ (장애 당시 요청이 msgs=29, ~24,075 tokens) |
tools / tool_choice |
지원 | ✅ |
| 실제 tool call 테스트 | finish_reason:"tool_calls", get_weather(city="서울") 생성 |
✅ |
| 기타 | structured_outputs, reasoning 지원 |
— |
tool calling 확인이 왜 필수인가: Hermes는 "LLM + 도구" 구조이고 max turns: 150으로 도구 호출을 반복합니다. 모델이 tools를 지원하지 않으면 메모리(fact_store), MCP, 터미널·브라우저 스킬이 전부 무력화되고, 최악의 경우 에러 없이 "저장했습니다" 같은 환각만 남습니다(조용한 실패). 잡담은 되므로 응답이 온다는 것만으로는 이 고장을 감지할 수 없습니다.
5. 근본 원인 2 — auth.json 권한 사고 (2차 유발)
로그 원문
2026-07-21 04:16:38 WARNING hermes_cli.auth: auth: failed to parse /opt/data/auth.json
([Errno 13] Permission denied: '/opt/data/auth.json') — starting with empty store.
Corrupt file preserved at /opt/data/auth.json.corrupt
2026-07-21 04:16:38 WARNING gateway.run: Primary provider auth failed:
Hermes is not logged into Nous Portal. — trying fallback
메커니즘
- Ubuntu에서
docker exec hermes ...는 root로 실행됨 - root로
hermesCLI를 돌리면auth.json등이 root 소유로 재기록됨 - 게이트웨이는 s6가 hermes 사용자로 띄우므로 그 파일을 읽지 못함 →
Permission denied - Hermes는 이를 "파일 손상"으로 오판해
auth.json.corrupt로 치우고 빈 스토어로 시작 → 로그인 상태 상실 → 401 auth.json은 모든 프로필이 공유하므로 전 채널이 동시에 죽음
이 함정은 이미 Hermes × Mattermost — Ubuntu 서버에서 채널별 다중 에이전트 구축에 프로필 파일에 대해 기록돼 있었습니다. 이번 사고는 그 사정권이
auth.json(=로그인 세션)까지 미친다는 점, 그리고 결과가 "게이트웨이 미기동"이 아니라 "기동은 되는데 인증만 실패" 라는 더 헷갈리는 형태로 나타난다는 점을 새로 알려줍니다.
파일이 멀쩡했다는 증거
같은 시각 root로 auth.json의 agent_key를 읽어 curl하면 HTTP 200이 나왔습니다. 즉 자격증명은 유효했고, 오직 읽기 권한만 문제였습니다. 이 관측이 "재인증(재로그인)" 시도를 막았습니다 — 섣불리 재로그인했다면 refresh token이 회전하며 상황이 더 악화됐을 수 있습니다.
6. 적용한 조치
6-1. 모델 교체 (5개 프로필)
for P in default sales finance dev qna; do
docker exec -u hermes hermes /opt/hermes/.venv/bin/hermes \
-p $P config set model.default "stepfun/step-3.7-flash:free"
done
# 확인
for P in default sales finance dev qna; do
echo -n "$P: "
docker exec -u hermes hermes /opt/hermes/.venv/bin/hermes -p $P config show \
| grep -o "'default': '[^']*'"
done
⚠️
hermes model을-p없이 실행하면 default 프로필만 바뀝니다. 실제로 이번에도 default만 교체된 채 나머지 4개가 404로 남아 있었습니다.
6-2. 소유권 복구
docker exec hermes sh -c "cp -a /opt/data/auth.json /opt/data/auth.json.bak-$(date +%s)"
docker exec hermes chown -R hermes:hermes /opt/data
# hermes 사용자가 실제로 읽는지 검증 (재기동 전 필수)
docker exec -u hermes hermes /opt/hermes/.venv/bin/python -c \
"import json;print('ok', json.load(open('/opt/data/auth.json')).get('active_provider'))"
auth.json이 빈 껍데기가 되고 자격증명이 .corrupt에만 남은 경우:
docker exec hermes sh -c "cp -a /opt/data/auth.json.corrupt /opt/data/auth.json \
&& chown hermes:hermes /opt/data/auth.json"
6-3. 게이트웨이 재기동
설정·자격증명은 프로세스 기동 시점에 로드되어 고정되므로 파일만 바꿔서는 반영되지 않습니다.
docker exec hermes sh -c "pkill -f 'hermes-gateway'; sleep 5" # s6가 자동 부활
docker exec -u hermes hermes /opt/hermes/.venv/bin/hermes gateway list
6-4. guest 프로필 (타인 소유, 사용자 승인 후 추가 조치)
처음에는 타인 테스트용이라 제외했으나, 같은 원인으로 죽어 있어 승인을 받아 함께 교체했습니다.
docker exec -u hermes hermes /opt/hermes/.venv/bin/hermes \
-p guest config set model.default "stepfun/step-3.7-flash:free"
docker exec hermes sh -c "ps -eo pid,args | grep '[k]atty'"
docker exec hermes kill <게이트웨이 PID> # 위에서 확인한 python3 프로세스
sleep 5 && docker exec -u hermes hermes /opt/hermes/.venv/bin/hermes gateway list
전체 재기동(pkill) 대신 해당 PID만 kill한 이유: 다른 5개 채널을 끊지 않고, "이 조치가 guest를 고쳤다"는 인과를 깨끗하게 확인하기 위함입니다.
프로세스 트리 구조 (이해해두면 재기동 판단이 쉬움):
182 s6-supervise gateway-guest ← 감독자 (건드리지 않음)
183 s6-supervise gateway-guest/log
184 python3 … hermes -p guest gateway run --replace ← 이것만 kill
185 s6-log … /opt/data/logs/gateways/guest
288 python3 … mcp_stdio_watchdog.py --ppid 184 … ← 184가 죽으면 함께 정리됨
289 python … /opt/data/profiles/guest/mcp/sample_server.py
💡
--create-time으로 기동 시각을 알 수 있습니다. guest 게이트웨이는--ppid 184 --create-time 1784162141= 약 5일 전(7/16,guest/.env생성 시각과 일치) 기동된 상태였습니다. 즉 한 번도 재기동된 적이 없어 메모리에 옛 설정을 들고 있었다는 확증이며, 재기동이 필수임을 보여줍니다.
7. 검증 상태 (정직한 기록)
| 항목 | 상태 | 근거 |
|---|---|---|
stepfun/step-3.7-flash:free 호출 가능 |
✅ 확인 | curl HTTP 200 |
| 대체 모델 tool calling 지원 | ✅ 확인 | finish_reason:"tool_calls" 실측 |
5개 프로필 config.yaml 교체 |
✅ 확인 | config set 성공 메시지 + config show |
default 봇 응답 회복 |
✅ 확인 | 13:26 Mattermost 응답 |
| 실제 요청이 새 모델로 나감 | ✅ 확인 | agent.log에 model=stepfun/step-3.7-flash:free 연속 10건 |
| 게이트웨이 전체 running | ✅ 확인 | gateway list → default(150)·dev(155)·finance(143)·guest(184)·qna(178)·sales(164) 6개 전부 ✓ |
| 401 재발 없음 | ✅ 확인 | Permission denied 마지막 기록이 04:16:39, 이후 무발생 |
| 404 재발 없음 | ✅ 확인 | 조치 후 errors.log에 provider 404 없음 |
| 나머지 4개 봇 실제 멘션 응답 | ⬜ 미검증 | 게이트웨이는 running이나 채널 멘션 테스트는 미실시 |
guest 프로필 모델 교체 + 재기동 |
🔶 조치함 / 검증 미확인 | 사용자 승인 후 6-4절 수행. 재기동 후 로그 확인 출력은 미수집 |
참고: 조치 후
errors.log에 남는tools.registry: check_fn ... returned False; dependent tools will be unavailable this turn경고는 정상입니다. 브라우저(CDP)·터미널·칸반 등의 전제조건이 이번 턴에 충족되지 않았다는 안내일 뿐, 장애와 무관합니다.
8. 교훈 · 재발 방지
8-1. 컨테이너 안에서 hermes CLI를 root로 실행하지 말 것
# ❌ 위험 — 파일을 root 소유로 만들어 게이트웨이를 마비시킴
docker exec hermes hermes model
docker exec -it hermes hermes config set ...
# ✅ 안전
docker exec -u hermes hermes /opt/hermes/.venv/bin/hermes model
호스트에 hermes 별칭·래퍼 스크립트를 두었다면 -u hermes가 들어가도록 수정하십시오. 이번 사고의 직접 원인입니다.
8-2. 시각 대조는 UTC 기준으로
컨테이너는 UTC, Mattermost 표시는 KST(UTC+9)입니다. Mattermost 시각 − 9시간 = 로그 시각.
docker exec hermes date -u # 컨테이너 시각
date # 서버 로컬 시각
8-3. 봇이 스스로 말하는 모델명을 믿지 말 것
조치 후 봇은 자신이 nous/hy3:free(존재한 적 없는 슬러그)를 쓴다고 답했습니다. LLM은 자기 모델 ID를 모르며, 남아 있는 세션 히스토리의 옛 모델명을 읽어 환각합니다. 모델 확인은 항상:
docker exec -u hermes hermes /opt/hermes/.venv/bin/hermes -p <name> config show | grep -A2 Model
docker exec hermes sh -c "grep -o 'model=[^ ]*' /opt/data/logs/agent.log | tail"
장애 조치 후에는 채널에서 세션을 초기화해 옛 컨텍스트를 버리는 것이 좋습니다.
8-4. 404를 "일시 장애"로 오해하지 말 것
Hermes는 상태코드를 구분하지 않고 404에도 백오프 재시도를 3회 태웁니다(2.19초 → 5.34초). 로그의 "retries"라는 단어에 속아 일시적 장애로 판단하면 안 됩니다 — 404는 설정 불일치이며 재시도로 절대 풀리지 않습니다.
9. 미해결 리스크 (후속 과제)
| # | 리스크 | 설명 |
|---|---|---|
| 1 | 단일 실패점 | 5개 봇이 무료 모델 1개에 전부 의존. stepfun도 hy3:free와 똑같이 사라질 수 있음 |
| 2 | fallback provider 부재 | config show의 API Keys가 전부 (not set). OpenRouter 키 하나만 등록해도 이중화 가능 |
| 3 | 크레딧 0 | 유료 모델 전면 차단. 무료 모델이 전멸하면 즉시 전체 중단 |
| 4 | auth.json 공유 구조 | 1시간 수명 OAuth 토큰을 6개 게이트웨이가 한 파일로 공유. 권한·갱신 사고가 나면 항상 전 채널 동시 장애 |
| 5 | guest 프로필 — 담당자 공유 필요 |
타인 소유 프로필의 모델을 승인 하에 변경함(tencent/hy3:free → stepfun/step-3.7-flash:free). 모델이 바뀌면 응답 품질·성격이 달라질 수 있으므로 담당자에게 변경 사실과 사유를 전달해야 함. 별건으로 mcp-stderr.log가 400KB로 비대(MCP 서버가 stderr를 계속 쏟는 중일 가능성) → 담당자 점검 항목 |
부록 A. 재발 시 진단 순서
봇이 ⚠️ 경고만 뱉을 때 아래 순서로 5분 안에 원인을 좁힐 수 있습니다.
# 0) 시각 기준 맞추기 (Mattermost 시각 − 9h)
docker exec hermes date -u
# 1) 살아있는 로그 파일 찾기 (경로 추측 금지, mtime으로 역추적)
docker exec hermes sh -c "find /opt/data -name '*.log' -o -name 'current' | xargs ls -lt | head -20"
# 2) 예외 원문 — 여기에 답이 있음
docker exec hermes sh -c "tail -60 /opt/data/logs/errors.log"
# 3) 상태코드로 1차 분류
# 404 → 모델/권한 문제 (4단계로)
# 401 + 'Permission denied' → 소유권 사고 (5단계로)
# 429/402 → 한도·크레딧
# 4) 모델이 실제로 호출 가능한가 (카탈로그 조회로는 판정 불가)
docker exec hermes sh -c '
K=$(/opt/hermes/.venv/bin/python -c "import json;print(json.load(open(\"/opt/data/auth.json\"))[\"providers\"][\"nous\"][\"agent_key\"])")
curl -s -w " HTTP:%{http_code}\n" https://inference-api.nousresearch.com/v1/chat/completions \
-H "Authorization: Bearer $K" -H "Content-Type: application/json" \
-d "{\"model\":\"<모델명>\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}"'
# 5) hermes 사용자가 자격증명을 읽을 수 있는가
docker exec -u hermes hermes /opt/hermes/.venv/bin/python -c \
"import json;print(json.load(open('/opt/data/auth.json')).get('active_provider'))"
부록 B. 호출 가능한 무료 모델 전수 조사
무료 모델이 또 사라졌을 때, 살아있는 후보를 실제 호출로 가려냅니다.
docker exec hermes sh -c '
K=$(/opt/hermes/.venv/bin/python -c "import json;print(json.load(open(\"/opt/data/auth.json\"))[\"providers\"][\"nous\"][\"agent_key\"])")
curl -s https://inference-api.nousresearch.com/v1/models -H "Authorization: Bearer $K" > /tmp/models.json
for M in $(grep -o "\"id\":\"[^\"]*:free\"" /tmp/models.json | sed "s/\"id\":\"//;s/\"//" | sort -u); do
C=$(curl -s -o /dev/null -w "%{http_code}" https://inference-api.nousresearch.com/v1/chat/completions \
-H "Authorization: Bearer $K" -H "Content-Type: application/json" \
-d "{\"model\":\"$M\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}")
echo "$C $M"
done'
200인 모델만 후보입니다. 채택 전 tools 지원과 context_length를 반드시 확인하십시오(4절 참조).
관련 글
- Hermes Agent — Ubuntu 서버 Docker 설치 + Mattermost 연동 — Ubuntu base 설치,
@agents-bot(default) 생성 - Hermes × Mattermost — Ubuntu 서버에서 채널별 다중 에이전트 구축 — 다중 프로필 구축. root 소유권 함정의 원 기록(프로필 파일 한정)
- Hermes dev 채널 holographic provider 도입 — dev 프로필 메모리 provider. tool calling에 의존하므로 이번 장애의 영향권
- Nous Portal — 크레딧 충전·계정 관리