메뉴
HN
Hacker News • 18일 전

모바일 에이전트를 구동하는 가상머신의 내부 구조

IMP
7/10
핵심 요약

Claude Code와 Instinct 등 에이전트가 폰에서 동작하도록 클라우드 VM에서 실행되는데, 두 서비스 모두 Firecracker microVM을 기반으로 한다는 것이 분석으로 밝혀졌다. Claude Code는 Rust로 작성된 커스텀 PID 1과 읽기 전용 하네스 디스크, 호스트 주도 라이프사이클로 테넌트 격리에 집중한 반면, Instinct는 자체 VM 플릿 없이 E2B 샌드박스 서비스를 임대해 사용한다. 에이전트 인프라의 실제 아키텍처를 이해하는 데 유용한 기술 분석 글이다.

번역된 본문

멋진 제품 진화 중 하나는 에이전트(Claude Code, Instinct, Poke 등)가 로컬 컴퓨터를 벗어나 폰에서도 사용할 수 있게 되었다는 점입니다. 결국 이는 고객에게 좋은 일입니다. 에이전트 회사들이 에이전트를 실행할 VM을 제공해주기 때문입니다! 다음은 ws-term에서 둘러본 내용을 바탕으로 주요 플랫폼이 어떻게 동작하는지에 대한 노트입니다.

폰에서의 Claude Code

Claude Code의 박스는 자체 Firecracker microVM입니다. 자체 커널을 가진 KVM 게스트로, Rust로 작성된 init로 부팅됩니다:

$ cat /proc/cmdline ... rdinit=/process_api ... --listen-vsock-port 2024 $ uname -r 6.18.5-fc-v20 # -fc- = Firecracker; 커스텀 빌드된 게스트 커널 $ ps -o comm -p 1 → process_api # PID 1은 systemd가 아니라 Rust/Tokio 바이너리

process_api가 PID 1이며, VM 내부에 살아있는 호스트의 제어 에이전트입니다. 디스크를 마운트한 뒤 vsock 포트 2024에서 대기하여 호스트가 외부에서 세션을 구동할 수 있게 합니다. 이것이 이 플랫폼의 핵심 특징입니다. 운영자가 당신의 테넌트 공간 안에 살아있으며, 이를 격리하기 위한 많은 엔지니어링이 투입됩니다 (PID 1은 non-dumpable, CAP_SYS_PTRACE가 있어도 /proc/1/mem 접근 거부, 셸에는 CAP_SYS_RESOURCE가 없음).

디스크는 당신 것(쓰기 가능, 영속적)과 그들 것(읽기 전용, 공유)으로 깔끔하게 분리됩니다:

$ lsblk -o NAME,SIZE,RO,MOUNTPOINT vda 256G 0 / # 당신 것: 쓰기 가능, 회수 후에도 유지 vdc 341M 1 /opt/claude-code # 그들 것: 324MB짜리 claude 하네스 (Bun) vdd 45.6M 1 /opt/env-runner # 그들 것: 태스크 런처 vde/vdf ... 1 /mnt/skills/... # 그들 것: 스킬들

하네스는 툴 콜을 실행하는 것으로, 읽기 전용 디스크에 있는 324MB 컴파일된 Bun 바이너리입니다. 모델은 다른 곳에서 실행되며, 추론 요청은 WebSocket이 아닌 HTTPS/2 위의 Server-Sent Events로 /v1/messages로 나가고, 443 포트만 허용하며 MITM되는 이그레스 게이트웨이(CN = Egress Gateway ... (production))를 거치며, api.anthropic.com은 /etc/hosts에 고정되어 있습니다. 인바운드는 전혀 없습니다 (192.0.2.2, RFC-5737 테스트 주소).

인증은 호스트가 발급한 OAuth 토큰으로, 디스크에 root만 읽을 수 있게 캐시되고 부팅마다 교체됩니다. 라이프사이클은 호스트가 주도하며 내부에서 측정한 결과: 초기화 약 430ms, 하네스 프로세스까지 약 6.4초. 시작은 인바운드 메시지로 트리거되고(호스트가 vsock으로 VM을 깨워 --session-mode resume 실행), 종료는 호스트가 결정하는 유휴 회수입니다. 회수되면 프로세스는 죽지만 vda는 온전히 분리되었다가 다음 콜드 부트에 다시 붙습니다. 그래서 컴퓨팅이 파괴되었는데도 대화가 연속적으로 느껴지는 것입니다.

(다이어그램: 사용자 키 입력 → session-ingress → Firecracker microVM 내부의 process_api(pid 1, Rust) → claude 하네스 → vda(당신 것, 영속) 및 읽기 전용 디스크들; 호스트 제어 평면이 vsock 포트 2024로 process_api에 연결; 하네스는 SSE로 이그레스 게이트웨이를 통해 api.anthropic.com으로 추론 요청)

Instinct

Instinct는 최근 출시된 새 스타트업으로, 메모리 측면에서 아주 훌륭한 작업을 하여 챗봇이 아닌 진짜 어시스턴트 같은 느낌을 줍니다.

