장르: 세션·장애 기록(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.defaultstepfun/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:invoke
    • agent_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

메커니즘

  1. Ubuntu에서 docker exec hermes ...root로 실행
  2. root로 hermes CLI를 돌리면 auth.json 등이 root 소유로 재기록
  3. 게이트웨이는 s6가 hermes 사용자로 띄우므로 그 파일을 읽지 못함 → Permission denied
  4. Hermes는 이를 "파일 손상"으로 오판auth.json.corrupt로 치우고 빈 스토어로 시작 → 로그인 상태 상실 → 401
  5. auth.json모든 프로필이 공유하므로 전 채널이 동시에 죽음

이 함정은 이미 Hermes × Mattermost — Ubuntu 서버에서 채널별 다중 에이전트 구축프로필 파일에 대해 기록돼 있었습니다. 이번 사고는 그 사정권이 auth.json(=로그인 세션)까지 미친다는 점, 그리고 결과가 "게이트웨이 미기동"이 아니라 "기동은 되는데 인증만 실패" 라는 더 헷갈리는 형태로 나타난다는 점을 새로 알려줍니다.

파일이 멀쩡했다는 증거

같은 시각 root로 auth.jsonagent_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.logmodel=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개에 전부 의존. stepfunhy3:free와 똑같이 사라질 수 있음
2 fallback provider 부재 config show의 API Keys가 전부 (not set). OpenRouter 키 하나만 등록해도 이중화 가능
3 크레딧 0 유료 모델 전면 차단. 무료 모델이 전멸하면 즉시 전체 중단
4 auth.json 공유 구조 1시간 수명 OAuth 토큰을 6개 게이트웨이가 한 파일로 공유. 권한·갱신 사고가 나면 항상 전 채널 동시 장애
5 guest 프로필 — 담당자 공유 필요 타인 소유 프로필의 모델을 승인 하에 변경함(tencent/hy3:freestepfun/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절 참조).


관련 글