Hermes Agent에 두 번째 봇을 올리다 — 멀티 프로필로 inbox 봇 추가하기

TL;DR

  • 홈랩 K3s의 Hermes Agent에 inbox 프로필을 추가해 하나의 파드에서 Discord 봇 2개를 운영
  • Discord Developer Portal의 “OAuth2 코드 승인 필요” 토글URL Generator 미작동 함정을 통과
  • s6 서비스로 등록해 파드 재시작 시 자동 기동
  • Cloudflare Access 인증은 K8s Secret(시크릿 값) + SOUL.md(환경변수 참조 지침) 조합으로 해결
  • 비밀값 자체를 프롬프트에 넣지 않고, 셸 환경변수 치환을 이용하는 패턴

이전 글: Hermes Agent를 처음 배포하면서 SQLite + NFS 문제로 스토리지부터 다시 짠 이야기는 Discord 봇 하나 올리는데 스토리지부터 다시 짰다에서 다뤘다.


들어가며

홈랩 K3s 클러스터에서 NousResearch Hermes Agent를 Discord 게이트웨이 봇으로 운영 중이다. 기존에는 default 프로필 하나로 운영했는데, 용도를 분리하기 위해 “inbox”라는 두 번째 프로필을 추가했다. Hermes Agent는 프로필별로 독립된 설정·메모리·세션을 갖기 때문에, 하나의 파드에서 여러 Discord 봇을 동시에 돌릴 수 있다.

이 글은 그 과정에서 밟은 삽질 포인트들과, Cloudflare Access 인증을 에이전트에게 안전하게 전달하는 패턴을 다룬다.


1단계 — inbox 프로필 생성

컨테이너 TUI에 접속해서 프로필을 만든다.

just tui
hermes profile create inbox

프로필은 /opt/data/profiles/inbox/에 생성된다. 기존 iSCSI PVC(hermes-data) 안이라 별도 볼륨은 필요 없다.


2단계 — Discord 봇 생성과 서버 초대

Discord Developer Portal에서 새 Application(Inbox)을 만들고 Bot을 추가한다. 여기서 두 가지 함정을 밟았다.

”Integration requires code grant” 에러

봇 초대 URL을 열면 이 에러가 뜬다. Developer Portal → 메뉴 → 인증 과정 섹션에서 “OAuth2 코드 승인 필요” 토글이 켜져 있으면 발생한다. 토글을 끄면 해결된다.

URL Generator가 URL을 생성하지 않는다

새 Developer Portal UI에서 OAuth2 URL Generator가 URL을 만들어주지 않는 경우가 있다. 이때는 수동으로 초대 URL을 만들면 된다.

https://discord.com/oauth2/authorize?client_id=<APPLICATION_ID>&permissions=117824&scope=bot
  • permissions=117824 = View Channels + Send Messages + Embed Links + Attach Files + Read Message History + Add Reactions
  • Application ID는 Developer Portal의 일반 정보 페이지에서 확인하거나, 봇 토큰의 첫 번째 . 앞 부분을 base64 디코딩하면 얻을 수 있다
echo "봇토큰첫번째부분" | base64 -d

3단계 — Discord 게이트웨이와 LLM 백엔드 설정

hermes -p inbox gateway setup

대화형으로 Discord 봇 토큰과 허용 사용자 ID를 입력한다. 설정값은 /opt/data/profiles/inbox/.env에 저장된다.

inbox 프로필에도 LLM 인증이 필요하다. default 프로필과 같은 OpenAI Codex OAuth를 사용했다.

hermes -p inbox auth add openai-codex

hermes -p inbox config set model.provider openai-codex
hermes -p inbox config set model.default gpt-5.5
hermes -p inbox config set model.base_url "https://chatgpt.com/backend-api/codex"

hermes model은 대화형 터미널에서만 동작하므로, kubectl exec 환경에서는 config set으로 우회한다.


4단계 — s6 서비스 등록

수동 실행(nohup ... &)으로는 파드 재시작 시 inbox 게이트웨이가 올라오지 않는다. s6 서비스로 등록하면 default와 함께 자동 시작된다.

hermes -p inbox gateway install

5단계 — Cloudflare Access 인증 문제

여기서부터가 본격적인 삽질이었다. inbox 봇이 Cloudflare Access 뒤의 리소스에 접근해야 하는데, 단순히 URL을 보내면 Cloudflare Access 로그인 페이지(HTTP 200, <title>Sign in · Cloudflare Access</title>)가 반환됐다.

왜 K8s Secret인가

방법장점단점
프로필 .env간단, 프로필별 분리kubectl exec으로 평문 노출
K8s SecretRBAC 접근 제어, etcd 암호화 가능전체 프로필에 노출