$ hostname → e2b.local $ cat /.e2b → n038afjvewg7jnc9pwdz

e2b.local이라는 것은 Instinct가 자체 VM 플릿을 운영하지 않는다는 뜻입니다. E2B 샌드박스("sandbox-as-a-service")를 임대합니다. 에이전트에게 컴퓨터를 쥐어주기 위한 일회성 Ubuntu 박스입니다:

Ubuntu 22.04.5, 2 vCPU, 1.9GB RAM, 29GB 디스크, 약 30분 유지, sandbox 사용자(uid 1001)

후드 안을 들여다보면...

$ systemd-detect-virt → kvm $ cat /proc/cmdline → pci=off virtio_mmio.device=4K@... i8042.noaux i8042.nokbd reboot=k panic=1 clocksource=kvm-clock root=/dev/vda ip=169.254.0.21::...:eth0:off:tap0 $ cat /sys/class/dmi/id/product_name → (빈 값) # SMBIOS가 아예 없음 $ ps -o comm -p 1 → systemd # init=/sbin/init, 커스텀 PID 1이 아님

여기도 Firecracker입니다! pci=off + virtio-over-MMIO + 빈 DMI + tap0 네트워킹은 Firecracker의 시그니처입니다: PCI 버스 없음, SMBIOS 없음, 최소한의 디바이스. 따라서 두 앱 모두 같은 microVM 위에 있으며, 차이는 누가 플릿을 운영하느냐입니다...

