픽셀 대신 접근성 트리를 읽는 컴퓨터 유즈: vercel-labs/agent-desktop v1.0.2의 스냅샷 ref·엄격한 정체성 해석·전달 의미론·헤드리스 정책 심층 해부와 Linux 박스 실측

한 줄 요약: vercel-labs/agent-desktop은 AI 에이전트가 데스크톱 앱을 스크린샷 픽셀이 아니라 운영체제 접근성 트리로 읽고 조작하게 해 주는 Rust CLI·C 라이브러리입니다. 2026년 11월 3일 오전(KST) GitHub Trending 전체 1위(하루 +2,275★, 누적 10,116★)에 올랐습니다. 이 글은 v1.0.2(Apache-2.0)를 기준으로 구조를 뜯어보고, 박스에서 Xvfb·xfwm4·AT-SPI 위에 GTK3 테스트 앱을 띄워 실제로 돌려 본 결과를 정리합니다.

agent-desktop의 기본 루프: snapshot -i로 접근성 트리와 ref를 받고, 그 ref로 헤드리스 행동을 한 뒤, 다시 snapshot으로 확인한다. 모든 명령은 ok·data/error·disposition을 담은 JSON 봉투 하나를 출력한다
agent-desktop의 기본 루프: snapshot -i로 접근성 트리와 ref를 받고, 그 ref로 헤드리스 행동을 한 뒤, 다시 snapshot으로 확인한다. 모든 명령은 ok·data/error·disposition을 담은 JSON 봉투 하나를 출력한다

1. 왜 지금 트렌딩 1위인가

컴퓨터 유즈(computer use) 에이전트는 보통 화면을 찍고, 멀티모달 모델이 버튼 위치를 추정하고, 그 좌표를 클릭하는 방식으로 돌아갑니다. 범용성은 좋지만 해상도·테마·창 배치가 조금만 달라져도 빗나가고, 클릭이 실제로 들어갔는지 확인하려면 다시 찍어서 다시 추론해야 합니다. agent-desktop은 처음부터 다른 길을 택했습니다. macOS의 AX, Windows의 UI Automation, Linux의 AT-SPI가 이미 갖고 있는 구조화된 UI 트리를 JSON으로 꺼내고, 요소마다 ref를 붙여 에이전트가 그 ref로 행동하게 합니다.

이번 급등의 직접적인 계기는 10월 28일(KST) v1.0.0입니다. 이 릴리스에서 Linux(AT-SPI) 지원이 들어오면서 세 OS를 하나의 CLI·C ABI로 묶은 첫 정식판이 나왔습니다. 이어 v1.0.1·v1.0.2가 배포 자동화와 macOS 권한 안내를 다듬었습니다. 같은 Vercel Labs의 브라우저 자동화 CLI인 agent-browser(오늘 Rust 트렌딩 8위, 45,295★)가 웹을 맡고, agent-desktop이 네이티브 앱을 맡는 구도입니다. 프로젝트 홈페이지도 agent-browser.dev/desktop에 있습니다.