보안 관점에서 K8s Secret을 선택했다.

# manifests/secret.yaml (gitignore 대상)
apiVersion: v1
kind: Secret
metadata:
  name: hermes-secrets
  namespace: hermes
type: Opaque
stringData:
  CF_ACCESS_CLIENT_ID: "<Client ID>"
  CF_ACCESS_CLIENT_SECRET: "<Client Secret>"
just secret

Deployment의 envFrom: secretRef: hermes-secrets가 이미 설정되어 있어서, Secret에 키를 추가하면 파드 재시작 후 환경변수로 주입된다.


6단계 — 봇이 환경변수를 쓰게 만들기

K8s Secret으로 환경변수를 주입했지만, Hermes Agent가 HTTP 요청 시 자동으로 헤더를 붙이지는 않는다. Hermes에는 도메인별 커스텀 HTTP 헤더를 설정하는 기능이 없다.

해결책은 SOUL.md(시스템 프롬프트)에 환경변수를 헤더로 포함하라는 지침을 추가하는 것이었다.

## Cloudflare Access

inboxpilot.jaypy.dev 도메인에 HTTP 요청할 때는 반드시 Cloudflare Access 서비스 토큰 헤더를 포함해야 한다.
터미널에서 환경변수를 읽어서 헤더로 전달할 것:

curl -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
     -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
     https://inboxpilot.jaypy.dev/api/...

환경변수 CF_ACCESS_CLIENT_ID와 CF_ACCESS_CLIENT_SECRET은 컨테이너에 이미 주입되어 있다.

핵심은 비밀값 자체를 SOUL.md에 넣지 않고, 환경변수 참조만 안내한 것이다. 봇이 fetch 도구 대신 터미널에서 curl을 실행하면 셸이 환경변수를 치환해서 헤더에 포함시킨다.

SOUL.md란?

Hermes Agent의 프로필별 시스템 프롬프트 파일이다. 매 대화 세션마다 모델에 주입되기 때문에, 세션이 리셋되더라도 지침이 유지된다. 메모리(MEMORY.md)와 달리 항상 로드되므로, “반드시 지켜야 할 행동 규칙”을 넣기 좋다.


삽질 포인트 정리

문제원인해결
”Integration requires code grant”봇 설정에서 OAuth2 코드 승인 필요 토글이 ON토글 OFF
URL Generator가 URL 미생성새 Portal UI 버그 또는 scope 조합 문제수동 URL 생성
”No inference provider configured”프로필에 LLM 인증 미설정auth add + config set
base_url이 openrouter로 설정됨프로필 생성 시 기본값config set model.base_url로 수정
파드 재시작 후 inbox 미기동수동 실행은 일회성gateway install로 s6 등록
CF Access가 API 요청 차단봇이 CF Access 헤더 미포함K8s Secret + SOUL.md 지침

보안 설계

시크릿이 여러 계층에 걸쳐 있어서, 각각의 저장 위치와 보호 방식을 정리한다.

시크릿저장 위치보호 방식
Discord 봇 토큰프로필 .env (iSCSI PVC)PVC 접근 제어, gitignore
OpenAI Codex 인증auth.json (iSCSI PVC)OAuth — API 키 없음
CF Access 토큰K8s Secret (hermes-secrets)RBAC, etcd 암호화 가능, gitignore
SOUL.md의 CF 지침iSCSI PVC환경변수 참조만 — 값 미포함

manifests/secret.yaml.gitignore 대상이라 git에 커밋되지 않는다. secret.yaml.example만 레포에 포함되어 키 이름과 구조를 안내한다.


최종 구성

hermes pod (K3s)
├── s6-overlay (PID 1)
│   ├── gateway-default  → Discord 봇 "hermes" (gpt-5.5, openai-codex)
│   └── gateway-inbox    → Discord 봇 "inbox" (gpt-5.5, openai-codex)
├── /opt/data (iSCSI PVC)
│   ├── config.yaml, .env, auth.json      ← default 프로필
│   └── profiles/inbox/
│       ├── config.yaml, .env, auth.json  ← inbox 프로필
│       ├── SOUL.md                       ← CF Access 헤더 지침
│       └── sessions/, logs/, ...
└── K8s Secret (hermes-secrets)
    ├── CF_ACCESS_CLIENT_ID   → 컨테이너 env로 주입
    └── CF_ACCESS_CLIENT_SECRET

하나의 파드, 하나의 PVC, 하나의 Secret. 프로필을 더 추가해도 구조는 동일하다 — hermes profile create로 프로필을 만들고, gateway install로 s6에 등록하면 끝이다.