원문 보기
원문 보기 (영어)
One awesome product evolution is that agents (Claude Code, Instinct, Poke, etc) are moving off our local computers so that we can use them on our phones. Ultimately this is great for the customer because that means the agent companies provide us with VMs for them to run on! Here's some notes on how the major platforms work based on looking around on ws-term . Claude Code on Your Phone Claude Code's box is its own Firecracker microVM , a KVM guest with its own kernel, booted straight into an init written in Rust: $ cat /proc/cmdline ... rdinit=/process_api ... --listen-vsock-port 2024 $ uname -r 6.18.5-fc-v20 # -fc- = Firecracker; a custom-built guest kernel $ ps -o comm -p 1 process_api # PID 1 is not systemd; it's a Rust/Tokio binary process_api is PID 1 and the host's control agent living inside your VM: it mounts the disks, then listens on vsock port 2024 so the host can drive the session from outside. That's the platform's defining trait: the operator lives inside your tenant space , and a lot of engineering goes into sealing it off (PID 1 is non-dumpable, /proc/1/mem is denied even with CAP_SYS_PTRACE , your shell is missing CAP_SYS_RESOURCE ). The disks split cleanly into yours (writable, persistent) and theirs (read-only, shared): $ lsblk -o NAME,SIZE,RO,MOUNTPOINT vda 256G 0 / # yours: writable, survives reclaim vdc 341M 1 /opt/claude-code # theirs: the 324 MB `claude` harness (Bun) vdd 45.6M 1 /opt/env-runner # theirs: the task launcher vde/vdf ... 1 /mnt/skills/... # theirs: skills The harness is the thing running your tool calls and is a 324 MB compiled Bun binary on a read-only disk. The model runs elsewhere; inference goes out as Server-Sent Events over HTTPS/2 (not a WebSocket) to /v1/messages , through an egress gateway that is 443-only and MITM'd ( CN = Egress Gateway ... (production) ), with api.anthropic.com pinned in /etc/hosts . There is no inbound at all ( 192.0.2.2 , an RFC-5737 test address). Auth is a host-minted OAuth token , cached root-only on disk and rotated per boot. Lifecycle is host-driven and measured from the inside: ~430 ms to init, ~6.4 s to the harness process. Spin-up is triggered by an inbound message (the host wakes the VM over vsock and runs --session-mode resume ); spin-down is idle reclaim decided by the host. When it's reclaimed, the processes die but vda detaches intact and reattaches on the next cold boot, which is why the conversation feels continuous even though the compute was destroyed. flowchart TB user(["your keystrokes"]) -->|http post| ingress["session-ingress"] ingress --> pa hostctl(["host control plane"]) -->|vsock port 2024| pa subgraph vm["Firecracker microVM"] pa["process_api, pid 1, Rust"] --> harness["claude, 324 MB Bun harness"] vda[("vda (rw), yours, persists")] --- harness ro[("vdc/vdd/vde/vdf (ro), theirs")] --- harness end harness -->|inference over SSE| gw["egress gateway, 443, mitm, api.anthropic.com"] Instinct Instinct is a new startup which launched recently and it does some very nice things on the memory side which gives that feel of it being a real assistant rather than a chatbot. $ hostname e2b.local $ cat /.e2b n038afjvewg7jnc9pwdz e2b.local means Instinct doesn't operate its own VM fleet; it rents E2B sandboxes ("sandbox-as-a-service"), a throwaway Ubuntu box you hand an agent so it has a computer: Ubuntu 22.04.5, 2 vCPU, 1.9 GB RAM, 29 GB disk, up ~30 min, user sandbox (uid 1001) And let's look under the hood... $ systemd-detect-virt → kvm $ cat /proc/cmdline pci=off virtio_mmio.device=4K@... i8042.noaux i8042.nokbd reboot=k panic=1 clocksource=kvm-clock root=/dev/vda ip=169.254.0.21::...:eth0:off:tap0 $ cat /sys/class/dmi/id/product_name → (empty) # no SMBIOS at all $ ps -o comm -p 1 → systemd # init=/sbin/init, not a custom PID 1 Firecracker again! pci=off + virtio-over-MMIO + empty DMI + tap0 networking is the Firecracker signature: no PCI bus, no SMBIOS, minimal devices. So both apps sit on the same microVM; the difference is who runs the fleet and what boots inside it. Where Claude Code boots a stripped custom init ( process_api as PID 1), E2B boots a full Ubuntu with systemd and a whole XFCE desktop: $ systemd-analyze Startup finished in 265ms (kernel) + 992ms (userspace) = 1.258s graphical.target reached after 977ms ~1.26 s to cold-boot all the way to a graphical desktop. The operator-in-guest exists here too, but it's just E2B's envd running as an ordinary systemd service, not a sealed PID 1. E2B sandboxes are configurable too (you pick the vCPU, RAM, disk, and idle timeout, and whether the box can be paused and resumed from a memory snapshot instead of cold-booted); Instinct runs a modest 2 vCPU / 1.9 GB desktop template. So if the box is disposable, where does the agent's memory live? In a directory called /memory , and this is the platform's defining idea: $ cat /memory/README.md Persistent memory for [[rohan-adwankar]] $ ls /memory entities/ comms/ timeline/ workstreams/ knowledge/ $ git -C /memory log --format='%an <%ae>' -1 Instinct Agent <agent@instinct.com> The agent's memory is a git repo of Markdown files with [[wiki-links]] , navigated by grep . The timeline/ coarsens over time (raw → hourly → daily → weekly, like human memory), and the agent is literally the git author : it doesn't call a memory API, it writes Markdown and commits it as itself. The durable layer is that repo, pushed to S3 , keyed by a per-user id, and stored not as files but as a single git bundle , which is a great little gotcha: $ git -C /memory remote -v origin s3://instinct-prod-agent-memory/filesystem-memory/user-01M1VW7... $ aws s3 ls s3://.../user-01M1VW7.../ --recursive HEAD refs/heads/main/<sha>.bundle # the ENTIRE vault, packed; `ls` after sync looks empty Auth is short-lived STS credentials , not long-lived keys, so a leaked sandbox self-heals when the token lapses: $ cat /etc/instinct-aws-creds export AWS_ACCESS_KEY_ID='ASIA…' # ASIA prefix + session token = temporary STS ... # (values redacted, live secrets) # role: instinct-sandbox-observations-role flowchart TB subgraph box["E2B sandbox (rented, disposable)"] agent["Instinct Agent (agent@instinct.com)"] -->|writes and commits| mem["/memory, a Markdown vault git repo"] creds["/etc/instinct-aws-creds, short-lived STS"] end mem -->|git push| store creds -->|authorizes git push| store subgraph store["S3 (durable, per-user)"] vault[("instinct-prod-agent-memory, the vault")] obs[("instinct-prod-observations, raw firehose")] end Durable thing = a git repo in S3. The machine is throwaway. Using a git repo for this is quite nice; the default structure seems to be like this: ~/instinct-vault/.. │󰫎 24 󰲡 Vault   .git │ 23   comms │ 22 Persistent memory for 󱗖 rohan-adwankar. Markdown + wiki-links, navigated by grep. Start here, then jump   chat │ 21  rohan-adwankar--inst│ 20 Who Rohan is, what he is working on, and what is connected live in entities/, workstreams/, and knowled   entities │ 19   people │󰫎 18 󰲣 Layout  rohan-adwankar.md │ 17   projects │ 15 README.md  ws-term.md │ 14 timeline/ chronological record, coarsening upward: raw/ → hourly/ → daily/ → weekly/ → monthly/   knowledge │ 13 entities/ people/ projects/ — the nouns of Rohan's world, one file each   decisions │ 12 comms/ chat/ email/ meetings/ — one file per thread, named <who>--<topic>--<date>.md  x-account-signup-dec│ 11 workstreams/ active/ completed/ someday/ — units of work; status: frontmatter matches the subdirectory   preferences │ 10 knowledge/ facts/ procedures/ preferences/ decisions/   instinct │ 8  autonomy.md │ 7 knowledge/preferences/instinct/ holds how Rohan wants the assistant itself to behave — autonomy, drafti  iteration-style.md│ 6   timeline │󰫎 5 󰲣 Conventions   daily │ 4  2026-09-06.md │ 3 ● Every file has frontmatter with id, type, and aliases. [[id]] resolves to id.md or id/_index.md.   workstreams │ 2 ● Entity and knowledge files are updated in place and read as current state, never as a log. History li 