개요
OpenArms는 특정 에이전트 런타임을 하드코딩하지 않는다. 어댑터가 기동·입력 전달·출력 포착·결과 마커 인식을 할 수 있으면 어떤 CLI 에이전트든 워커로 붙는다. 핵심 원칙은 하나다 — 역할(role)이 안정적인 개념이고, 프로바이더(provider)는 그 역할을 채우는 교체 가능한 구현이다. 라이브 테스트베드는 지금 Hermes를 PM 역할에, Antigravity(AGY)를 워커 역할에 바인딩하지만, 이건 그 인스턴스의 바인딩이지 제품 기본값이 아니다. 같은 프로바이더가 어느 역할이든 채울 수 있고, 역할은 프로바이더 이름을 따라 개명되지 않는다.
이 장은 이 장이 건드리는 모든 도구/서비스를 — Hermes, AGY/Antigravity, tmux, Telethon, venv/uv, 그리고 그 사이의 전송(mailbox) — 왜 골랐는지·어떻게 구성하는지·배포판에서 훅을 어떻게 거는지까지 구체적으로 다룬다. 더 넓은 역할 모델은 핵심 개념, 설치 절차는 빠른 시작·설치, 어댑터를 직접 짜는 절차는 개발자 가이드를 본다.
Hermes — PM 프로바이더
왜 Hermes인가 (선택 근거)
Hermes CLI는 라이브 테스트베드에서 PM(코디네이터) 역할을 맡는다. 고른 이유는 Hermes가 공식 플러그인 훅 레이어를 노출하기 때문이다. 즉 OpenArms를 붙이려고 Hermes core를 패치하거나 사이드카 텔레그램 모니터를 띄울 필요가 없다 — 게이트웨이 디스패치와 CLI 입출력 지점에 콜백을 등록만 하면 된다. 대안(코어 포크, 외부 폴링 데몬)은 업스트림 업데이트마다 깨지고, 권위가 두 곳으로 갈라진다. 훅 등록 방식은 Hermes가 자기 업데이트를 받아도 브리지가 살아남는다.
어떻게 구성하는가 (스택)
Hermes 프로바이더가 소유하는 것:
- 플러그인 브리지 —
integrations/hermes/openarms_contract_bridge/. 공식 Hermes 플러그인으로 로드되는 훅 어댑터다. 매니페스트는plugin.yaml, 훅 진입점은__init__.py, 나머지는attachment_sender.py·current_inbound.py·registry_bindings.py·thread_deletion_tools.py·thread_tool_registry.py같은 헬퍼다. - PM 리스너(중립 코어) —
scripts/telegram_thread_router_listener.py. Hermes가 PM 프로바이더로서 구동하지만, 리스너 자체는 프로바이더 비의존이라 중립 코어에 남는다.
브리지는 레포 밖에 설치된다(예:
~/.hermes/hermes-agent/plugins/openarms_contract_bridge). 그래서 import openarms가
되려면 레포 루트가 sys.path에 있어야 한다. 브리지의 _bootstrap_openarms_root()는 먼저
OPENARMS_ROOT env를 보고, 없으면 ~/openarms 같은 알려진 체크아웃 위치와 자기 파일의
상위 디렉터리(openarms/를 실제로 담은 후보)까지 폴백해서 임포트를 복구한다. 이 폴백이
없으면 OPENARMS_ROOT 없이 리스폰된 세션이
Failed to load plugin 'openarms-contract-bridge': No module named 'openarms'로 죽고,
네이티브 openarms_thread_* 툴이 통째로 사라져 모델이 raw 터미널 CLI로 떨어진다.
plugin.yaml이 선언하는 계약:
name: openarms-contract-bridge
version: "0.2.0"
description: "OpenArms foreground contract bridge for Hermes official plugin hooks."
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_renderenv 표면은 OPENARMS_HERMES_* 접두로 갈래가 나뉜다 — 바이너리/프로세스
(OPENARMS_HERMES_BIN, ..._SESSION_ID), tmux 좌석(..._CLI_PANE,
..._GATEWAY_PANE, ..._FOREGROUND_SESSION), CLI 미러(..._CLI_INPUT_MIRROR,
..._CLI_RESPONSE_MIRROR), 라우팅/계약(..._ROUTE_TARGET, ..._ROUTE_LEDGER,
..._ROUTE_MAX_PENDING). 엔진이 역할 우선 명명으로 일반화되면 이 키들은 중립
OPENARMS_* PM-역할 키로 승계되지만, 오늘의 라이브 키는 OPENARMS_HERMES_*다.
배포판에서 훅을 어떻게 거는가
브리지는 Hermes의 공식 register(ctx) 진입점 하나로 자기를 꽂는다. 지원되지 않는 훅은
조용히 건너뛴다(_register_if_supported는 register_hook이 던지면 False를 반환). 짧은
예시:
HERMES_HOOK_PRE_GATEWAY_DISPATCH = "pre_gateway_dispatch"
HERMES_HOOK_TRANSFORM_LLM_OUTPUT = "transform_llm_output"
HERMES_HOOK_CLI_INPUT_BEFORE_SUBMIT = "cli_input_before_submit"
HERMES_HOOK_CLI_RESPONSE_AFTER_RENDER = "cli_response_after_render"
def register(ctx):
_register_if_supported(ctx, HERMES_HOOK_PRE_GATEWAY_DISPATCH, pre_gateway_dispatch)
_register_if_supported(ctx, HERMES_HOOK_TRANSFORM_LLM_OUTPUT, transform_llm_output)
if _register_foreground_hooks_enabled():
_register_if_supported(ctx, HERMES_HOOK_CLI_INPUT_BEFORE_SUBMIT, cli_input_before_submit)
_register_if_supported(ctx, HERMES_HOOK_CLI_RESPONSE_AFTER_RENDER, cli_response_after_render)
# 네이티브 thread 툴도 ctx.register_tool 로 등록 — LLM이 1급 툴로 선택
_register_thread_tools(ctx)각 훅의 역할:
pre_gateway_dispatch— 인바운드가 게이트웨이로 들어오기 전에 가로채 라우팅 결정과 현재 인바운드 스레드 컨텍스트를 세팅한다.transform_llm_output/cli_response_after_render— 사람에게 보이기 전에 출력에서 내부 마커/raw id를 걷어내고 역할 헤더([PM]/[Worker])를 보장한다.cli_input_before_submit— CLI에 제출되기 직전의 입력을 미러로 포착한다.
register_tool을 통해 등록되는 네이티브 thread 툴(openarms_thread_list,
openarms_thread_delete_batch 등)은 Hermes의 기본 메시징/다이얼로그 목록(원본 사용자명과
topic #### 라벨을 그대로 덤프하는)을 대신해 LLM이 1급 툴로 고른다. 각 핸들러는
공개-안전 문자열(번호 목록·제목만)을 반환하고 openarms 내부를 지연 임포트해서 플러그인
임포트가 telethon에 하드 의존하지 않게 한다.
Antigravity (AGY) — 워커 프로바이더
왜 Antigravity인가
Antigravity CLI(agy)는 라이브에서 워커 역할을 맡는다. 고른 이유는 두 가지다. 첫째,
AGY는 네이티브 첨부/파일 처리 표면과 워크스페이스 컨텍스트를 가져, 한 번에 들어온
한 장의 이미지를 한 번의 [PM] 턴으로 받아 일하는 워커 흐름에 맞는다. 둘째, AGY는
stdio JSON-RPC MCP 서버를 워커의 네이티브 스레드 어포던스로 붙일 수 있어, 워커가
PM과 동일한 툴-백드 증거(스레드 목록·삭제 계획)를 만든다.
어떻게 구성하는가
AGY 프로바이더가 소유하는 것:
- 페르소나/플러그인 —
integrations/antigravity/:agy-global-AGENTS.md— 전역 워커 페르소나/규칙(대화 우선, 강한 툴 게이트).openarms_provider/— Antigravity 네이티브 플러그인 번들:plugin.json,commands/openarms-worker.md,hooks/openarms.json,skills/openarms-worker/SKILL.md.
- 워커-스레드 MCP —
integrations/antigravity/adapter/openarms_worker_threads_mcp.py. AGY env 키/agy.env/AGY 라우터 레지스트리를 해석하는 프로바이더 특정 stdio JSON-RPC MCP 서버. 기존scripts/경로에는 얇은 re-export 심이 남아 라이브mcp_config.json진입점이 계속 해석된다. - 리스너/런타임 —
scripts/agy_telegram_listener.py(유저의 AGY 봇 채팅 메시지를 AGY foreground 페인으로 넣는 인바운드 브리지),scripts/agy_cli_to_telegram_mirror_once.py(완료된 한 턴을 final-only로 텔레그램에 1회 미러).
env 표면은 OPENARMS_AGY_*이고, 중립 승계 키는 OPENARMS_WORKER_*다(예:
OPENARMS_WORKER_BOT_TOKEN ← OPENARMS_AGY_TELEGRAM_BOT_TOKEN). fail-closed 게이트는
OPENARMS_AGY_TELEGRAM_CHAT_ID — 이 키가 없으면 AGY는 복구되지 않는다(과거 fail-closed
회귀의 가드).
배포판에서 훅을 어떻게 거는가
AGY는 선언적 훅 매니페스트(hooks/openarms.json)로 브리지를 푼다. 짧은 예시:
{
"openarms_provider_bridge": {
"role": "worker",
"provider": "antigravity",
"authority": "official_antigravity_cli_surface",
"attachment_policy": "use_native_attachment_surface_before_fallback"
}
}읽는 법: 이 세션은 워커 역할이고, 권위는 공식 Antigravity CLI 표면(임의 스크래치
스크립트가 아니라)이며, 첨부는 폴백 전에 네이티브 첨부 표면을 먼저 쓴다. 워커 스킬
(skills/openarms-worker/SKILL.md)은 이 권위를 행동 규칙으로 강제한다 — 예를 들어 이미지
식별 요청은 자체 비전/CUA로 처리하지 않고 openarms-lens-identify CLI를 한 번 호출하며,
파일 전송은 응답 본문에 MEDIA:/절대경로 한 줄을 적어 엔진이 그 마커를 읽어 해당
텔레그램 스레드로 보내고 마커를 사람 답변에서 제거하게 한다.
전송(Transport) — 산출물 우편함
전송은 로컬 간 가져올 수 있는 산출물의 우편함이다. 워커 계약 레인도, 기억의 권위도, 정리 권한도 아니다 — ACK·REJECT·재시도·격리를 가진 메일박스다.
여기서 중요한 경계는 텔레그램 ≠ 워크플로 DB라는 것이다. 텔레그램 Bot API는 전송 (getUpdates/webhook으로 받고, sendMessage/sendPhoto/sendDocument로 보냄)이고, 들어온 업데이트는 봇이 받을 때까지만, 그것도 24시간을 넘기지 않고 보관된다. 그래서 PM 기억·워커 핸드오프·RAG/wiki 승격·감사 로그·장기 복구는 텔레그램이 아니라 OpenArms 원장/볼트가 권위다.
텔레그램 = 보이는 대화 표면 + Bot API 전송 + 제한된 앱 상태
OpenArms = 내구성 워크플로 상태 + 기억 볼트 + 증거 원장 + 전송 로그브리지의 인바운드 컨텍스트는 runtime/hermes-pm-current-inbound-thread.json(또는
OPENARMS_HERMES_PM_CURRENT_THREAD_STATE 오버라이드)에 chat_id/message_thread_id/
reply_to_message_id로 박혀, 응답이 들어온 그 스레드로 정확히 돌아가게 한다.
tmux — foreground 실행석(execution seat)
왜 tmux인가
provider CLI(Hermes/AGY/Claude)는 사람이 보는 foreground 세션 안에 살아 있어야 한다 —
대화 컨텍스트, 인증 좌석, 화면 상태가 그 세션에 묶이기 때문이다. tmux는 그 좌석을 잡아두는
backend다. 고른 이유: 세션 지속성(터미널이 떨어져도 프로세스가 산다), 프로그래밍적
페인 제어(send-keys로 입력 주입), 출력 포착(capture-pane로 화면 읽기). 이 세
가지가 “CLI를 잡아두고, 입력을 넣고, 출력을 뽑는” 어댑터 계약과 정확히 맞는다.
중요: tmux는 제품 정체성이 아니라 현재 구현의 backend다. 로드맵은 execution_seat를
tmux/pty/ssh/windows-terminal/provider-native로 추상화하는 방향이다 — tmux를 코어에
굳히지 않는다. WSL/Linux/macOS/Mac VM에서 자연 동작하고, Windows native는 비주력이다.
어떻게 구성하는가
핵심 패턴은 detached 세션 + 명명이다:
# 좌석을 백그라운드로 만든다 (-d = 붙지 않고 생성, 이름 필수)
tmux new -s fleet -d
# 입력 주입: 대상은 session:window.pane
tmux send-keys -t fleet:openarms-pm.0 '안녕' Enter
# 화면 포착: 스크롤백 포함
tmux capture-pane -t fleet:openarms-pm.0 -pOpenArms는 이 좌표(session:window.pane)를 env로 받는다 — OPENARMS_PM_PANE
(기본 fleet:openarms-pm.0), OPENARMS_HERMES_CLI_PANE,
OPENARMS_AGY_FOREGROUND_SESSION/OPENARMS_AGY_PANE 등. tmux 바이너리 자체는
OPENARMS_TMUX/OPENARMS_TMUX_BIN으로 가리킨다. 세션 이름은 항상 의미 있게 짓는다 —
무명 세션은 숫자 id(0,1,2…)로 떨어져 fleet 도구가 좌석을 잃는다.
배포판에서 훅을 어떻게 쓰는가
배포판의 주입 리스너가 이 패턴을 그대로 쓴다 — 인바운드를 받아 PM 페인으로 넣는 얇은
인젝터를 nohup으로 띄워 인터랙티브 세션에서 분리한다(짧은 예시):
export OPENARMS_PM_PANE="${OPENARMS_PM_PANE:-fleet:openarms-pm.0}"
export OPENARMS_TMUX="${OPENARMS_TMUX:-tmux}"
# 백로그 스킵 후 인젝터 기동 (decoupled)
pkill -f "openarms_provider/inject_listener.py" 2>/dev/null || true
nohup python3 "$REPO/integrations/claude_code/openarms_provider/inject_listener.py" \
>> "$OPENARMS_HOME/logs/pm_injector.log" 2>&1 &
disown리스너는 좌표 env를 읽어 send-keys로 텍스트를 페인에 넣고, 결과는 capture-pane(또는
프로바이더 미러 훅)으로 포착해 전송 우편함으로 되돌린다.
Telethon — Telegram MTProto 클라이언트
왜 Telethon(MTProto)인가, Bot API 대비
OpenArms의 현재 first messaging provider는 텔레그램이고, 두 경로가 있다 — HTTP Bot API와 MTProto. 일반 전송(메시지/사진/문서)은 Bot API로 충분하다. 하지만 OpenArms가 하는 일 중 일부는 Bot API의 공개 표면을 넘는다:
- 토픽/스레드 일괄 삭제·정리 — MTProto는 한 번에 최대 100개 메시지를 bulk
delete/forward 한다.
openarms_thread_delete_batch의 영구 삭제 의미가 여기서 나온다. - 큰 파일 — Bot API는 다운로드 20MiB/업로드 50MiB 한계인데, MTProto는 2GiB까지 된다.
- 임의 메시지 id로 조회, owner 세션 수준의 토픽 열거 — 안전한 삭제 절차(열거 → 보고 → 명시 id 목록만 삭제)에 필요한 권한.
그래서 공개 전송은 Bot API, owner-runtime 토픽 조작은 MTProto(Telethon) 로 갈린다. Telethon은 순수 파이썬 asyncio MTProto 구현이라 셋업이 쉽고, 이벤트 구동·엔티티 해석이 에이전트 자동화에 잘 맞는다. (대안 TDLib은 C++ 바인딩이라 무겁다.)
⚠️ owner MTProto 세션은 강력하다 — 토픽 삭제는 반드시 열거→보고→명시 id 목록만 삭제로 진행하고, blanket sweep은 금지다(과거 over-delete 사고의 교훈).
어떻게 구성하는가
- 의존성:
telethon(필수). Hermes 브리지는telethon>=1.36을pip_dependencies로 선언한다. 단, 플러그인 임포트 경로는 telethon에 하드 의존하지 않게 내부를 지연 임포트한다 — telethon이 없는 환경에서도 플러그인 로드 자체는 살아 있어야 하기 때문. - 비밀값: bot token / chat destination / MTProto 세션은 owner-local secret이다. 레포에
커밋하지 않고
~/.openarms/credentials/telegram.env또는.env로 둔다 (.env.example참조). 배포 스크립트는 토큰을 출력하지 않고 자격 파일로 옮긴 뒤 권한을600으로 잠근다. - flood control: MTProto는 사용자당 30 msg/sec 같은 flood 제한이 있으므로 미러/전송은
사이클당 전송 상한(
..._CLI_MIRROR_MAX_SEND_PER_CYCLE)과 간격 (..._CLI_MIRROR_INTERVAL)으로 조절한다.
배포판에서 훅을 어떻게 쓰는가
브리지의 네이티브 thread 툴이 곧 MTProto 진입점이다. register_tool로 등록된
openarms_thread_list/openarms_thread_delete_batch/openarms_thread_send_attachment가
호출될 때만 telethon 내부를 지연 임포트한다. 따라서 배포판에서 훅 연동은 “telethon을 항상
임포트”가 아니라 “툴이 호출될 때 owner 세션을 연다”는 형태다. 텔레그램 표면의 더 넓은 설명은
메시징 플랫폼을 본다.
venv / uv — 격리 실행 환경
왜 격리 환경인가, 그리고 venv vs uv
OpenArms는 telethon·[jsonschema](https://json-schema.org/)·pytest 등 외부 의존성을 쓰고, 같은 호스트에 여러
provider venv(예: Windows의 hermes-agent venv)가 공존한다. 그래서 레포 전용 격리
환경이 필수다 — 호스트 전역 파이썬을 오염시키면 어느 인터프리터로 테스트가 도는지
모호해진다(실제로 Windows hermes venv에는 pytest/telethon이 없어 테스트 수집 자체가
안 된다).
- venv — 파이썬 3.3+에 내장된 표준 격리. 추가 설치 없이 어디서나 되는 신뢰 기반.
공식 권위(
pyproject.toml/requirements-dev.txt)를 그대로 쓴다. - uv — Astral의 Rust 기반 드롭인 대체(supersede)(pip/virtualenv/pip-tools를 대신). venv 생성이
훨씬 빠르고, 전역 캐시 재사용으로 공통 의존성을 즉시 설치하며,
uv.lock으로 정확한 설치 버전을 고정해 재현성을 준다. 2026 신규 프로젝트의 기본 권장.
OpenArms는 둘 다 받는다 — 표준 보장은 venv로, 빠른/재현 설치는 uv로. 권위는 어디까지나
코드 메타데이터(pyproject.toml)다.
어떻게 구성하는가
표준(venv) 경로:
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest -q빠른(uv) 경로 — 같은 pyproject.toml을 읽는다:
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"테스트는 WSL/Linux/macOS의 python3 로 레포 루트에서 돌린다. 한글 리터럴 출력이
깨지지 않게 PYTHONIOENCODING=utf-8을 둔다:
PYTHONIOENCODING=utf-8 python3 -m pytest -q의존성 계층(권위는 pyproject.toml/requirements-dev.txt):
- 필수 — 없으면 스위트가 안 돈다:
pytest,telethon,jsonschema. - 권장 — 없으면 실패가 아니라 SKIP 되어 커버리지가 조용히 빠진다:
[networkx](https://networkx.org/)(wiki_chain 그래프),pyyaml(node_contract 권위 렌더). - 선택 — 없어도 통과, 분기만 켜짐:
leidenalg+python-igraph(community Leiden 분기), 래스터라이저resvg_py/cairosvg(graph_network PNG 단계).
배포판에서 훅을 어떻게 쓰는가
배포(seat 설치) 스크립트는 격리 환경을 가정하고 동작한다 — 스킬 파일을 좌석의
~/.claude/skills/로 복사하고, ~/.openarms를 bootstrap 한 뒤, 토큰을
~/.openarms/credentials/telegram.env로 옮겨 chmod 600으로 잠근다(짧은 예시):
python3 openarms/paths.py bootstrap >/dev/null 2>&1
sed -E 's/^export //' "$TOK_SRC" > ~/.openarms/credentials/telegram.env
chmod 600 ~/.openarms/credentials/telegram.env설치 전에 그 인터프리터로 한 번 preflight를 돌려 필수 의존성 PASS/MISSING을 확인한다 (import-only 검사, 설치/네트워크 없음):
python3 scripts/dev_preflight.pyCLI 에이전트 생태계 (현재·다음)
Hermes와 AGY/Antigravity가 현재 증명 프로바이더 한 쌍이다. 2026년 현재 터미널 네이티브 CLI 에이전트 생태계는 빠르게 넓어지고 있어 — Claude Code, OpenAI Codex, Google Antigravity(구 Gemini CLI 후속), 오픈소스의 OpenCode·Aider·Goose 등 — OpenArms의 중립 계약 모델은 이들 중 어댑터가 갖춰진 것을 워커로 받아들이는 것을 목표로 한다. 어댑터 작성 절차는 개발자 가이드를 본다.
연동은 모두 공식 CLI 계약으로 표현된다 — 어느 고용 세션이 권위인지, 위임된 계약 세션은 무엇인지, 입력을 어떻게 보내고 출력을 어떻게 포착하며, 사람에게 보이기 전에 터미널 잉크를 어떻게 걷어내고, 어떤 증거가 계약을 닫는지.
출처(코드/문서): integrations/README.md · integrations/hermes/ ·
integrations/antigravity/ · docs/20_installation/README.md ·
docs/30_providers/telegram/.
출처(외부 모범): HTTP Bot API vs MTProto — Telethon docs ·
MTProto vs HTTP Bot API (Telethon Wiki) ·
tmux(1) manual ·
Using environments — uv docs ·
awesome-cli-coding-agents ·
Best Terminal AI Coding Agents 2026 (amux)