개요
메시징 플랫폼은 OpenArms의 얇은 가시 표면입니다. 제품의 핵심이 아니라, 전경에서 도는 일을 사람이 보고 한 줄로 개입하는 창입니다. 그래서 이 장은 “어떤 채팅 앱을 쓰느냐”보다 그 표면을 떠받치는 스택(봇 API·MTProto·tmux 좌석·프로바이더 CLI·파이썬 환경)을 구체적으로 다룹니다. 각 도구를 왜 골랐고, 어떻게 엮으며, 배포판에서 hook을 어떻게 거는지 순서로 봅니다.
텔레그램 (현재 1차 표면)
현재는 텔레그램을 1차로 다룹니다. 포럼 토픽·스레드가 워커의 계약·블로커·증거·결과 레인이 되고, 봇 정체성과 슬롯, PM 제어가 그 위에 섭니다. 그룹 채널은 지금의 오케스트레이션 권위가 아니라, 큐레이션된 캡슐·증거를 위한 미래 전송 레인입니다.
왜 텔레그램인가 (선택 근거)
- 포럼 토픽이 곧 레인이다. 텔레그램 슈퍼그룹의 포럼 토픽(
message_thread_id)은 별도 인프라 없이 워커별·작업별 스레드를 만들어 줍니다. OpenArms는 영구 5레인 스켈레톤(접수/진행/검증/완료/보관)을 토픽으로 그대로 깝니다. 디스코드 포럼·슬랙thread_ts로도 같은 모델을 옮길 수 있지만, 텔레그램은 봇 생성 비용이 가장 낮습니다. - 봇 생성에 전화번호·심사가 없다. BotFather에서 토큰 하나만 받으면 됩니다. 카카오 채널·라인 OA·왓츠앱 비즈니스처럼 사업자 승인 단계가 없어 1인 운영자가 즉시 띄웁니다.
- 두 가지 접근 경로를 한 계정에서 쓴다. 같은 텔레그램을 봇 API(HTTP)와 MTProto(직접 TCP) 두 길로 동시에 만질 수 있어, 가벼운 송수신과 무거운 작업 (대용량 첨부·토픽 열람·소유자 권한)을 길로 나눌 수 있습니다.
세부 설계 규칙(봇 토큰·업데이트 인그레스·토픽/슬롯·그룹 가시성)은 프로바이더 문서에 정리돼 있고, 표면에는 날 내부를 노출하지 않는 것이 원칙입니다.
봇 API vs MTProto: 두 길을 왜 둘 다 쓰나
OpenArms는 두 텔레그램 접근 방식을 역할로 나눠 씁니다. 이건 우연이 아니라, 두 길의 한계가 정확히 갈리기 때문입니다.
| 길 | 무엇 | OpenArms에서 담당 | 한계 |
|---|---|---|---|
| 봇 API (HTTP) | api.telegram.org/bot<token>/... 폴링·전송 | PM 인그레스(getUpdates), 답장 송신(sendMessage), 첨부(sendDocument/sendPhoto), 타이핑 액션 | 다운로드 20MiB·업로드 50MiB 제한, 봇이 직접 못 보는 정보(토픽 전체 열람 등) 존재 |
| MTProto (telethon) | Telegram 서버와 직접 TCP, api_id·api_hash·세션 | 워커 스레드 본문 열람, 소유자 권한 작업(토픽 정리·삭제), 큰 미디어, 사용자 세션 미러 | 사용자 세션 자격이 durable local secret로 남음(A5 리스크) |
설치 의존성에서 telethon이 필수 코어로 잡혀 있는 이유가 이것입니다. 가벼운 송수신은
표준 라이브러리 urllib만으로 봇 API를 때리고(예: 첨부 전송 media.py는
requests·telethon 없이 multipart/form-data를 직접 만듭니다), MTProto가
필요한 무거운 작업에서만 telethon 클라이언트를 새로 띄웁니다.
외부 비교의 권위 정리: telethon 공식 문서가 “봇 API는 tdlib 위의 HTTP 추상화라 노출이 적고 파일이 작으며, MTProto는 직접 연결이라 지연이 낮고 2GiB까지 다룬다”고 정확히 같은 트레이드오프를 말합니다(Telethon: Bot API vs MTProto).
어떻게 구성하나 (스택·연결)
- 봇 토큰 (봇 API 길). BotFather에서 봇을 만들어 토큰을 받습니다. 토큰은
~/.openarms/credentials/telegram.env에OPENARMS_TELEGRAM_BOT_TOKEN=...으로 넣고chmod 600으로 잠급니다(레포에 절대 커밋 금지). 토큰 하나 = 텔레그램을 향한 서비스 정체성 하나일 뿐, 워커 정체성이 아닙니다. 워커/PM 구분은 OpenArms 레지스트리와 role lease로 합니다. - MTProto 세션 (telethon 길). my.telegram.org에서
api_id·api_hash를 받고, telethon으로 한 번 로그인해 세션을 만든 뒤 별도 env 파일(예:telegram_mtproto.env)에 둡니다. 런타임은OPENARMS_TELEGRAM_MTPROTO_ENV가 그 파일을 가리키게 합니다. - 슬롯 매핑. 봇 정체성은 BotFather가, 목적지(채팅·토픽)는 env가 정합니다:
OPENARMS_TELEGRAM_SLOT_CHAT_ID(채널/그룹 chat id),OPENARMS_TELEGRAM_SLOT_THREAD_ID(포럼 토픽 thread id). 한 OpenArms 계약 슬롯이 하나의 텔레그램 목적지에 묶입니다.
봇 API 인그레스 짧은 예시 (백로그 스킵)
PM 좌석을 띄울 때 옛 테스트 메시지가 다시 흘러들지 않도록, getUpdates 오프셋을
현재 최대 update_id + 1로 초기화한 뒤 리스너를 nohup으로 띄웁니다. 배포판의
run_pm_injector.sh가 하는 일이 정확히 이것입니다:
# offset = 현재 백로그의 마지막 update_id + 1 → 백로그 건너뛰기
curl -s "https://api.telegram.org/bot$TOKEN/getUpdates?offset=-1"
# 그 오프셋을 state에 적고, 인그레스 리스너를 백그라운드로 기동여기서 핵심 규칙은 인그레스 소유자 하나입니다. 같은 봇에 getUpdates 폴러를 둘
이상 띄우거나 webhook과 폴러를 동시에 두면 업데이트를 서로 뺏어 갑니다(A2 인그레스
경합). OpenArms는 PM 인그레스를 한 프로세스로 못 박습니다.
tmux — 라이브 전경 실행석
OpenArms의 워커/PM은 “프롬프트를 받고 답하는 함수”가 아니라, 계속 살아 있는 CLI 세션입니다. 그 세션을 사람이 보지 않는 동안에도 붙잡아 두고, 밖에서 한 줄을 밀어 넣고, 화면을 떠오는 좌석이 필요합니다. 그 backend가 현재 tmux입니다.
왜 tmux인가 (대안 대비)
- 분리(detached) 세션이 핵심 가치다.
tmux new-session -d로 띄운 세션은 사람이 붙어 있지 않아도 그 안의 CLI가 계속 돕니다. PM/워커가 24시간 전경을 유지해야 하는 OpenArms에 정확히 맞습니다. - 밖에서 키를 밀어 넣고, 화면을 떠올 수 있다.
send-keys로 임의 텍스트·Enter를 주입하고capture-pane으로 보이는 내용을 stdout으로 떠옵니다 (tmux(1) man page). 이게 OpenArms가 “텔레그램 한 줄 → CLI 입력”을 잇는 메커니즘입니다. - 대안 대비. raw pty/expect는 화면 재부착·창 관리가 약하고, ssh 단독은 세션 영속성이 없으며, provider-native API는 CLI마다 다릅니다. tmux는 이 셋의 좋은 점 (영속·주입·캡처)을 한 번에 줍니다.
단, tmux는 제품 정체성이 아니라 현재 구현의 backend입니다. 로드맵은
execution_seat를 tmux/pty/ssh/windows-terminal/provider-native로 추상화해 tmux를 굳히지 않는 방향입니다. Windows native는 비주력이고, WSL/Linux/macOS/Mac VM에서 tmux가 자연 동작합니다 (자세한 좌석 의존성 계층은 빠른 시작·설치 참조).
어떻게 구성하나
좌석은 named session·window·pane으로 주소를 갖습니다(fleet:openarms-pm.0 형태).
런타임은 이 주소를 env로 읽습니다:
OPENARMS_PM_PANE— PM pane의 tmux 타깃(기본fleet:openarms-pm.0)OPENARMS_TMUX/OPENARMS_TMUX_BIN— tmux 바이너리(기본tmux)OPENARMS_HERMES_ROUTE_TARGET=tmux:hermes-cli:0.0— 워커/PM 라우팅 타깃을 tmux 좌석으로 지정
주소를 코드에 박지 않고 env로 읽는 이유는, fleet/restore 도구가 바인딩을 config에서
읽어야 하기 때문입니다(hermes=PM/agy=worker를 하드코딩하지 않는다는 표준 규칙).
배포판에서 hook을 어떻게 쓰나 (tmux 주입 예시)
텔레그램 인그레스가 받은 한 줄을 PM 좌석에 넣을 때, 배포판의 inject_listener.py는
화면을 스크랩하지 않고 send-keys로 밀어 넣기만 합니다. 텍스트와 Enter를 따로 보내는
것이 포인트입니다 — 텍스트가 키 조합으로 해석되지 않도록:
TMUX = os.environ.get("OPENARMS_TMUX", "tmux")
PANE = os.environ.get("OPENARMS_PM_PANE", "fleet:openarms-pm.0")
# 1) 리터럴 텍스트(-l), 2) 그 다음 Enter 를 별도 send-keys 로
subprocess.run([TMUX, "send-keys", "-t", PANE, "-l", text], check=False)
subprocess.run([TMUX, "send-keys", "-t", PANE, "Enter"], check=False)이게 OpenArms의 “주입은 하되 답장은 캡처-스크랩으로 떠오지 않는다”는 경계입니다. 답장은 프로바이더 hook이 자기 권위로 내보냅니다(아래 Hermes/AGY 절).
파이썬 환경 — venv / pip / uv
OpenArms는 외부 LLM CLI(Hermes/AGY/Claude)를 붙잡아 구동하는 엔진이라, “어느 파이썬으로 무엇을 돌리느냐”가 자주 사고의 원천입니다. 그래서 환경 경계를 명시적으로 둡니다.
왜 격리된 venv인가, 그리고 어느 인터프리터인가
- 테스트/런타임 인터프리터를 못 박는다. 지원 러너는 **WSL/Linux/macOS의
python3**이고, Windows의 hermes-agent venv (...\hermes\hermes-agent\venv\Scripts\python.exe)는 지원 러너가 아닙니다 — 거기엔pytest·telethon이 없어 테스트 수집 자체가 안 됩니다. 이걸 명시하지 않으면 “Windows venv에서 테스트가 안 걷힌다”는 가짜 실패가 납니다. - 권위는 코드 메타데이터. 의존성의 단일 권위는
pyproject.toml/requirements-dev.txt이고, 문서는 그 위의 가이드입니다.
어떻게 구성하나 (표준 quickstart)
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]" # editable 설치 (소스 수정 즉시 반영)
PYTHONIOENCODING=utf-8 python3 -m pytest -q # 한글 리터럴 깨짐 방지-e(editable) 설치를 쓰는 이유는, OpenArms가 자기 소스를 끊임없이 고치는 활성 코드라
재설치 없이 변경이 바로 먹어야 하기 때문입니다. 필수 코어는 telethon(MTProto),
[jsonschema](https://json-schema.org/)(report-schema 게이트)이고, [networkx](https://networkx.org/)/pyyaml이 없으면 일부 테스트가
실패가 아니라 조용히 SKIP되어 커버리지가 새므로 설치를 권장합니다.
배포판에는 import-only preflight가 있어, 테스트를 돌릴 바로 그 인터프리터로 의존성 유무를 먼저 확인합니다(설치·네트워크 없이 exit 0/비-0):
python3 scripts/dev_preflight.pyuv를 고려한다면 (외부 best practice)
[uv](https://docs.astral.sh/uv/)(Astral, Rust 기반)는 pip·virtualenv·pip-tools의 드롭인 대체(supersede)로, 같은
pyproject.toml을 읽고 uv.lock으로 결정적 설치를 줍니다. uv venv는 약 10ms로
python -m venv(1초+)보다 훨씬 빠르고, uv run은 활성화 없이 프로젝트 환경에서 바로
실행해 “venv 활성화를 깜빡한” 실수 부류를 없앱니다
(Real Python: uv vs pip,
uv 문서). OpenArms의 권위는 여전히
pyproject.toml이므로, uv를 쓰더라도 uv pip install -e ".[dev]"로 같은 editable
설치를 그대로 재현할 수 있습니다. 속도-민감한 CI나 멀티-OS 재현이 중요해지는 분화
버전에서 검토할 후보입니다(현재 라이브 권위 경로는 venv+pip).
프로바이더 CLI hook — Hermes(PM) / AGY(worker)
표면(텔레그램)과 좌석(tmux)이 깔리면, 그 위에서 실제로 말하는 주체는 프로바이더 CLI 입니다. 현재 라이브 testbed의 바인딩은 Hermes=PM, AGY(Antigravity)=worker이며, 이건 제품 기본값이 아니라 이 배포 인스턴스의 바인딩입니다(역할≠프로바이더). 전체 연동 그림은 연동 장을, 라우팅·판단 경계는 핵심 개념 장을 보세요.
Hermes (PM 역할)
-
왜 PM에 붙였나. Hermes는 공식 플러그인 hook 로더를 가진 CLI라, OpenArms가 Hermes 코어를 패치하지 않고도 전경 디스패치에 끼어들 수 있습니다. 코어를 안 건드리고 hook으로만 붙는다는 점이 PM(조율·판단) 자리에 맞습니다.
-
어떻게 구성하나. 배포판은
integrations/hermes/openarms_contract_bridge/에 공식 플러그인을 둡니다.plugin.yaml이 매니페스트로, 의존성·필요 env·거는 hook을 선언합니다:name: openarms-contract-bridge pip_dependencies: [ "telethon>=1.36" ] requires_env: - OPENARMS_ROOT - OPENARMS_HERMES_ROUTE_TARGET # tmux:hermes-cli:0.0 - OPENARMS_HERMES_ROUTE_EXECUTOR # tmux - OPENARMS_TMUX_BIN hooks: - pre_gateway_dispatch # 텔레그램 전경 인입 → OpenArms 라우팅 - transform_llm_output # 내보낼 텍스트 가공(역할 헤더·미러) - cli_input_before_submit # CLI 제출 직전 - cli_response_after_render # CLI 응답 렌더 직후 미러 -
배포판에서 hook을 어떻게 켜나.
config/openarms.hermes.env.example을 사설 프로파일로 복사하고OPENARMS_REGISTER_FOREGROUND_HOOKS=1로 전경 hook을 등록합니다. 그러면 Hermes 플러그인 로더가 위 hook들을 잡고, OpenArms가 Hermes 코어 수정 없이 라우팅·재작성에 끼어듭니다. 예컨대transform_llm_output/cli_response_after_render단계에서 OpenArms는 역할 헤더([PM]/[Worker])를 붙이고, 라우팅-id·경로·토큰 모양 같은 내부를 사용자 표면에 새지 않게 다듬습니다.
AGY / Antigravity (worker 역할)
-
왜 worker에 붙였나. AGY는 native attachment 표면과 강한 도구 게이트를 가진 CLI라, 증거·첨부를 다루는 worker 턴에 맞습니다.
-
어떻게 구성하나. 배포판은
integrations/antigravity/openarms_provider/에 Antigravity-native 번들(plugin.json,commands/,hooks/openarms.json,skills/openarms-worker/SKILL.md)을 둡니다. hook 설정은 역할·프로바이더·권위를 선언적으로 박습니다:{ "openarms_provider_bridge": { "role": "worker", "provider": "antigravity", "authority": "official_antigravity_cli_surface", "attachment_policy": "use_native_attachment_surface_before_fallback" } }attachment_policy가 “native 첨부 표면을 폴백보다 먼저”라고 못 박는 이유는, worker가 파일을 보낼 때 Hermes 게이트웨이로 셸아웃하지 말고 자기 CLI의 네이티브 첨부를 쓰라는 뜻입니다(증거 경로를 한 곳으로 모음).
PM 송신 backstop (코드 레이어)
프로바이더 hook과 별개로, PM 좌석(여기선 Claude)이 답을 내보낼 때는 raw curl 대신
배포판의 pm_send.py를 통해 보냅니다. 메커니즘(중복 제거·재시도·내부 식별자/경로/토큰
모양 redaction)은 코드에 박혀 invisible하게 강제되고, 판단(무엇을 말할지)은 스킬에
free하게 남깁니다:
pm_send.py --chat <chat_id> [--thread <thread_id>] --text "<reply>"이게 “워커는 자유롭게 말하되, 라우팅 봉투 마커·-100… 슈퍼그룹 id·절대 경로·봇 토큰은
표면에 절대 안 샌다”는 경계를 코드로 보장하는 부분입니다(A8 가시-표면 누출 차단). PM
운용 루프 전체는 OpenArms 사용 장에서
이어집니다.
공격 표면 요약 (왜 이렇게 잠그나)
위 선택들은 텔레그램 프로바이더의 알려진 리스크에 맞춰 잠근 결과입니다:
| 표면 | 텔레그램에서의 모양 | OpenArms의 잠금 |
|---|---|---|
| A1 자격 탈취 | 봇 토큰, api_id/api_hash, telethon 세션 | env 분리 + chmod 600 + 레포 커밋 금지 |
| A2 인그레스 경합 | 폴러 다중·webhook+폴러 동시 | PM 인그레스 소유자 하나로 못 박음 |
| A5 로컬 세션 노출 | telethon 세션 파일이 durable local memory | MTProto는 무거운 작업에서만, 세션은 사설 env |
| A6 레인 오바인딩 | 잘못된 chat_id/message_thread_id | 슬롯을 레지스트리·env로 명시 매핑 |
| A8 가시-표면 누출 | 원시 id·토큰·경로가 사용자 화면에 | pm_send.py/hook 재작성에서 redaction |
프로바이더 중립 방향
메시징 프로바이더도 교체 가능한 구현입니다. 텔레그램이 지금의 증명 표면이고, 디스코드·
슬랙·라인·카카오·왓츠앱 등은 같은 어댑터 계약으로 붙을 수 있는 포트폴리오 후보입니다.
각 플랫폼은 위와 같은 길의 구분이 다르게 나타납니다 — 디스코드는 Gateway/intent·forum
post, 슬랙은 OAuth scope·Events API/Socket Mode·thread_ts, 왓츠앱은 Business
Platform·Cloud API·webhook, 라인/카카오는 OA 채널·사업자 승인 제약. OpenArms 코어는
이들을 provider-specific 어댑터 개념으로만 알고, 중립 개념(chat identity·credential
reference·ingress owner·route owner·foreground CLI surface)으로 추상화합니다.
각 플랫폼별 연결 절차와 가시성 차이는 같은 골격(표면→좌석→프로바이더 hook→redaction) 위에서 순차적으로 채웁니다.