AI 시대의 미니멀 Windows 셸이 두 가지를 새로 배웠다. 데스크톱을 직접 조작할 수 있고, 음성으로 그 자신을 조작할 수 있다. 미션 M0014가 OS-Control 표면을 도입해 모든 읽기 전용 기능을 CLI 동사(AgentZeroLite.exe -cli os …)와 LLM 도구(os_*) 양쪽에 대칭적으로 노출했다. 미션 M0008은 이미 완전 온디바이스 음성 파이프라인(Whisper + Vulkan, Akka Streams, 명령 인터셉터)을 배달했다. 두 가지를 합치면 — 의도적으로 설계된 승인 게이트 안에서 — 온디바이스 어시스턴트가 핸즈프리로 컴퓨터 자신을 조작할 수 있게 된다.
무엇이 추가되었나
두 가지 능력, 하나의 활주로.
능력 | 미션 | 무엇을 주는가 |
OS-Control 표면 | M0014 | 윈도우 나열, 스크린샷, UIA 트리 탐색, 클릭, 타이핑, 활성화 — CLI 또는 LLM 양쪽에서. |
음성 파이프라인 | M0008 (+ 후속) | 마이크 → Whisper STT (CPU+Vulkan) → 명령 분류기 → 봇 디스패치. 정지어, "터미널 요약해", 일반 패스스루까지. |
모든 동사·도구·음성 명령은 JSONL 한 줄짜리 감사 로그를 남긴다. 아무것도 머신을 떠나지 않는다. 읽기 전용은 무조건 허용, 입력 시뮬레이션은 게이트 통과 필수. 핵심은 대칭성이다 — 요청이 어떤 채널로 도착하든(키보드, 음성, 원격 CLI, LLM 도구 호출) 같은 파사드가 같은 규칙을 강제한다.
한눈에 보는 아키텍처
음성 신호와 키보드 신호는 동일한 디스패치 표면으로 합류한다. 거기서 "AI 모드" 토글이 결정한다 — 텍스트가 포커스된 터미널로 바로 흘러갈지, 아니면 온디바이스 LLM 에이전트 루프를 거칠지. 후자는 다시 OS-Control 파사드를 도구로 호출할 수 있다.
flowchart LR subgraph IN[입력] MIC[🎙 마이크] KBD[⌨ 키보드] RCLI[📡 원격 CLI<br/>WM_COPYDATA] end subgraph VP[음성 파이프라인 · M0008] VAD[VAD + Akka Streams<br/>VoiceSegmenterFlow] STT[Whisper.net<br/>CPU + Vulkan] VCI[VoiceCommand<br/>Interceptor] end subgraph BOT[AgentBot · UI 게이트웨이] MODE{AI Mode?} LOOP[AgentLoopActor<br/>IAgentLoop] end subgraph TOOLS[툴벨트 · M0013/M0014] TR[터미널 릴레이<br/>도구] OS[OS-Control 파사드<br/>OsControlService] end subgraph WIN[Windows 표면] UIA[UI Automation 트리] GDI[GDI BitBlt → PNG] INP[SendInput<br/>게이트] end MIC --> VAD --> STT --> VCI VCI -- "stop / summarize" --> BOT VCI -- 패스스루 --> BOT KBD --> BOT RCLI --> BOT MODE -- off --> TR MODE -- on --> LOOP LOOP --> TR LOOP --> OS OS --> UIA OS --> GDI OS -.게이트.-> INP OS --> AUDIT[(tmp/os-cli/audit/<br/>YYYY-MM-DD.jsonl)]
이 그림이 명시하는 것 몇 가지:
- 하나의 파사드, 여러 호출자.
OsControlService는 CLI 디스패처, in-process LLM 툴벨트, E2E 스모크 프로브 모두에게 도달한다. 셋 다 감사 로그에caller필드를 박아두므로, 포렌식 시점에서 "어느 채널이 그 클릭을 일으켰는가"를 추적할 수 있다.
- LLM은 픽셀을 보지 않는다. 스크린샷은 경로만 반환한다. 에이전트 루프 컨텍스트가 이미지로 부풀지 않고, 프롬프트 인젝션 공격면이 작게 유지된다.
- 음성은 또 하나의 입력일 뿐. 키보드와 같은 봇 디스패치로 흘러간다. 트랜스크립트가 도착한 뒤 AI 모드 + 도구 호출이 무엇을 할지 결정한다 —
os_*도구가 발사될지 여부 포함.
대칭 표면
모든 읽기 전용 OS-Control 기능은 두 번 노출된다 — CLI 동사로 한 번, LLM 도구로 한 번:
CLI 동사 | LLM 도구 | 부작용 종류 | 게이트 |
os list-windows | os_list_windows | 열거 | 없음 |
os get-window-info <hwnd> | (CLI 전용) | 읽기 | 없음 |
os screenshot | os_screenshot | GDI BitBlt → PNG 파일 | 없음 |
os element-tree <hwnd> | os_element_tree | UIA 탐색 | 없음 |
os text-capture <hwnd> | (CLI 전용, Phase A) | UIA Name 집계 | 없음 |
os dpi | (CLI 전용) | 메트릭 | 없음 |
os activate <hwnd> | os_activate | Z-order 변경 | 없음 |
os mouse-click <x> <y> | os_mouse_click | 입력 시뮬레이션 | 게이트 |
os mouse-move / mouse-wheel | (CLI 전용, Phase B) | 입력 시뮬레이션 | 게이트 |
os keypress <spec> | os_key_press | 입력 시뮬레이션 | 게이트 |
os audit [--last N] | (CLI 전용) | 감사 로그 조회 | n/a |
읽기 전용 동사는 무조건 허용된다. 입력 시뮬레이션은 둘 중 하나가 필요하다:
- CLI 동사에
--allow-input플래그(호출별), 또는
- 프로세스 환경 변수
AGENTZERO_OS_INPUT_ALLOWED=1(1/true/yes모두 인정).
호출별 사용자 확인 프롬프트는 없다. 게이트는 의도적으로 이진이다 — 자동화에 마찰을 끼우면 의미가 사라진다. 감사 로그가 사후 포렌식 가시성을 책임진다.
게이트가 닫혀 있는데 LLM이 게이트 도구를 호출하면 거부 봉투를 받는다:
{ "ok": false, "error": "OS input simulation is gated. Set environment variable AGENTZERO_OS_INPUT_ALLOWED=1 (LLM/GUI) or pass --allow-input on the CLI verb to enable.", "verb": "<tool name>" }
LLM 시스템 프롬프트는 게이트 거부 후 재시도를 명시적으로 금지한다. 모델은 루프를 돌지 말고
done으로 실패를 보고해야 한다.음성 트랜스크립트가 분류되는 방식
음성 트랜스크립트는 LLM으로 곧장 가지 않는다. 먼저
VoiceCommandInterceptor라는 의도적으로 작은 분류기를 거쳐, 자주 쓰는 인앱 명령 몇 가지를 결정론적으로 처리한다.flowchart TD T[Whisper 트랜스크립트] --> N{공백/Null?} N -->|예| P[PassThrough no-op] N -->|아니오| TR2[Trim + 후행 구두점 제거] TR2 --> S{StopPhrases 정확<br/>일치?} S -->|예| STOP[StopSpeaking<br/>TTS 취소, LLM 디스패치 생략] S -->|아니오| K{터미널 + 요약 포함<br/>or terminal + summar?} K -->|예| SUM[SummarizeTerminal<br/>스냅샷 후 LLM 요청] K -->|아니오| PT[PassThrough<br/>봇으로 전달]
규칙이 의도적으로 단순한 이유 — 짧은 트랜스크립트 단위 매칭만 하고 NLP를 끼우지 않는다 — 는 Whisper 출력 자체가 실행마다 변동(구두점, 대소문자, 후행 마침표)이 있기 때문이다. "음성을 리모컨처럼 쓴다"는 용도에는 정교함보다 예측 가능성이 이긴다. 정지어에는
그만, 그만해, stop, shut up이 들어간다. 터미널 요약은 두 키워드가 어떤 순서로든 등장하면 매칭하며, Whisper가 한국어 기술용어를 영어로 받아쓰는 경우(lang=ko여도 발생)를 위해 영어 폴백도 있다.산출물이 떨어지는 곳
OS-Control이 하는 모든 일은
tmp/os-cli/ 아래로 떨어진다(gitignored):tmp/os-cli/ ├── audit/2026-05-08.jsonl 호출별 한 줄 (cli, llm, e2e) ├── screenshots/2026-05-08/ PNG (HH-mm-ss-fff prefix) └── e2e/2026-05-08.log E2E 스모크 요약
감사 JSONL 스키마:
{ "ts": "2026-05-08T11:42:09.123+09:00", "caller": "cli | llm | e2e", "verb": "list_windows | screenshot | mouse_click | …", "args": { }, "ok": true, "error": null }
os audit --last 50이 오늘자 꼬리를 출력해준다. 감사 로그는 best-effort: 레코더 내부 실패는 예외를 삼켜서 일시적 디스크 오류가 실제 동작을 깨지 않게 한다. 사후 포렌식 기록이지 트랜잭션 게이트가 아니다.활용 사례 — 이 조합이 무엇을 푸는가
1. 다음 탭에서 키보드 치는 동안 음성으로 화면 리뷰
탭 A에서 코드 작성 중, 탭 B에서 빌드 로그가 흐르고 있다. 마이크에 대고 말한다:
"터미널 작업 요약해 줘."
VoiceCommandInterceptor가 이를 SummarizeTerminal로 분류한다. 봇은 활성 터미널 출력을 스냅샷해 온디바이스 LLM에 "요약" 프롬프트와 함께 디스패치한다. AI 모드 + os_screenshot을 곁들이면 사람 리뷰어용 그레이스케일 PNG를 tmp/os-cli/screenshots/today/에 선택적으로 떨어뜨린다. 손은 다른 키보드를 떠나지 않는다.2. 핸즈프리 TTS 중단
음성 응답(TTS)은 이제 AI 모드의 표준 출력 중 하나이고(M0011 후속이 이번에 같이 들어왔다), 길어지기 쉽다. 이때:
"그만."
…는 StopSpeaking 분기를 친다. 진행 중인 TTS 큐를 비우고 LLM 디스패치 자체를 건너뛴다. "정확한 마법 문구를 외워야 한다"는 어색한 순간이 없다 — 분류기가 후행 구두점과 대소문자를 정리해서
그만., 그만해, stop!이 모두 매칭된다. (참고 커밋: 00610f6 fix(voice): gate AI-bubble TTS on mic-on; drain in-flight TTS at mic-off.)3. CLI를 통한 원격 데스크톱 점검
CLI는 같은 머신의 어떤 셸에서도 호출할 수 있고,
-cli os 동사들은 in-process 실행이라 WM_COPYDATA 홉도 GUI 가동도 필요 없다. 원격 점검이 자명해진다:# 같은 박스의 Claude 탭(또는 임의의 피어 터미널)에서: AgentZeroLite.exe -cli os list-windows --filter "Visual Studio" AgentZeroLite.exe -cli os screenshot --hwnd 0x000A0234 AgentZeroLite.exe -cli os element-tree 0x000A0234 --depth 5
ConvertFrom-Json으로 받으면 운영자 데스크톱의 현재 상태가 자기 기술 가능한 JSON 보고서로 나온다. CI 스크립트도 같은 표면을 받는다. 읽기 전용은 옵트인 불필요, 입력 시뮬레이션은 운영자가 실행별로 명시 승인.4. LLM 주도 UI 검사 (대신 타이핑은 안 함)
AI 모드에서 이렇게 묻는다:
"내 노트패드 열려 있어? 열려 있으면 내용 캡처해."
문법 제약형 에이전트 루프(
LocalAgentLoop for Gemma 4 로컬 / ExternalAgentLoop for OpenAI 호환)가 다음을 발사한다:os_list_windows({ "title_filter": "Notepad" })→ hwnd 목록.
os_screenshot({ "hwnd": <hwnd>, "grayscale": true })→ 경로 반환.
os_text-capture(CLI, 향후 LLM 노출) — 텍스트 추출.
done과 함께 운영자 친화 요약.
LLM은 픽셀을 받지 않는다 — 파일 경로 + 텍스트 추출만. 운영자가 노트패드에 다시 밀어넣고 싶다면(텍스트 입력, Ctrl+S 전송) 그건 게이트를 건넌다.
AGENTZERO_OS_INPUT_ALLOWED=1이 없으면 모델은 거부 봉투를 받고 재시도 없이 done으로 에러를 보고한다 — 시스템 프롬프트가 이를 명시적으로 못 박는다.5. 운영자 승인 세션의 자기조종 마이크로 플로우
집중 작업 블록 동안 운영자가 게이트를 켠다:
$env:AGENTZERO_OS_INPUT_ALLOWED = "1"
…그러면 LLM은
os_activate → os_key_press → os_screenshot 체인을 엮어 진짜 자기조종 마이크로 태스크("노트패드 활성화 → 오늘 날짜 헤더 입력 → 결과 스크린샷")를 만들 수 있다. 작업 블록이 끝나면 Remove-Item Env:AGENTZERO_OS_INPUT_ALLOWED(또는 셸 종료)로 게이트를 다시 잠근다. 누가 발사했든 감사 로그는 모든 호출을 잡아둔다.의도적으로 배제한 안티패턴
- 스크린샷 바이트를 LLM 컨텍스트에 인라인 금지. 툴벨트는 경로 문자열만 반환한다. 바이트 인라인은 토큰을 낭비하고 프롬프트 인젝션 공격면을 키운다.
os_launchLLM 도구 없음.os_activate는 기존 hwnd를 요구한다. CLI에는open-win이 있지만 이를 LLM에 노출하면 재귀 위험(LLM이 자기 자신의 인스턴스를 운전)이 생긴다. 향후 미션이 필요하다면 자신의 승인 플래그 뒤에 별도로 들어간다.
- 스크린샷 경로 재사용 금지. 파일명에 밀리초 정밀도 (
HH-mm-ss-fff) prefix를 박아 거의 동시에 찍은 두 캡처가 충돌하지 않게 한다.
- "사소한" 호출도 감사 누락 금지.
os_list_windows도 로그한다 — 열거 자체가 흔적을 남길 가치가 있는 핑거프린팅 신호이기 때문.
Origin (AgentWin)와의 패리티
AgentWin Origin 프로젝트는 더 풍부한 15-동사 / 9-LLM-도구 표면을 배달했다. M0014는 Lite의 정체성에 맞는 80% 슬라이스만 가져왔다:
- Imported: list-windows, get-window-info, screenshot, element-tree, text-capture, dpi, activate, mouse-click, mouse-move, mouse-wheel, keypress.
- Skipped: scroll-capture (element-tree에 흡수),
copy <text>(기존copy동사와 충돌), virtual-desktop service (전용 향후 미션).
리프트가 의도적이었던 이유: Lite는 이미 필요한 Win32 P/Invoke의 90%(
Project/AgentZeroWpf/NativeMethods.cs)와 ElementTreeScanner를 가지고 있었다. M0014는 그 위에 래퍼, 감사, 게이팅, CLI 디스패치, LLM 브리지, E2E 스모크 프로브를 얹었다.시도해보기
새 빌드가 어떤 기능도 구동하지 않고 도달 가능한지 스모크 테스트:
pwsh Docs/scripts/launch-self-smoke.ps1 -Configuration Debug
스텝: list-windows → get-window-info → screenshot → element-tree → dpi. Exit 0이면 모든 프로브 통과, PNG와 감사 로그는
tmp/os-cli/ 아래 보존된다.전체 인스톨러 + 포터블 ZIP은 여기에 떨어진다:
마무리
이번 릴리즈의 테마는 "음성이 켜졌다"도 "OS 자동화가 켜졌다"도 아니다. 같은 파사드가 모든 입력 벡터를 통제한다는 점, 이게 자기조종을 배달 가능하게 만든다. 음성으로 터미널에 타이핑하는 건 유용하다. 음성으로 로컬 LLM에게 데스크톱을 검사하고 그레이스케일 스냅샷을 떨어뜨리라고 시키는 건 — 모든 호출이 감사되고 입력이 게이트되는 조건에서 — 다른 종류의 유용함이다. 어시스턴트에게 잠시 키보드를 넘기고도, 그가 무엇을 건드렸는지 정확히 알 수 있는 종류.
TECH LINKS
- 𝕏 @webnori