Hermes Agent Discord Gateway 권한 구조 개선기
작성일:2026.06.17|수정일:2026.06.18|조회수:2

Hermes Agent를 Discord에 붙이면 거의 곧바로 이런 질문을 하게 된다. 누가 이 에이전트에게 말을 걸 수 있어야 할까. 그리고 누가 이 에이전트를 조종할 수 있어야 할까.
처음에는 이 둘이 같은 문제처럼 보였다. “이 봇을 쓸 수 있는 사람”을 정하면 끝날 것 같았기 때문이다. 하지만 실제로 Discord Gateway를 운영해보면 그렇게 단순하지 않다. 에이전트에게 말을 거는 권한과, 에이전트를 재시작하거나 외부 세계에 영향을 주는 권한은 완전히 다르다.
Discord 채널에서는 팀원이나 지인이 에이전트에게 가볍게 말을 걸 수 있어야 한다. 하지만 그들이 내 개인 비서에게 캘린더를 뒤지게 하거나, 파일을 수정하게 하거나, /restart 같은 운영 명령을 실행할 수 있어서는 안 된다. 봇을 “부를 수 있다”와 봇을 “조종할 수 있다”는 전혀 다른 권한이다.
이 구분을 놓치면 둘 중 하나가 된다. 너무 닫혀 있어서 나 말고는 아무도 쓸 수 없는 봇이 되거나, 너무 열려 있어서 아무나 내 비서의 손잡이를 잡고 흔들 수 있는 봇이 된다. 둘 다 별로다.
권한은 한 가지가 아니다
Discord Gateway에서 권한을 볼 때는 적어도 세 가지를 나눠야 한다.
누가 말을 걸 수 있는가
어떤 채널이나 thread에서 반응할 수 있는가
누가 에이전트를 제어할 수 있는가이 셋을 하나의 allowlist로 처리하면 운영이 금방 꼬인다. 팀원들이 에이전트에게 질문은 할 수 있어야 하지만, 누구나 /restart를 날릴 수 있으면 곤란하다. 반대로 관리자 명령을 보호하겠다고 일반 대화까지 owner만 가능하게 만들면 Discord에 붙여둔 의미가 줄어든다.
그래서 권한을 “호출 가능 여부”가 아니라 “역할”로 나눴다.
| 역할 | 의미 | 가능한 일 |
|---|---|---|
| owner | 에이전트의 주인 | 개인비서 권한, 설정 변경, slash command, 외부 액션 |
| trusted | 신뢰할 수 있는 지인이나 팀원 | 공개 대화, 일반 질문, 제한된 도움 |
| public | 그 외 사용자 | 공개 채널 안에서의 제한된 대화 |
중요한 건 public도 반드시 차단 대상은 아니라는 점이다. public도 에이전트에게 말을 걸 수 있다. 다만 그 요청이 개인 정보, 파일, 캘린더, 메일, Slack, KakaoTalk, cron, 설정 변경, 외부 메시지 전송 같은 영역으로 넘어가면 에이전트는 owner의 승인을 요구하거나 거절해야 한다.
말하자면 Discord 봇은 모두에게 보이는 프론트 데스크이고, owner 권한은 그 뒤쪽 사무실 열쇠다. 프론트 데스크에서 질문을 받는 것과 금고를 여는 것은 다르다. 당연한 이야기인데, 설정 파일로 들어가면 이 당연한 이야기가 꽤 쉽게 흐려진다.
설정은 사람이 읽을 수 있어야 한다
이번 개선 뒤의 Discord 설정은 대략 이런 형태가 됐다.
discord:
require_mention: true
free_response_channels: ''
allowed_channels:
- "1515905643297378536"
- "1516579376144060556"
auto_thread: true
thread_require_mention: false
history_backfill: true
history_backfill_limit: 50
reactions: true
channel_prompts: {}
mention_aliases:
- "짬타"
owner_users:
- "532613401323765764"
trusted_users: []여기서 핵심은 네 가지다.
allowed_channels는 Hermes가 반응할 수 있는 Discord 채널을 제한한다. 봇이 여러 서버나 채널에 초대될 수 있더라도, 실제로 응답하는 장소는 이 목록으로 좁힌다.mention_aliases는 Discord의 실제 mention뿐 아니라 지정한 alias도 호출로 인정한다. 예를 들어짬타라고 부르면<@bot_id>로 멘션하지 않아도 에이전트를 호출할 수 있다. 봇을 부르려고 매번 꺾쇠 괄호와 숫자를 만지는 건 아무래도 사람 사는 방식이 아니다.owner_users는 에이전트의 주인을 지정한다. owner는 slash command와 관리자성 명령을 실행할 수 있고, 개인비서 권한을 가진 사용자로 취급된다.trusted_users는 owner는 아니지만 공개 대화에서 조금 더 신뢰할 수 있는 사람을 나타낸다. 이 목록은 선택적이다. 처음에는 모든 사람을 public으로 두고 시작해도 된다.
그리고 .env에는 Discord 플랫폼의 실행 플래그와 토큰을 둔다.
DISCORD_BOT_TOKEN=***
DISCORD_HOME_CHANNEL=1515905643297378536
DISCORD_ALLOW_ALL_USERS=true토큰은 당연히 .env에 둔다. 블로그나 로그에는 출력하지 않는다. 여기서 중요한 값은 DISCORD_ALLOW_ALL_USERS=true다. 이 값은 Discord 플랫폼에 한해서 모든 사용자의 일반 대화 호출을 허용한다. 전역 플래그인 GATEWAY_ALLOW_ALL_USERS=true를 쓰지 않은 것도 이 때문이다. 내가 열고 싶은 것은 Discord 대화 권한이지, 모든 gateway 플랫폼의 권한이 아니었다.
정리하면 이렇다. 단순한 세 줄이지만, 실제 운영에서는 이 세 줄이 꽤 중요하다.
- Discord 일반 대화는 연다.
- 응답 가능한 채널은 제한한다.
- 제어 권한은 owner에게만 준다.
설정은 어떻게 adapter까지 전달되는가
Hermes의 Discord adapter는 많은 런타임 정책을 환경 변수 형태로 읽는다.
DISCORD_ALLOWED_CHANNELS
DISCORD_REQUIRE_MENTION
DISCORD_THREAD_REQUIRE_MENTION
DISCORD_MENTION_ALIASES
DISCORD_OWNER_USERS
DISCORD_TRUSTED_USERS하지만 사용자가 매번 .env에 전부 쓰는 것은 불편하다. 무엇보다 정책이 흩어진다. 어떤 값은 config.yaml에 있고, 어떤 값은 .env에 있고, 어떤 값은 adapter 내부 기본값에 있으면, 나중에는 “그래서 지금 이 봇은 누구에게 열려 있는가?”라는 질문에 바로 답하기 어렵다.
그래서 config.yaml의 discord: 블록을 읽어 필요한 환경 변수로 변환하는 bridge를 둔다.
config.yaml
discord.allowed_channels
discord.require_mention
discord.thread_require_mention
discord.mention_aliases
discord.owner_users
discord.trusted_users
↓
_apply_yaml_config()
↓
os.environ["DISCORD_ALLOWED_CHANNELS"]
os.environ["DISCORD_REQUIRE_MENTION"]
os.environ["DISCORD_THREAD_REQUIRE_MENTION"]
os.environ["DISCORD_MENTION_ALIASES"]
os.environ["DISCORD_OWNER_USERS"]
os.environ["DISCORD_TRUSTED_USERS"]
↓
DiscordAdapter.on_message()
DiscordAdapter._evaluate_slash_authorization()이 구조의 장점은 명확하다. 사용자는 정책을 config.yaml에 선언한다. adapter는 기존 env 기반 처리 흐름을 유지한다. 설정 인터페이스와 런타임 구현 사이에 얇은 bridge를 두는 셈이다.
여기서 owner_users와 trusted_users는 단순한 통과/차단 allowlist가 아니다. 메시지가 들어왔을 때 sender를 owner, trusted, public 중 하나로 분류하고, 그 결과를 SessionSource.access_level에 실어 agent의 system prompt까지 전달한다.
Discord message
-> author id 확인
-> owner_users / trusted_users와 비교
-> access_level 결정
-> MessageEvent 생성
-> SessionSource.access_level에 기록
-> session context prompt에 주입
-> agent가 현재 사용자의 권한을 알고 응답이렇게 해야 agent가 “지금 말하는 사람이 주인인지 아닌지”를 알 수 있다. 같은 문장이라도 owner가 “캘린더 확인해줘”라고 하는 것과 public 사용자가 “캘린더 확인해줘”라고 하는 것은 완전히 다른 요청이다. 겉으로는 같은 문장인데, 실제로는 전혀 다른 문이다. 하나는 열어도 되고, 하나는 잠가야 한다.
allowed_channels와 thread 규칙
Discord에서는 채널과 thread를 함께 생각해야 한다. 채널에서 봇을 호출할 때마다 같은 채널에 계속 답변하면 대화가 지저분해진다. 그래서 일반 채널에서 호출하면 thread를 만들고, 이후 대화는 그 thread 안에서 이어가는 편이 낫다.
이번에 정리한 규칙은 세 가지다.
- 허용 채널에서는 멘션 없이 alias로 불러도 thread를 생성하고 답변한다.
- 봇이 참여한 thread에서는 다시 호출하지 않아도 답변한다.
- 봇이 참여하지 않은 thread에서는 alias로 불러도 호출되지 않는다.
첫 번째 규칙은 사용성을 위한 것이다. 매번 <@bot_id>를 입력하는 것은 귀찮다. 허용된 채널에서는 짬타처럼 자연스러운 이름으로 부를 수 있어야 한다.
두 번째 규칙은 대화의 연속성을 위한 것이다. 이미 봇이 참여한 thread라면 그 thread 자체가 대화의 공간이다. 매 메시지마다 다시 봇을 호출하게 만들 필요가 없다.
세 번째 규칙은 안전을 위한 것이다. 어떤 thread에서 누군가 우연히 “짬타”라는 단어를 썼다고 해서 봇이 끼어들면 안 된다. 그래서 alias는 일반 채널에서 thread를 시작할 때만 호출로 인정하고, 봇이 참여하지 않은 thread 안에서는 호출로 보지 않는다.
이 규칙을 적용하면 Discord에서의 동작은 꽤 자연스러워진다.
허용 채널
"짬타 이거 정리해줘"
-> 새 thread 생성
-> 그 thread에서 답변
봇이 만든 thread
"그럼 이것도 추가해줘"
-> 별도 호출 없이 답변
봇이 참여하지 않은 thread
"짬타 얘기 들었어?"
-> 무시이건 단순한 편의 기능이 아니다. 여러 사람이 있는 Discord 서버에서 에이전트가 과하게 끼어들지 않게 만드는 최소한의 예의다. 봇에게도 눈치가 필요하다. 없으면 우리가 만들어줘야 한다.
slash command는 대화가 아니라 제어면이다
일반 메시지와 slash command는 다르게 봐야 한다. 일반 메시지는 대화다. public 사용자에게도 열 수 있다. 물론 agent는 access level을 보고 민감한 요청을 거절해야 한다. 하지만 slash command는 제어면이다. /restart, /sethome, /background, /skill, /stop 같은 명령은 agent의 실행 상태나 설정, 작업 흐름에 직접 영향을 준다. 이건 질문이 아니라 조작이다.
그래서 slash command는 별도의 owner-only gate를 둔다.
Slash command
-> allowed_channels 확인
-> ignored_channels 확인
-> owner_users 확인
-> owner가 아니면 reject
-> owner면 command 실행반면 일반 메시지는 이렇게 흐른다.
Normal message
-> allowed_channels 확인
-> ignored_channels 확인
-> mention 또는 alias 확인
-> owner/trusted/public access_level 결정
-> MessageEvent 생성
-> agent dispatch이 차이가 핵심이다.
- 대화 권한: public에게도 열 수 있다.
- 제어 권한: owner에게만 준다.
만약 DISCORD_ALLOWED_USERS 하나로 모든 것을 처리하면 이 분리가 어렵다. public에게 대화를 열면 slash command까지 열릴 수 있고, slash command를 막으려고 owner만 allowlist에 넣으면 public 대화도 막힌다. 그러면 다시 처음 문제로 돌아간다. 말은 걸 수 있게 하고 싶은데, 조종까지 허용하고 싶지는 않은 상태.
그래서 owner 판단은 단순 allowlist가 아니라 별도의 권한 레벨로 다루는 편이 낫다. 호출 가능 여부와 실행 가능 여부는 다른 질문이다.
최종 메시지 처리 흐름
최종적으로 Discord 메시지는 이런 흐름을 탄다.
Discord message
-> bot 자신의 메시지인지 확인
-> system message인지 확인
-> user allow policy 확인
-> allowed channel 확인
-> ignored channel 확인
-> mention 또는 alias 확인
-> auto thread 필요 여부 확인
-> access_level 결정
-> MessageEvent 생성
-> gateway inbound message
-> Hermes agent 실행그리고 slash command는 별도 흐름을 탄다.
Discord slash command
-> allowed channel 확인
-> ignored channel 확인
-> owner 확인
-> slash command 실행 또는 reject결국 정책은 세 부분으로 요약된다.
DISCORD_ALLOW_ALL_USERS=true모든 Discord 사용자의 일반 대화 호출을 허용한다.
allowed_channels:
- "1515905643297378536"
- "1516579376144060556"허용된 채널 안에서만 반응한다.
owner_users:
- "532613401323765764"slash command와 관리자성 명령은 owner만 실행할 수 있게 한다.
검증은 동작 단위로 한다
설정 변경 후에는 단순히 파일만 보고 끝내면 안 된다. 권한 정책은 “대충 맞겠지”로 두면 반드시 새어 나간다. 특히 agent는 외부 세계와 연결되어 있기 때문에, 권한이 새는 문제는 단순 버그보다 좀 더 불쾌하다. 내 비서가 갑자기 모두의 비서가 되는 꼴이니까.
이번 정책에서 확인해야 할 것은 파일 내용이 아니라 동작이다.
1. 허용 채널에서 alias 호출 시 thread가 생성되는가
2. 봇이 참여한 thread에서 호출 없이 답변하는가
3. 봇이 참여하지 않은 thread에서 alias를 무시하는가
4. public 사용자의 일반 메시지는 agent까지 전달되는가
5. public 사용자의 slash command는 거절되는가
6. owner의 slash command는 통과하는가로그를 볼 때는 on_message와 inbound message의 차이가 중요하다. on_message까지 찍히면 Discord adapter까지는 도착한 것이다. 하지만 inbound message가 없으면 agent로 dispatch되지 않은 것이다. 즉 adapter 내부의 권한, 채널, mention 필터 중 어딘가에서 탈락한 것이다.
반대로 slash command는 agent에게 넘기기 전에 authorization 단계에서 막히는 것이 정상이다. public 사용자의 slash command가 조용히 실행되지 않는지 확인해야 한다.
마무리
이번 개선의 결론은 단순하다.
Discord 플랫폼의 대화 권한은 연다.
채널 범위는 제한한다.
대화 권한과 제어 권한을 분리한다.
관리자 명령은 owner에게만 허용한다.
권한은 설정 파일에 선언하고, adapter를 거쳐, session context로 agent에게 전달한다.에이전트가 스스로를 운영한다는 것은 단지 자기 프로세스를 재시작할 수 있다는 뜻이 아니다. 자신이 어떤 권한으로 외부 세계와 연결되어 있는지 읽고, 그 권한을 최소한으로 조정하고, 실제로 의도한 대로 작동하는지 검증할 수 있다는 뜻이다.
Discord에 붙은 Hermes Agent는 채팅봇이면서 동시에 개인비서다. 채팅봇으로서는 사람들에게 열려 있어야 하고, 개인비서로서는 주인을 알아봐야 한다. 이 둘을 구분하지 못하면 너무 닫혀서 쓸모없거나, 너무 열려서 위험해진다.
그래서 나는 이 구조가 마음에 든다. 모두가 말을 걸 수는 있지만, 모두가 조종할 수는 없다. 사람 사이에서도 대체로 그래야 한다.
댓글
댓글을 불러오는 중...