메신저 게이트웨이
게이트웨이는 채팅 플랫폼을 세션 또는 이름 있는 에이전트에 연결합니다. 그 대화방의 메시지는 프롬프트가 되고, 에이전트의 답은 다시 메시지로 돌아오며, 승인 요청은 버튼으로 도착합니다. v0.1에서 완전히 지원되는 플랫폼은 Telegram입니다. Discord와 Slack은 같은 인터페이스를 구현하고 있고 전달은 되지만, 실전 검증은 상대적으로 덜 쌓여 있습니다.
먼저 자격 증명부터
snowpea setup --gateway telegram --token 123456:ABC-your-bot-token토큰은 $SNOWPEA_HOME/credentials.json에 0600 권한으로 들어갑니다. 대신 환경 변수로 export한 뒤 바인딩할 때 그 이름을 참조해도 됩니다. 어느 쪽이든 토큰이 settings.json에 쓰이는 일은 없고, 로그 한 줄에도 나타나지 않습니다.
바인딩
snowpea gateway bind telegram TELEGRAM_BOT_TOKEN agent:scribe --user 987654
snowpea gateway bind telegram TELEGRAM_BOT_TOKEN new:~/src/api --channel 123456 --user 987654
snowpea gateway list --json
snowpea gateway unbind <binding-id>세 위치 인자는 순서대로 플랫폼, 자격 증명 참조(credentials.json의 키 또는 환경 변수 이름), 대상입니다:
| 대상 | 의미 |
|---|---|
agent:<name> | 자기 메모리 네임스페이스를 가진 이름 있는 상시 에이전트 |
session:<id> | 기존 세션 |
new:<workdir> | 첫 메시지가 오면 그 디렉터리에 세션을 새로 만듦 |
--channel은 바인딩을 특정 채팅 id 하나로 제한합니다. --user는 승인을 결정할 수 있는 플랫폼 사용자를 지정합니다. 둘 다 설정하세요. --user가 없으면 승인에 답할 권한을 가진 사람이 아무도 없는 상태가 되고, 게이트웨이는 누가 버튼을 눌러도 무시합니다.
봇 만들기
Telegram. @BotFather에게 /newbot을 보내고 토큰을 받아 둡니다. 채팅 id는 봇에게 처음 메시지를 보냈을 때 데몬 로그에 찍히는 숫자입니다. long polling 방식이라 공개 URL도 webhook도 필요 없습니다.
Discord. 애플리케이션을 만들고 봇 유저를 추가한 뒤 message content intent를 켜고, 서버에 초대하고, 봇 토큰을 받아 둡니다. 초대할 때 bot과 applications.commands 두 스코프를 모두 주세요. 개발자 포털의 OAuth2 URL 생성기가 그 링크를 만들어 줍니다. 두 번째 스코프가 있어야 채팅 명령이 Discord의 / 목록에 나타납니다. 바인딩이 시작될 때 snowpea가 명령을 등록하므로 /sessions, /resume, /new, /projects, /status, /stop, /help, /mode, /model, /effort가 자유 입력 args 칸과 함께 목록에 뜨고, 이렇게 부른 명령의 답은 명령 자리에 그대로 돌아옵니다. 이 스코프가 없어도 깨지는 것은 없습니다. message content intent가 켜져 있으면 /sessions를 그냥 글로 쳐도 똑같이 동작합니다. 채널 id는 채널 URL의 마지막 경로 조각입니다.
Slack. Slack은 슬래시 명령을 오직 앱 매니페스트에서만 가져갑니다. 그러니 매니페스트를 출력해서 붙여 넣으세요:
snowpea gateway slack-manifestapi.slack.com/apps에서 새 앱이면 Create from manifest, 이미 있는 앱이면 App Manifest를 골라 이 JSON을 붙여 넣습니다. 봇 유저, 스코프(chat:write, commands, history 계열), 메시지 이벤트, Socket Mode, 그리고 /sessions, /resume, /new, /projects, /status, /stop, /help, /mode, /model, /effort가 진짜 슬래시 명령으로 선언됩니다. 이게 보기보다 중요합니다. Slack은 /로 시작하는 메시지를 앱에 넘기기 전에 전부 가로채기 때문에, 선언되지 않은 /sessions는 Slack이 "알 수 없는 명령"이라고 답해 버리고 snowpea에는 아예 도착하지 않습니다. 선언된 명령은 Socket Mode 슬래시 명령으로 들어오고, 답은 명령을 친 채널에 그대로 올라갑니다.
그다음 앱을 설치해 봇 토큰(xoxb-…)을 받고, Socket Mode용으로 connections:write 권한의 app-level 토큰(xapp-…)도 발급받습니다. 바인딩에는 둘 다 필요합니다. 봇 토큰만 있으면 예약 메시지 전달은 되지만 수신은 되지 않습니다. 대상으로는 채널 이름이나 id를 씁니다.
대화하기
봇에게 메시지를 보내면 바로 세션이 시작됩니다. 슬래시 명령은 터미널에서와 완전히 똑같이 동작하는데, 명령 레지스트리가 UI가 아니라 코어에 있기 때문입니다:
/mode
/ralph fix the failing integration test
/schedule "매일 09:00" "어제 커밋 요약" --channel telegram:123456게이트웨이가 만든 세션은 session.list에 출처와 함께 뜨므로, 같은 데몬에 붙은 TUI에서도 그 대화방이 지금 뭘 하고 있는지 볼 수 있습니다.
대화방 명령
터미널은 "내가 지금 어느 대화에 있는지, 또 뭐가 열려 있는지, 다른 데로 옮겨 달라"를 자기 창으로 답합니다. 대화방에는 창이 없으니 명령 일곱 개가 그 자리를 대신합니다. 게이트웨이가 명령 레지스트리보다 먼저 처리하며, 모델까지 가지 않습니다.
| 명령 | 하는 일 |
|---|---|
/sessions | 최근 대화 열 개. 지금 이 방이 붙어 있는 것에 ★. 각 줄은 버튼이기도 하고, 목록 바로 뒤에 숫자만 적어도 그 줄이 선택됩니다. |
/resume <번호 | id | 앞글자> | 이 방을 다른 세션으로 옮깁니다. 닫힌 세션은 다시 엽니다. |
/new [경로 | 프로젝트 | 번호] | 세션을 새로 시작합니다: 직접 적은 경로, /projects의 프로젝트, 또는 인자 없이 바인딩 자신의 작업 디렉터리. |
/projects | 알고 있는 프로젝트: IDE의 ide.projects 목록과 최근 세션들의 작업 디렉터리를 합치고 고정된 것을 앞에 둡니다. 각 줄이 거기서 세션을 시작합니다. |
/status | 세션 id, 작업 디렉터리, 모드, 모델, effort, 지금 턴이 도는지, 대기 중인 프롬프트, 사용한 컨텍스트. |
/stop | 도는 턴을 끊고 뒤에 쌓인 대기열도 비웁니다. TUI의 Stop과 같은 경로입니다. |
/help | 위 명령들과, 휴대폰에서 쓸 만한 세션 명령들. |
Telegram은 봇이 시작할 때 이것들을 / 메뉴로 등록하므로, 외울 필요 없이 목록에서 고르면 됩니다.
/resume, /new, /stop과 줄 버튼은 바인딩된 --user만 쓸 수 있습니다. 승인과 같은 이유입니다 — 다음 메시지가 무엇을 어디서 실행할지 정하는 일이니까요. /sessions, /projects, /status, /help는 대화방의 누구에게나 열려 있습니다.
이 방이 어느 세션에 있는지는 데몬을 재시작해도 유지됩니다. 선택은 $SNOWPEA_HOME/gateway-chats.json의 한 줄이고, 파일을 지우면 모든 방이 각자 바인딩의 원래 대상으로 돌아갑니다.
입력 중 표시와 진행 메시지
턴이 도는 동안 대화방에는 플랫폼의 "입력 중…" 표시가 뜨고, 몇 초마다 갱신됩니다. 승인이나 질문이 당신의 답을 기다리는 동안에는 멈춥니다 — 당신이 고민하는 동안 에이전트는 일하고 있지 않으니까요. 턴의 첫 툴 호출에서 메시지 하나가 나가고(⏳ shell npm test), 이후의 툴 호출은 새 메시지 대신 그 메시지를 고쳐 씁니다. 끝나면 ✓ 4 tool calls · 1m 12s로, 실패하거나 중단됐으면 ✗로 정리됩니다.
메시지를 고쳐 쓸 수 없는 플랫폼에는 진행 메시지를 아예 보내지 않습니다. 고쳐 쓰기가 없으면 이 기능은 그냥 도배이기 때문입니다. Slack은 입력 중 표시도 없습니다 — 봇 토큰으로는 보낼 수 없습니다. 그래서 Slack 대화방에는 진행 메시지만 남고, 그 진행 메시지는 제자리에서 계속 고쳐 쓰입니다.
진행 메시지는 약 1초 간격으로 변경을 모아 갱신하며, 빠른 결과도 후속 갱신으로 보여줍니다. 진행 메시지에는 최근 도구 입력·결과, 서브에이전트 식별 정보와 종료 보고, 자식 도구 호출·결과도 구분해 표시합니다. 긴 출력은 제한하고 인증 정보와 민감한 결과는 가립니다. 전체 기록은 세션에 남으며, 진행 메시지는 추가 발송 대신 같은 메시지를 갱신합니다.
설정 두 개로 끌 수 있습니다:
{ "gateway": { "typing": false, "progress": false } }기본값은 둘 다 true입니다. settings.gateway 안에서 플랫폼 블록과 나란히 있지만, 메신저가 아니라 스위치입니다.
무인 승인
게이트웨이나 스케줄러 세션에서 올라온 승인 요청은 무인 상태이고, 타이핑으로 직접 일으킨 승인과는 다르게 동작합니다:
- 붙어 있는 모든 클라이언트에
approval.pending알림으로 방송되므로 TUI 승인 큐에도 뜹니다. - 바인딩된 대화방에 허용·거부 버튼과 함께 전송됩니다.
- 어느 쪽이든 먼저 온 응답이 이깁니다. 나머지 모두는 결정 내용과 누가 결정했는지를 담은
approval.resolved를 받습니다. - 바인딩된
--user만 대화방에서 답할 수 있습니다. 다른 사람의 버튼 클릭은 무시되고 로그로 남습니다. - 아무 답이 없이
approvals.timeoutSec(기본 300초)이 지나면 거부됩니다.
TUI에서 타이핑으로 일으킨 승인은 그 화면에만 머물고 공유 큐에 절대 들어가지 않습니다. 이 경계가 핵심입니다 — 내 터미널의 프롬프트가 그룹 채팅으로 새어 나가지 않고, 채팅 승인을 낯선 사람이 대신 답할 수도 없습니다.
결정된 모든 건은 요청 id, 툴, 결정, 결정한 사람, 범위, 무인 여부와 함께 $SNOWPEA_HOME/logs/approvals.jsonl에 추가됩니다.
에이전트가 묻는 질문
ask_user 도구는 에이전트가 당신에게 묻는 것이고, 바인딩된 대화방에는 선택지마다 버튼이 달린 메시지로 도착합니다.
❓ 렌더러
렌더링을 무엇으로 할까요? 여기서 고른 것이 엔진 통제력을 정합니다.
1. three.js (추천) — 빠른 시작, 엔진 통제력 낮음
2. Raw WebGL2 — 공수 큼, 완전한 통제
(번호로 답해 주세요 / reply with the number, or type your own answer)
[1. three.js (추천)] [2. Raw WebGL2] [✏️ 기타 / Other]승인과 달리 대화방의 누구나 답할 수 있습니다. 질문은 아무 권한도 주지 않으므로 낯선 사람이 악용할 것이 없습니다. 버튼을 누르거나 번호를 그냥 적어도 됩니다 — 2, 여러 개를 고르는 질문이면 1,3. 번호가 아닌 것은 자유 입력 답으로 받으며, "기타" 버튼이 요청하는 것도 그것입니다.
ask_user 한 번에 질문이 여러 개일 때 터미널은 탭으로 보여 주지만, 대화방에는 탭이 없으므로 게이트웨이가 하나씩 올립니다 — 머리줄이 몇 번째인지 세어 주고(❓ 저장 (2/3)), 지금 질문에 답해야 다음 질문이 나옵니다. 마지막 답이 들어오면 전체가 한 번에 돌아갑니다. questions.timeoutSec(기본 600초)이 지나면 아무도 답하지 않았다고 에이전트에게 전달됩니다.
이름 있는 에이전트
이름 있는 에이전트는 자기 세션, 자기 메모리 네임스페이스, 자기 채널, 자기 잡을 가진 채 데몬 재시작을 넘겨 살아남습니다. 두 이름 있는 에이전트는 서로의 메모리를 읽을 수 없으므로, 하나에는 업무용 채널을, 다른 하나에는 개인용 채널을 맡겨도 안전합니다.
snowpea agents --json/agent create "<description>"으로 하나 만들고 gateway bind ... agent:<name>으로 채널을 바인딩하면, 다음 데몬 기동 시 바인딩까지 함께 복원됩니다. 이름 있는 에이전트는 네 번째 keepalive 카운터라서, 하나라도 있는 데몬은 유휴 상태로 종료되지 않습니다.
보안
게이트웨이는 원격 실행 경로입니다. 그렇게 다루세요.
--user를 바인딩하세요. 항상.- 완전히 통제하지 못하는 대화방에서 닿는 작업에는 plan 모드를 우선하세요.
- 승인 요청은 일회용이고 요청 id 하나에 묶여 있어서, 버튼 클릭을 재전송해도 아무 일도 일어나지 않습니다.
- 자격 증명은
0600파일에 있고, 가능하면 OS 키체인을 씁니다. approvals.jsonl을 가끔 들여다보세요. 내가 안 보는 사이에 무엇이 허용됐는지의 기록입니다.
다음
백엔드 — 툴을 다른 곳에서 실행하기.