개요
OpenArms는 CLI 에이전트를 부리는 사람을 위한 에이전트 운영 관제면입니다. 사용자가 모든 CLI·LLM 워커를 일일이 지켜보지 않아도, PM 역할이 현재 상태를 읽고 맡기거나 보류하거나 격리하거나 풀어주며 작업대를 흐트러뜨리지 않고 이력을 보존합니다.
누가 쓰는가
- CLI 에이전트를 혼자 부리는 개발자 — 터미널 에이전트가 실제로 무슨 일을 하고 있는지, 왜 막혔는지, 다음 판단은 누구 몫인지를 텔레그램 같은 얇은 표면으로 지켜봅니다.
- 여러 워커를 조율하는 PM — 고용 에이전트가 계약 에이전트에게 일을 위임하고, 결과를 같은 스레드로 돌려받아 수용·재작업·보류를 판단합니다.
- 진행을 추적하는 팀 — 어떤 워커가 어떤 계약 아래 있는지, 무엇이 증거이고 무엇이 다음 행동인지를 칸반으로 함께 봅니다.
대표 흐름
사용자 → 고용 에이전트 → 계약 에이전트 → 고용 측 판단 → 사용자핵심은 사람이 프로토콜 라벨이나 날 터미널 출력, 작업 id를 읽지 않고도 “지금 무슨 일이 어디까지 됐는지”를 사람 말로 이해하는 것입니다.
이 흐름을 실제로 굴리는 부품은 다섯입니다 — PM 역을 맡는 Hermes, 워커 역을 맡는 AGY(Antigravity), 둘을 붙잡아 두는 실행석 tmux, 텔레그램으로 읽고 쓰는 전송층 Telethon(MTProto), 그리고 설치·격리를 담당하는 **venv**입니다. 아래에서 각각을 “왜 골랐나 / 어떻게 구성하나 / 배포판에서 훅을 어떻게 쓰나”로 풀어 씁니다. 역할(PM/워커)이 고정 개념이고 provider(Hermes/AGY)는 그 역할을 채우는 교체 가능한 어댑터라는 점이 설계의 뼈대입니다 — 자세한 역할≠provider 규칙은 연동 챕터를 보세요.
부품 깊이 읽기
Hermes — PM(코디네이터) provider
왜 이걸 골랐나. PM 역은 “사람과 대화하면서 동시에 워커를 조율하고, 텔레그램 토픽
(스레드)을 직접 만들고 지우고 이름 바꾸는” 역입니다. Hermes를 쓰는 결정적 이유는 공식
플러그인 훅 레이어가 있다는 점입니다. 덕분에 Hermes 코어를 패치하지 않고도(= 업스트림
업데이트와 충돌 없이) OpenArms의 라우팅·억제·스레드 도구를 그 위에 얹을 수 있습니다.
대안인 “터미널 출력 스크레이핑(tmux 화면 긁기)“은 깨지기 쉽고, “코어 포크”는 유지비가
크며, “사이드카 텔레그램 모니터 별도 기동”은 중복 응답·경쟁 상태를 부릅니다. Hermes의
공식 훅 + 공식 register_tool API는 이 셋을 모두 피해 갑니다.
어떻게 구성하나. 브리지는 레포 밖(예:
~/.hermes/hermes-agent/plugins/openarms_contract_bridge)에 설치되는 별도 플러그인이고,
매니페스트(plugin.yaml)가 의존성·필요 env·훅 목록을 선언합니다.
name: openarms-contract-bridge
version: "0.2.0"
pip_dependencies:
- telethon>=1.36
requires_env:
- OPENARMS_ROOT
- OPENARMS_HERMES_ROUTE_TARGET
- OPENARMS_HERMES_ROUTE_EXECUTOR
- OPENARMS_TMUX_BIN
hooks:
- pre_gateway_dispatch
- transform_llm_output
- cli_input_before_submit
- cli_response_after_render브리지가 레포 밖에 살기 때문에 import openarms가 바로 되지 않습니다. 그래서 로드 시
OPENARMS_ROOT → ~/openarms → 체크아웃 부모 디렉터리 순으로 레포 루트를 찾아
sys.path에 끼워 넣는 부트스트랩이 먼저 돕니다. 이게 없으면 respawn이 env 없이 떴을 때
No module named 'openarms'로 네이티브 스레드 도구가 통째로 사라지고, 모델이 날
터미널 CLI로 후퇴해 raw “topic ” 라벨을 뱉습니다. 토큰·목적지 같은 비밀은 절대
레포에 넣지 않고 config/openarms.hermes.env.example을 본떠 owner-local로 둡니다.
배포판에서 훅을 어떻게 쓰나. 플러그인은 register(ctx) 한 곳에서 훅과 도구를
모두 등록합니다 — 지원되지 않는 훅은 조용히 건너뛰는 방어적 등록입니다.
def register(ctx):
ctx.register_hook("pre_gateway_dispatch", pre_gateway_dispatch)
ctx.register_hook("transform_llm_output", transform_llm_output)
# foreground 훅이 켜져 있으면 입력/렌더 미러 훅도
ctx.register_hook("cli_input_before_submit", cli_input_before_submit)
ctx.register_hook("cli_response_after_render", cli_response_after_render)
# 네이티브 스레드 도구(LLM이 1급 도구로 선택)
for spec in _THREAD_TOOLS:
ctx.register_tool(spec["name"], "openarms_threads",
spec["schema"], spec["handler"])각 훅이 하는 일:
pre_gateway_dispatch— 사람 메시지가 모델에 닿기 전에 OpenArms가 판단합니다. Hermes는skip/rewrite/allow만 받지만, OpenArms는 더 풍부한route결정을 계산해 해당 턴을 지정된 foreground tmux pane에 직접 제출합니다 (OPENARMS_HERMES_ROUTE_EXECUTOR=tmux). 라우트가 성공하면 공식 응답 미러가 켜져 있을 땐skip(두 번 답하지 않게), 미러가 없으면allow(사용자가 답은 받게)를 돌려줍니다.transform_llm_output— 모델 출력이 사용자에게 가기 전에 다듬습니다(rewrite/suppress).cli_input_before_submit/cli_response_after_render— foreground CLI의 입력과 렌더된 응답을 미러링하고, 미러된 텔레그램 버블이 두 번째 턴으로 재진입하지 않게 억제 키(suppress ledger)를 만듭니다.
네이티브 스레드 도구(openarms_thread_list/_read/_create/_rename/_delete_batch
/_archive_batch/_restore …)는 공식 register_tool로 등록돼 모델이 내장 메시징
목록 대신 이걸 1급 도구로 고릅니다. 모든 핸들러는 공개 안전 문자열(번호 목록·제목만)을
돌려주고, telethon 같은 무거운 의존성은 호출 시점에 지연 import합니다. MTProto fetch가
멈춰도 전체가 굳지 않도록 도구 한 번 호출은 새 스레드+새 이벤트 루프에서 30초로 바운드
됩니다. 삭제는 confirm_permanent 플래그로 가드하고, 한 번에 최대 40개로 제한해
~160개 유령 토픽을 한 호출로 쓸어 wall-time을 넘기는 일을 막습니다(나머지는 보고해
다시 호출하게 합니다). env 키 전체 표면은 레포의 integrations/hermes/README.md가
권위입니다.
AGY (Antigravity CLI) — worker(계약) provider
왜 이걸 골랐나. 워커 역은 실제 일(첨부 읽기, 이미지 역검색, 화면 캡처 인증)을 하고
증거를 PM에게 돌려주는 역입니다. Antigravity CLI는 네이티브 첨부/파일 처리와 MCP
(Model Context Protocol) 로딩을 갖춰, 워커가 받은 첨부를 그 자리에서 다루고 스레드
도구를 네이티브로 호출할 수 있습니다. 또 AGY의 대화는 비공개 DM(is_forum=false)이라
PM 쪽 forum과 성격이 다른데, 이 차이를 provider 어댑터가 흡수하므로 같은 역할 계약을
공유합니다.
어떻게 구성하나. AGY는 두 갈래로 OpenArms에 붙습니다.
- 네이티브 플러그인 번들(
integrations/antigravity/openarms_provider/):plugin.json+commands/openarms-worker.md+skills/openarms-worker/SKILL.mdhooks/openarms.json. SKILL은 “이미지의 인물/출처/링크를 물으면 자체 비전이나 드래그&드롭이 아니라openarms-lens-identifyCLI를 한 번 호출하라” 같은 워커 규율을 못박습니다(자체 CUA는 느리고 PM과 공유 자원인 클립보드·바탕화면을 오염시킴).
- MCP 서버(
adapter/openarms_worker_threads_mcp.py): stdio JSON-RPC로 MCP를 말하는 의존성 0(stdlib만)인 작은 서버. 컴파일된agyCLI가~/.gemini/config/mcp_config.json을 통해 로드해openarms_thread_list/openarms_thread_delete/openarms_thread_delete_batch등을 네이티브 도구로 호출합니다. 봇 토큰·chat id는 하드코딩이 아니라 런타임에 AGY env 키에서 해석합니다.
AGY의 DM에는 읽을 토픽 디렉터리가 없으므로(MTProto가 0을 반환), 생성된 토픽의 유일한
durable 기록은 라우터 레지스트리의 누적 dynamic_bindings입니다
(OPENARMS_AGY_THREAD_REGISTRY, 기본 state/agy-pm-thread-router-live.json).
배포판에서 훅을 어떻게 쓰나. AGY 훅 매니페스트는 역할·attachment 정책을 선언합니다.
{
"openarms_provider_bridge": {
"role": "worker",
"provider": "antigravity",
"authority": "official_antigravity_cli_surface",
"attachment_policy": "use_native_attachment_surface_before_fallback"
}
}attachment_policy가 “네이티브 첨부 표면을 폴백보다 먼저”라고 못박는 게 핵심입니다 —
워커가 옛 스크린샷·stale 터미널 출력으로 추측하지 않고 현재 첨부를 권위로 삼게
강제합니다. 스크린샷을 보내야 할 땐 응답 본문에 MEDIA:/절대경로 한 줄을 적으면 OpenArms
엔진이 그 마커를 읽어 질문이 온 바로 그 텔레그램 스레드로 파일을 보내고, 마커는 사람이
보는 답변에서 자동 제거합니다. 워커는 PM 판단(최종 성공·보드 상태·토픽 삭제)을 대신
내리지 않고, 자기 레인의 로컬 스레드 상태만 갱신하거나 owner-runtime 액션 후보를 PM에게
올립니다. 라이브 testbed의 현 바인딩은 hermes=PM / antigravity=worker이지만 이는
인스턴스일 뿐 제품 기본값이 아니며, fleet 도구는 이 바인딩을 config에서 읽어야
합니다(하드코딩 금지).
tmux — foreground 실행석(execution seat)
왜 이걸 골랐나. PM/워커 provider CLI는 사람처럼 살아 있는 foreground 세션이어야
합니다. tmux는 분리(detached)된 채로 계속 사는 세션을 주고, 붙어 있지 않아도 외부에서
send-keys로 키를 밀어 넣을 수 있어 “라우트 결정 → pane에 턴 제출”이 자연스럽습니다.
nohup/screen보다 이름 기반 세션·스크립트 친화성·재접속이 우수해 오래 도는 대화형
프로세스에 더 신뢰할 만하다는 게 일반적 평가입니다(tmux 세션 관리 가이드).
다만 tmux는 제품 정체성이 아니라 현 구현의 backend일 뿐입니다 — 로드맵은
execution_seat를 tmux/pty/ssh/windows-terminal/provider-native로 추상화해 tmux에
못 박지 않는 방향입니다. 그래서 WSL/Linux/macOS/Mac VM이 선호 환경이고 Windows native는
비주력(GUI·문서·텔레그램 확인 쪽)입니다.
어떻게 구성하나. 분리 세션을 이름으로 띄우고, pane을 provider에 바인딩합니다.
# 워커/ PM CLI를 붙잡아 둘 분리 세션
tmux new-session -d -s fleet -n openarms-pm
# 이후 외부 프로세스가 이 pane에 턴을 밀어 넣는다
tmux send-keys -t fleet:openarms-pm.0 '안녕하세요, 상태 보고' Enter이름 있는 세션을 쓰는 이유는 숫자 id보다 attach/switch/스크립팅이 훨씬 쉽기 때문입니다.
바인딩은 env로 선언합니다 — PM은 OPENARMS_HERMES_CLI_PANE/..._SESSION, 워커는
OPENARMS_AGY_FOREGROUND_SESSION/..._PANE, 그리고 Hermes 브리지가 라우트를 실행할
때 쓰는 OPENARMS_HERMES_ROUTE_EXECUTOR=tmux, OPENARMS_TMUX_BIN(tmux 실행 파일
경로). 주의할 점은 호스트 재부팅이 tmux 서버를 죽인다는 것 — 영속이 필요하면
재기동 스크립트/서비스 매니저로 세션을 복구해야 합니다.
배포판에서 훅을 어떻게 쓰나. 배포판의 얇은 인젝터가 tmux pane을 “받는 입”으로 씁니다.
run_pm_injector.sh는 텔레그램 backlog를 건너뛰도록 getUpdates offset을 현재 최대
update_id+1로 초기화한 뒤, 텔레그램→pane 인젝터를 nohup으로 분리 기동합니다.
export OPENARMS_PM_PANE="${OPENARMS_PM_PANE:-fleet:openarms-pm.0}"
export OPENARMS_TMUX="${OPENARMS_TMUX:-tmux}"
nohup python3 .../openarms_provider/inject_listener.py \
>> "$OPENARMS_HOME/logs/pm_injector.log" 2>&1 & disown이렇게 하면 Hermes 브리지의 pre_gateway_dispatch 라우트가 같은 pane을 실행기로 삼아
“사람 말 → tmux pane 제출 → 응답 미러 → 텔레그램 회신”이 한 좌석 위에서 닫힙니다.
Telethon (MTProto) — 텔레그램 전송층
왜 이걸 골랐나. OpenArms는 텔레그램의 **포럼 토픽(스레드)**을 칸반 레인으로 직접
부립니다 — 토픽을 만들고, 이름 바꾸고, 내용을 읽고, 지우고 복구합니다. Bot API만으로는
이 중 상당수가 막힙니다: Bot API는 임의 message id로 메시지를 가져오지 못하고,
같은 봇의 인스턴스를 동시에 둘 띄우면 옛 인스턴스가 업데이트를 못 받으며, 파일도 다운로드
20MiB·업로드 50MiB로 제한됩니다. MTProto(Telethon)는 messages.getMessages로 임의
메시지를 읽고, 동시 세션을 허용하며, 최대 2GiB 파일을 다룹니다(Telethon: Bot API
vs MTProto). 그래서 OpenArms는 owner의 MTProto 세션을 읽기·토픽 변형의
권위로 쓰고, 단순 전송은 Bot API와 병행합니다.
어떻게 구성하나. Telethon은 PM 브리지의 pip_dependencies(telethon>=1.36)이자
설치 문서의 필수 의존성입니다 — 없으면 테스트 스위트가 아예 안 돕니다. owner의
MTProto 세션 파일은 이 호스트에 있고(레포에 커밋 금지), 토큰·chat은 owner-local 비밀로
둡니다. 한 가지 함정: 읽기용 chat과 변형용 chat이 다릅니다. editForumTopic/close/
reopen 같은 변형은 Bot-API용 owner chat id를 쓰지만, MTProto get_messages (reply_to=topic_id) 읽기는 그 id에 PeerIdInvalidError를 냅니다 — 읽기는 토픽이
사는 forum peer(chat_selector)를 써야 합니다. OpenArms 어댑터가 이 둘을 분리해
잡아 줍니다.
배포판에서 훅을 어떻게 쓰나. 앞서 본 Hermes 네이티브 스레드 도구들이 곧 Telethon
훅 지점입니다 — 각 핸들러가 topic_inventory_report/fetch_topic_recent_messages
같은 MTProto 호출을 30초 바운드 스레드 안에서 부르고, 결과는 공개 안전 문자열로만
돌려줍니다. 첨부 전송은 OpenArms 자체 미디어 transport(send_media_auto)로 나가
Bot API 50MiB 천장을 넘는 원본도 보냅니다. 워커(AGY) 쪽은 forum이 없어 MTProto 디렉터리
읽기가 0을 반환하므로, 토픽 기록은 레지스트리 dynamic_bindings로 보존하고 도구는 그
id 목록을 권위로 씁니다.
venv — 설치·환경 격리
왜 이걸 골랐나. OpenArms 코어는 telethon·[jsonschema](https://json-schema.org/) 같은 무거운 의존성을 끌고,
provider CLI(Hermes/AGY)는 각자 별도 venv를 가집니다. 표준 라이브러리 venv는 추가
설치 없이 Python 3.3+에 들어 있어 “처음 설치하는 사람”의 마찰이 가장 적습니다
(packaging.python.org). 빠른 대안 [uv](https://docs.astral.sh/uv/)는 venv보다 환경 생성이 훨씬
빠르고 pip/venv/build를 한 도구로 묶지만 가상환경을 기본 강제하는 더 의견 있는 방식이라,
배포 기본값은 표준 venv로 두고 uv pip install -e .는 선택지로 둡니다(uv vs
pip). 핵심은 editable 설치(pip install -e .) — 레포를 고친 즉시
설치본에 반영돼, 라이브 어댑터/브리지를 만지며 돌리는 OpenArms 개발 흐름과 맞습니다.
어떻게 구성하나. 권위는 pyproject.toml/requirements-dev.txt이고, Quickstart는
한 손에 들어옵니다.
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest -q테스트는 **WSL/Linux/macOS의 python3**로 레포 루트에서 돌립니다. Windows의
hermes-agent venv는 pytest/telethon이 없어 지원 러너가 아닙니다. 그 인터프리터로
의존성이 갖춰졌는지 먼저 보려면 import-only 사전점검을 돌립니다.
python3 scripts/dev_preflight.py # 필수 의존성 다 있으면 exit 0배포판에서 훅을 어떻게 쓰나. 배포판 PM 좌석 부트스트랩(setup_pm_seat.sh)이
스킬을 깔고 ~/.openarms를 초기화한 뒤 토큰 자격증명을 ~/.openarms/credentials/ telegram.env(0600)로 옮깁니다. 이 env가 venv 안에서 source돼 Telethon 토큰·chat이
해석되고, 그 위에서 인젝터·브리지가 같은 인터프리터를 공유합니다 — 즉 venv는 단순
설치 도구가 아니라 “어떤 Python이 토큰을 들고 provider 훅을 돌리는가”의 경계입니다.
시나리오별 활용 사례
1) 혼자 부리는 개발자 — “지금 막힌 게 뭐야”
WSL의 tmux fleet 세션에 PM(Hermes) 좌석이 떠 있고, 인젝터가 텔레그램→pane으로 사람
말을 흘려보냅니다. 사용자가 텔레그램으로 “지금 막힌 거 뭐야?”라고 치면
pre_gateway_dispatch가 그 턴을 pane에 제출하고, PM이 워커 증거를 모아 “X에서 권한이
없어 멈춤, 다음은 당신이 승인할 차례”처럼 사람 말로 회신합니다. 사용자는 raw id나
터미널 출력을 한 줄도 읽지 않습니다.
2) 여러 워커를 조율하는 PM — 위임과 수용·재작업
PM이 일을 워커(AGY)에게 위임하면, AGY는 네이티브 첨부 표면으로 받은 파일을 읽거나
openarms-lens-identify로 이미지 출처를 찾아 증거(검토한 것·확인한 것·막힌 것·다음
PM 판단)를 같은 스레드로 돌려줍니다. PM은 그 증거로 수용/재작업/보류를 판단하고, 결정은
칸반 노드로 보존됩니다.
3) 진행을 추적하는 팀 — 텔레그램 토픽 = 칸반 레인
영구 5레인(접수/진행/검증/완료/보관)이 텔레그램 포럼 토픽으로 살아 있고, Telethon이
이 토픽들을 만들고 읽고 정리합니다. 잡설·유령 토픽이 쌓이면 PM이
openarms_thread_delete_batch(40개 캡, 나머지 보고)로 안전하게 정리하되 레인 자체는
지우지 않습니다. 팀은 “어떤 워커가 어떤 계약 아래 있고 무엇이 증거인가”를 같은 보드로
봅니다.