항목 값 (2026-11-03 KST 확인)
저장소 vercel-labs/agent-desktop
설명 Computer use CLI for AI agents
라이선스 Apache-2.0 (LICENSE·Cargo 워크스페이스·npm 패키지 모두 동일)
최신 릴리스 v1.0.2 (2026-10-31 06:17 KST 공개), v1.0.0은 2026-10-28 10:15 KST
스타 / 포크 10,116★ / 679 (GitHub API, 11:57 KST)
트렌딩 전체 1위 (오늘 +2,275★), Rust 1위
생성일 2026-02-20 (KST)
언어·툴체인 Rust 1.89.0, edition 2024, Cargo 워크스페이스 6개 멤버
설치 npm install -g agent-desktop (x64·ARM64 네이티브 바이너리 동봉), 또는 GitHub Releases 아카이브
지원 OS macOS, Windows, Linux (glibc 2.35+, AT-SPI)
최근 커밋 2026-11-03 10:32 KST (macOS Dock·메뉴 막대 상태 항목 접근 수정, #480)

2. 픽셀 루프와 트리 루프는 무엇이 다른가

README의 사용법은 세 줄입니다. 관찰하고(snapshot), ref로 행동하고(click), 다시 관찰합니다. 루프 자체는 에이전트가 쥐고, 도구는 ‘눈과 손’만 제공합니다. 스킬 문서도 “It is a tool, not an agent”라고 못 박아 둡니다.

agent-desktop snapshot --app Finder -i      # 상호작용 가능한 요소와 ref
agent-desktop click @s8f3k2p9:e8            # 그 스냅샷의 ref로 행동
agent-desktop snapshot --app Finder -i      # 다시 관찰해서 확인
관점 스크린샷·좌표 기반 컴퓨터 유즈 agent-desktop (접근성 트리)
입력 신호 PNG 픽셀 → 비전 모델이 위치 추정 역할·이름·상태·가능한 액션이 담긴 JSON 트리
행동 대상 (x, y) 좌표 스냅샷에 묶인 ref (@스냅샷ID:eN)
포커스·커서 대개 실제 마우스를 움직이고 창을 앞으로 끌어옴 기본은 헤드리스: 접근성 API로만 행동하고 커서·포커스를 건드리지 않음
성공 판정 다시 찍어서 다시 추론 disposition.delivery와 post_state로 구조적으로 보고
토큰 비용 이미지 토큰이 매 턴 발생 텍스트 JSON. 큰 앱은 --skeleton·--root로 나눠 읽음
약점 해상도·테마·배치 변화에 취약 접근성을 노출하지 않는 앱(캔버스·게임·일부 커스텀 UI)은 보이지 않음

표의 마지막 줄이 중요합니다. 트리 방식은 정확하고 싸지만, 앱이 접근성 정보를 제대로 내놓을 때만 그렇습니다. 그래서 이 프로젝트는 “무엇을 확실히 알고 무엇을 모르는지”를 표현하는 데 코드의 상당 부분을 씁니다. 아래에서 그 부분을 자세히 봅니다.

3. 저장소 구조: 하나의 core와 세 개의 OS 어댑터

저장소 구조: 플랫폼 중립 core(약 8.5만 줄) 아래 macOS·Windows·Linux 어댑터가 증거를 모으고, CLI·C ABI(FFI)·npm 런처가 출구가 된다. 박스 집계 .rs 2,741개, 약 36.8만 줄
저장소 구조: 플랫폼 중립 core(약 8.5만 줄) 아래 macOS·Windows·Linux 어댑터가 증거를 모으고, CLI·C ABI(FFI)·npm 런처가 출구가 된다. 박스 집계 .rs 2,741개, 약 36.8만 줄

Cargo 워크스페이스는 crates/core, crates/macos, crates/windows, crates/linux, crates/ffi, src(CLI) 여섯 멤버로 되어 있습니다. 박스에서 클론해 세어 보니 .rs 파일 2,741개, 약 36.8만 줄이었습니다(테스트 포함). 가장 큰 크레이트는 Linux 어댑터입니다. AT-SPI가 툴킷마다(GTK3·GTK4·Qt·Chromium) 보고하는 방식이 제각각이고 X11과 Wayland도 따로 다뤄야 해서, 이번 1.0의 무게중심이 어디에 있었는지 그대로 드러납니다.

경로 역할 .rs 줄 수 (박스 집계)
crates/core 플랫폼 중립 계약: 역할·상태·능력 어휘, ref 정체성·해석, actionability 검사, 액션 체인, 세션·트레이스 85,150
crates/macos AXUIElement 읽기, CGEvent 물리 입력, 권한 가이드 43,026
crates/windows UIA 읽기·패턴, SendInput, 무결성 수준(UIPI) 판정 79,045
crates/linux AT-SPI 트리 워커, X11/XTest·Mutter RemoteDesktop 입력, 창 식별(_NET_CLIENT_LIST 등) 123,842
crates/ffi libagent_desktop_ffi C ABI. 프로세스를 새로 띄우지 않고 같은 프로세스 안에서 호출 15,197
src/ CLI 진입점, 인자 파싱, 배치, 명령 정책, 비주얼 디버그 13,040
npm/ Node 런처와 postinstall. 7개 플랫폼 바이너리 동봉(박스 설치 기준 29MB) JS 536줄
skills/ 에이전트용 스킬 문서(공통·OS별·FFI·JEV). 바이너리에도 내장 —
probes/, tests/, benchmarks/ OS 실측 기록(Linux FINDINGS.md 1,054줄), conformance·e2e·실앱 테스트, 로케이터 벤치 —

설계 원칙은 “어댑터는 증거만 모으고, 판단은 core가 한다”입니다. 접근 가능한 이름을 예로 들면, 각 어댑터는 이름 후보 슬롯(라벨·제목·설명 등)과 각 슬롯을 읽었는지 여부만 넘기고, 우선순위 계산은 core가 한 번만 합니다. 어댑터가 각자 이름을 계산하면 우선순위가 두 벌이 되고, 두 벌은 결국 어긋난다는 것이 CONCEPTS.md의 설명입니다.

4. 핵심 개념: ‘모른다’를 ‘없다’로 뭉개지 않는다

CONCEPTS.md는 이 프로젝트의 사고방식을 가장 잘 보여 주는 문서입니다. 모든 속성은 Known / Absent / Unknown 세 상태 중 하나로 다룹니다. Absent는 “물어봤더니 없다”는 답이고, Unknown은 “읽기에 실패해서 모른다”입니다. 이 둘을 bool 하나로 합치면 부정문에서 문제가 터집니다. 예를 들어 ‘비활성이 아니다’를 확인하려 할 때 읽기에 실패한 요소까지 ‘확실히 비활성 아님’으로 바뀌어 버립니다.

개념 정의 실무적 의미
Snapshot 특정 시점의 접근성 트리 관찰. 할당한 ref와 함께 저장 상태는 ~/.agent-desktop(또는 AGENT_DESKTOP_HOME)에 남음
Ref 스냅샷 안에서 행동 가능하거나 더 내려가 볼 수 있는 노드에 붙는 짧은 ID 항상 @스냅샷ID:eN 형태. bare @e8은 v0.10.0부터 거부
RefMap ref를 나중에 다시 찾는 데 필요한 정체성 증거의 저장본 값과 좌표는 정체성에 넣지 않음. 역할과 안정 필드(native id·이름·설명)만 사용
Strict Ref Resolution 행동 직전에 ref를 실제 요소로 다시 찾되, 못 찾거나 애매하면 추측하지 않고 거부 STALE_REF·AMBIGUOUS_TARGET은 오류가 아니라 ‘안전하게 멈췄다’는 신호
Partial Observation 시간 예산이 떨어지면 버리지 않고 본 데까지 반환하고, 잘린 노드를 표시 complete 플래그를 봐야 함. drill-down은 완전한 관찰에서만 허용
Web Wrapper Chromium·Electron이 만드는 이름 없는 래퍼 컨테이너는 논리 깊이에서 제외 Slack 같은 Electron 앱도 기본 깊이 예산 안에 들어옴
Secure Field 비밀번호 칸은 관찰·행동 결과 어디에도 값을 내보내지 않음 쓰기는 되지만 에코하지 않고, 확인할 수 없으면 verified: None
Interaction Lease 데스크톱 입력에 대한 머신 전체 배타권. 디스패치 동안만 잡음 여러 에이전트가 동시에 써도 입력이 섞이지 않음. 대기 중에는 잡지 않아 직렬화를 최소화
Session / Trace 세션 디렉터리에 refmap과 JSONL 트레이스 세그먼트를 프로세스별로 기록 trace show/export로 병합 타임라인. 스크린샷 아티팩트는 마스킹되지 않음

창 식별도 같은 철학을 따릅니다. 창 핸들은 재활용될 수 있으므로, 핸들에 “소유 프로세스의 생성 시각에서 뽑은 토큰”을 붙여 확인합니다. Linux에서는 X11 클라이언트 id에 boot-id로 한정한 프로세스 시작 시각을 붙입니다. 오래 걸리는 화면 캡처는 앞뒤로 정체성을 확인하는 ‘Identity Sandwich’로 감싸서, 중간에 창 순서가 바뀌면 바이트를 버립니다.

5. 출력 계약: 성공보다 ‘전달됐는가’를 말한다

모든 명령은 JSON 봉투 하나를 출력합니다. 종료 코드는 0(성공), 1(구조화된 오류, JSON은 stdout), 2(명령줄 파싱 오류, 봉투는 여전히 출력)입니다. 에이전트 입장에서 가장 쓸모 있는 필드는 disposition입니다. 실패냐 성공이냐보다 “입력이 실제로 앱에 들어갔을 수 있는가”가 재시도 판단에 더 중요하기 때문입니다.

delivery 값 의미 retry
not_delivered 아무 입력도 보내지 않았음 (인자 오류, 읽기 전용 명령의 오류, 조회 실패 등) safe
delivered_unverified 입력은 보냈지만 효과를 확인하지 못함 (예: 버튼 클릭) unsafe
delivered_verified 보냈고 다시 읽어서 효과까지 확인함 (예: set-value 후 값 재확인) unsafe
delivery_uncertain 들어갔는지 알 수 없음 unsafe
unknown 전달에 대해 아무것도 확정하지 못함 (예: 이미 들어갔을 수도 있는 TIMEOUT) 자동 재시도 금지

규칙은 간단합니다. retry가 safe일 때만 자동으로 다시 시도하고, 나머지는 get·is·새 스냅샷으로 실제 상태를 읽고 나서 결정합니다. 토글을 두 번 눌러 원상복구하거나 결제 버튼을 두 번 누르는 사고를 계약 수준에서 막는 장치입니다. Windows에서는 SendInput이 UIPI에 막혀도 성공을 반환하기 때문에, 그 반환값을 전달 증거로 쓰지 않는다는 점도 명시돼 있습니다.

오류 코드 (16종 중 주요) 언제 권장 대응
STALE_REF 저장된 정체성과 맞는 라이브 요소가 없음 새 스냅샷 후 새 ref로 재시도
AMBIGUOUS_TARGET 여러 요소가 맞음, 또는 여러 창 find --role --name --exact, --root, --window-id로 좁히기
POLICY_DENIED 시맨틱 경로가 없고 물리 입력은 정책상 금지 사용자가 허락한 경우에만 --headed. 헤드리스 실패를 덮으려고 headed로 바꾸지 말 것
ACTION_NOT_SUPPORTED 대상이 그 액션을 노출하지 않음 스냅샷의 available_actions 확인
SNAPSHOT_NOT_FOUND 스냅샷 ID가 없거나 만료 다시 스냅샷
APP_NOT_FOUND / WINDOW_NOT_FOUND 앱·창 조회 실패 (항상 not_delivered) list-apps가 출력한 이름 그대로 사용
TIMEOUT 대기 조건 만료, 또는 정체성 읽기가 기한까지 불완전 disposition 먼저 확인

6. 헤드리스가 기본이다: Interaction Policy

CLI의 모든 호출은 각자 프로세스로 실행되고, 기본 정책은 strict headless입니다. ref 액션은 접근성 API(AT-SPI Action.DoAction, EditableText.SetTextContents 등)로만 수행하고, 커서를 움직이거나 포커스를 빼앗거나 키를 합성하거나 클립보드를 쓰지 않습니다. 시맨틱 경로가 없으면 조용히 물리 입력으로 대체하지 않고 실패합니다. 사람이 같은 PC로 일하는 중에도 에이전트를 돌릴 수 있는 이유가 여기 있습니다.

정책 켜는 법 허용 범위 주의
headless (기본) — 접근성 API만. 커서·포커스·합성 입력·클립보드 없음 Windows의 type은 헤드리스 경로가 없어 실패하므로 set-value를 써야 함
focus fallback CLI 플래그 없음. 명시적 키 입력의 기본 정책, 언어 바인딩에서 선택 가능 포커스는 허용, 커서 이동은 불허 ref 액션이면 앱이 아니라 해당 창을 정확히 앞으로
headed 전역 --headed 액션이 선언한 포커스·커서 전제조건 허용, 물리 입력(CGEvent·SendInput·XTest) double-click·hover·drag 등은 Linux에서 X11 + headed에서만. Wayland는 포인터를 겨눌 수 없음

물리 입력을 쓸 때의 안전장치도 따로 있습니다. 드래그·modifier 조합·긴 텍스트 같은 다단계 입력은 중간에 실패하면 원위치 드래그와 key-up을 보내는 Release Guard로 감쌉니다. 키나 버튼을 누른 채 유지하는 key-down·mouse-down 계열 네 명령은 그 상태를 관리할 데몬이 생길 때까지 모든 OS에서 실행을 거부합니다. README가 “60개 명령 이름 중 56개가 동작한다”고 쓰는 이유입니다.

7. 플랫폼 지원 매트릭스

기능 macOS Windows Linux
접근성 트리 예 예 예 (AT-SPI)
클릭·입력·키보드 예 예 AT-SPI, X11 / GNOME 키
마우스 입력 예 예 X11, --headed
스크린샷 예 예 X11 창, Wayland는 포털 동의
클립보드 예 예 예 (Wayland는 wl-clipboard 필요)
앱·창 관리 예 예 실행·목록, X11 창 제어
알림 예 예 GNOME Shell
셸 표면 Dock, 메뉴 막대 항목 예 GNOME 트레이, Action Center
권한 손쉬운 사용 권한 필요 (permissions --guide) 같은 무결성 수준 앱은 권한 불필요 사용자 그래픽 세션 + at-spi2-core

Linux 스킬 문서는 Ubuntu 24.04 이상을 요구하고, 다른 배포판은 at-spi2-core 브리지 수정(dc0dc33)이 들어간 버전이어야 한다고 적습니다. Ubuntu 22.04의 브리지는 Action.GetActions에서 앱을 죽인다는 경고도 있습니다. 아래 박스 실측에서 이 경고를 그대로 재현했습니다.

8. 설치와 첫 실행

# 설치 (macOS / Windows / Linux 공통)
npm install -g agent-desktop
agent-desktop version

# 에이전트에게 먼저 읽힐 내장 문서 (바이너리 버전과 항상 일치)
agent-desktop status                 # 플랫폼, 권한, supported_surfaces
agent-desktop skills get desktop     # 루프·ref·검증·재시도 규칙
agent-desktop skills get platform    # 현재 OS에서 달라지는 점

# 하니스에 스킬 설치
npx skills add https://github.com/vercel-labs/agent-desktop --skill agent-desktop

# 큰 앱은 지도부터 그리고 한 영역만 파고들기
agent-desktop snapshot --skeleton --app Slack -i --compact
agent-desktop snapshot --root @s8f3k2p9:e3 -i --compact

AGENTS.md에 “agent-desktop을 쓰기 전에 skills get desktop과 skills get platform을 실행하고 따르라”는 두 줄만 넣어도 됩니다. 문서를 바이너리에 넣어 둔 덕분에 에이전트가 읽는 규칙과 실제 동작 버전이 어긋나지 않습니다. C ABI를 쓰면 매 명령마다 프로세스를 띄우는 비용 없이 같은 프로세스에서 호출할 수 있습니다. ad_init(7)처럼 ABI 메이저 버전이 맞아야 초기화되고, v1.0.2 이후 main은 ABI 7로 올라가 있습니다.

9. 박스 실측: Xvfb + xfwm4 + AT-SPI 위의 GTK3 앱

박스 실측(2026-11-03 KST, v1.0.2): Xvfb·xfwm4·AT-SPI 위 GTK3 앱에서 set-value·click·check·secure 필드·close-app 결과와 버튼 100/500/2,000개 창의 snapshot -i 지연. 왼쪽은 agent-desktop screenshot이 실제로 찍은 창이다
박스 실측(2026-11-03 KST, v1.0.2): Xvfb·xfwm4·AT-SPI 위 GTK3 앱에서 set-value·click·check·secure 필드·close-app 결과와 버튼 100/500/2,000개 창의 snapshot -i 지연. 왼쪽은 agent-desktop screenshot이 실제로 찍은 창이다

macOS나 Windows 실기기는 없어서, 리눅스 박스(x86_64, 8코어)에서 v1.0.2를 npm으로 설치하고 별도 가상 디스플레이를 만들어 시험했습니다. 처음 시도부터 문서가 경고한 함정을 순서대로 밟았기 때문에 그 과정도 함께 적습니다. 숫자는 모두 이 박스에서 실제로 나온 출력입니다.

단계 구성 / 시도 실제 결과
설치 npm install agent-desktop@1.0.2 (npm이 postinstall을 막은 상태) version → {"os":"linux","target":"x86_64","version":"1.0.2"}. 스크립트 없이도 동작
시도 1 Xvfb만 띄우고 창 관리자 없이 GTK3 앱 실행 status는 accessibility granted. 하지만 list-apps·snapshot은 ACTION_NOT_SUPPORTED / native_window_unproven. EWMH 창 목록이 없으면 창을 증명하지 못하고 거부
시도 2 xfwm4 추가, conda-forge의 옛 GTK3 스택(at-spi2-core 2.40 / at-spi2-atk 2.38) 스냅샷은 ref 4개를 정상 반환. 그러나 직후 테스트 앱이 D-Bus 시그니처 오류(a(sss))로 abort. 문서가 경고한 옛 브리지의 Action.GetActions 문제와 같은 증상
시도 3 (최종) 시스템 GTK 3.24.49 + 브리지 2.56.2 (ctypes로 만든 GTK3 앱), 버스 런처는 at-spi2-core 2.58.3 모든 명령 정상 동작. 아래 표의 결과

테스트 앱은 라벨 count: 0, 메모 입력칸, 비밀번호 칸, 체크박스 Enable sync, 버튼 Increment로 구성했습니다. snapshot --app python3.13 -i는 19ms 만에 complete: true, ref 4개를 돌려줬습니다. 비밀번호 칸은 states: ["secure"]만 있고 값은 없었습니다. 앱 이름은 list-apps가 출력한 python3.13과 정확히 같아야 했고, python3로 주면 APP_NOT_FOUND가 나왔습니다.

명령 (헤드리스 기본) 결과 봉투 핵심 앱 쪽 로그 소요
set-value @…:e1 "hello from agent-desktop" ok, EditableText.SetTextContents, delivered_verified, post_state.value 일치 ENTRY b'hello from agent-desktop' 35ms
click @…:e4 ×2 ok, Action.DoAction, delivered_unverified (retry unsafe) CLICKED 1, CLICKED 2 30–33ms
check @…:e3 ok, delivered_verified, post_state checked CHECK 1 30ms
is @…:e3 --property checked result: true — 14ms
get @…:e2 --property value (비밀번호 칸) value: null. 실제 값 hunter2는 노출되지 않음 — 10ms
click @e4 (bare ref) INVALID_ARGS, not_delivered / safe, 정규 형식 안내 — 6ms
다시 스냅샷 라벨 count: 2, 입력칸 value, 체크 상태 반영 — —
옛 스냅샷의 @…:e4로 다시 click ok, 행동함. 값만 바뀌고 역할·이름이 같으면 같은 요소로 판정 CLICKED 3 29ms
screenshot --app python3.13 430×294 PNG (위 그림 왼쪽) — 52–55ms
close-app python3.13 graceful, process_instance_exited, delivered_verified 프로세스 종료 55–57ms
박스 실측으로 본 ref 정체성 경계: bare @e4는 INVALID_ARGS, 라벨이 바뀐 버튼의 옛 ref는 STALE_REF, 값만 바뀐 요소는 그대로 행동, 스크롤 영역 click은 POLICY_DENIED, 버튼 set-value는 ACTION_NOT_SUPPORTED. v1.0.2 Linux에서 같은 이름 쌍둥이 버튼은 아직 행동했다
박스 실측으로 본 ref 정체성 경계: bare @e4는 INVALID_ARGS, 라벨이 바뀐 버튼의 옛 ref는 STALE_REF, 값만 바뀐 요소는 그대로 행동, 스크롤 영역 click은 POLICY_DENIED, 버튼 set-value는 ACTION_NOT_SUPPORTED. v1.0.2 Linux에서 같은 이름 쌍둥이 버튼은 아직 행동했다

두 번째 앱으로는 정체성 판정의 경계를 시험했습니다. 버튼 Rename me는 클릭하면 라벨이 Renamed로 바뀌고, 같은 라벨의 OK 버튼 두 개를 나란히 놓았습니다.

시나리오 v1.0.2 Linux 실제 결과 해석
Rename 버튼 첫 클릭 ok, delivered_unverified, 앱 로그 RENAMED 정상
같은 옛 ref로 두 번째 클릭 (라벨이 바뀐 상태) STALE_REF (resolve_no_candidate), not_delivered / safe, 새 스냅샷 요구 이름이 정체성의 일부라서 다른 요소로 보고 거부함
스크롤 영역 ref에 click POLICY_DENIED, supported_action 검사 실패, 물리 대체 거부 헤드리스에서는 추측해서 누르지 않음
버튼에 set-value ACTION_NOT_SUPPORTED (editable·SetValue 검사 실패) actionability 검사 결과가 항목별로 들어 있어 원인 추적이 쉬움
쌍둥이 OK 중 하나의 ref로 click ok, 행동함 (앱 로그 OK CLICKED) v1.0.2는 아직 막지 않음. main의 미출시 변경 #442(봉투 2.6)는 이 경우를 AMBIGUOUS_TARGET / identity_not_unique로 바꾼다고 CHANGELOG에 적혀 있음
find --role button --name OK --exact total_matches: 2, 경로 동일 쌍둥이 구분은 --root 등으로 범위를 좁혀야 함

마지막으로 트리 크기에 따른 스냅샷 지연을 쟀습니다. 스크롤 창 안에 버튼 N개를 넣고 snapshot -i를 세 번씩 돌렸습니다.

버튼 수 N ref 수 snapshot -i (3회) JSON 크기 find –exact 1건
100 107 50 / 44 / 42ms 13,931 bytes 40ms
500 507 161 / 182 / 137ms 67,132 bytes 130ms
2,000 2,007 1,036 / 770 / 715ms 267,642 bytes 701ms

지연은 노드 수에 거의 비례했고, find도 같은 전체 창 읽기를 거치므로 스냅샷과 비슷했습니다. 이 단순한 평면 트리에서는 --compact가 크기를 거의 줄이지 못했습니다. 2,000개 요소 창의 JSON 26만 바이트를 매 턴 컨텍스트에 넣는 것은 부담이므로, 큰 앱에서는 README 권장대로 --skeleton으로 지도를 그리고 --root로 한 영역만 읽는 것이 맞습니다. 이 수치는 이 박스(Xvfb, 합성 GTK3 앱)에서만 유효하며, 실제 GNOME 앱이나 Electron 앱의 수치가 아닙니다. 프로젝트도 ‘Dev Box’ 개념으로 “한 기계에서만 잰 숫자는 그 기계에 대한 사실”이라고 선을 긋습니다.

10. 비슷한 도구와의 비교

도구 관찰 방식 대상 에이전트 친화성 agent-desktop과의 차이
vercel-labs/agent-desktop OS 접근성 트리 → JSON + ref macOS·Windows·Linux 네이티브 앱 JSON 봉투, disposition, 내장 스킬, C ABI —
스크린샷 기반 CUA (모델 제공사 computer use 도구 등) 스크린샷 + 좌표 행동 화면에 보이는 모든 것 모델이 직접 픽셀을 해석 범용성은 넓지만 정밀도·비용·전달 확인 면에서 불리. 접근성이 없는 앱은 이쪽이 유일한 수단
trycua/cua VM·샌드박스 + 컴퓨터 유즈 에이전트 인프라 격리된 가상 데스크톱 샌드박스·벤치·드라이버 묶음 격리 실행 환경이 강점. agent-desktop은 ‘지금 이 데스크톱’에서 트리를 읽는 계층이라 함께 쓸 수 있음
vercel-labs/agent-browser 브라우저 자동화 웹 페이지 같은 팀, 같은 ref 철학 웹은 agent-browser, 네이티브는 agent-desktop으로 역할이 나뉨
pywinauto / Appium(Windows·Mac2 드라이버) UIA·AX 기반 주로 테스트 자동화 사람이 짜는 스크립트·셀렉터 중심 agent-desktop은 스냅샷 ID로 묶인 ref, 전달·재시도 의미론, 세 OS 단일 계약을 에이전트용으로 정의
dogtail 등 AT-SPI 테스트 도구 AT-SPI Linux GUI 테스트 Python API Linux 전용. 창 정체성 증명·Secure Field·Lease 같은 안전 계약은 없음

11. 트레이드오프와 주의할 점

  • 접근성을 내놓지 않는 앱은 보이지 않습니다. 캔버스로 그리는 앱이나 게임, 접근성 구현이 부실한 사내 앱에서는 트리가 비어 있거나 의미 없는 그룹만 나옵니다. 이런 앱은 결국 스크린샷 방식과 섞어 써야 합니다. 저장소의 JEV 헬퍼가 그 다리 역할을 합니다.
  • Linux는 ‘세션 안에서’ 실행해야 합니다. 박스 실측처럼 창 관리자(EWMH)가 없으면 native_window_unproven으로 거부하고, 옛 at-spi2 브리지에서는 대상 앱이 죽을 수 있습니다. SSH 셸이나 버스가 없는 컨테이너에서는 앱이 하나도 보이지 않습니다.
  • 1.0 직후라 계약이 아직 움직입니다. main의 Unreleased 항목은 봉투 2.6, C ABI 7, 쌍둥이 ref의 AMBIGUOUS_TARGET 전환, 이전 스냅샷 ref 무효화를 예고합니다. 오류 코드로 분기하는 하니스라면 버전을 고정하고 마이그레이션 문서(docs/json-output.md)를 따라가야 합니다.
  • 클릭은 대부분 delivered_unverified입니다. 버튼을 눌렀다는 사실만 보장하고 효과는 보장하지 않습니다. 반드시 재관찰로 결과를 확인하는 루프를 짜야 하며, 무작정 재시도하면 중복 실행이 납니다.
  • 개인정보가 출력에 섞입니다. README가 첫 화면에서 경고하듯 스냅샷, 클립보드, 스크린샷, 트레이스, 디버그 HTML에는 사적인 데스크톱 내용이 들어갈 수 있습니다. 세션 리플레이 아티팩트는 마스킹되지 않습니다. 공개 로그나 업로드, LLM 제공사로 가는 컨텍스트에 무엇이 실리는지 따로 통제해야 합니다.
  • 대형 트리는 비쌉니다. 박스에서도 요소 2,000개 창은 약 0.7–1.0초, 26만 바이트였습니다. 스켈레톤과 드릴다운을 기본 습관으로 들이는 것이 좋습니다.
  • 헤드리스 실패를 headed로 덮지 마세요. 스킬 문서는 사용자가 물리 입력을 명시적으로 허락했을 때만 --headed를 쓰라고 합니다. 에이전트가 스스로 headed로 바꾸면 사람의 커서와 포커스를 가져가게 됩니다.
상황 추천 이유
사내 업무 앱·설정 창·파일 관리자 자동화 agent-desktop 우선 정확한 ref, 헤드리스, 전달 의미론
Electron 앱(Slack 등) 조작 agent-desktop + skeleton/root Web Wrapper 처리로 깊이 예산 안에 들어오지만 트리가 큼
웹 페이지 작업 agent-browser 등 브라우저 도구 DOM 수준 제어가 더 정확
게임·캔버스·원격 데스크톱 화면 스크린샷 기반 CUA 접근성 트리가 없음
격리·재현이 필요한 평가 VM/샌드박스(cua 등) 안에서 agent-desktop 실행 실제 사용자 데스크톱과 분리
Ubuntu 22.04 등 옛 Linux 도입 보류 또는 at-spi2 업그레이드 브리지 버그로 대상 앱이 abort할 수 있음 (박스에서 재현)

12. 라이선스 메모

저장소, Cargo 워크스페이스, npm 패키지 모두 Apache-2.0입니다. 상업적 사용·수정·재배포가 가능하고, 특허 라이선스 조항과 NOTICE·변경 고지 의무가 따라옵니다. README는 네이티브 CLI와 JEV 헬퍼가 별개라고 분명히 적습니다. 선택 기능인 JEV 헬퍼(skills/jev-desktop, 저장소 클론의 scripts/jev/)는 화면 설명을 설정한 모델 제공사로 보내며 TYPESAFE_API_KEY가 필요합니다. 데이터 처리 정책을 검토할 때 이 둘을 구분해야 합니다.

공식 저장소 배너: Observe·Click·Type·Drag·Scroll을 macOS·Windows·Linux에서 하나의 CLI로 제공한다는 agent-desktop의 콘셉트 (vercel-labs/agent-desktop assets/banner.png)
공식 저장소 배너: Observe·Click·Type·Drag·Scroll을 macOS·Windows·Linux에서 하나의 CLI로 제공한다는 agent-desktop의 콘셉트 (vercel-labs/agent-desktop assets/banner.png)

13. 정리

agent-desktop의 핵심은 “AI가 데스크톱을 본다”는 기능보다 무엇을 확실히 알고 무엇을 모르는지 정직하게 말하는 계약에 있습니다. ref는 스냅샷에 묶이고, 애매하면 행동하지 않으며, 행동했다면 들어갔는지와 확인됐는지를 나눠 보고합니다. 박스 실측에서도 set-value와 check는 검증까지, 클릭은 전달까지만 보고했고, 라벨이 바뀐 버튼은 STALE_REF로 멈췄으며, 비밀번호 값은 끝까지 나오지 않았습니다. 반대로 v1.0.2에서는 쌍둥이 버튼을 아직 구분하지 못했고, Linux는 창 관리자와 최신 at-spi2 없이는 아예 동작하지 않았습니다. 지금 도입한다면 버전을 고정하고, 재관찰 루프를 기본으로 하고, 출력에 담기는 데스크톱 내용을 따로 통제하는 것이 출발점입니다.