한 줄로
에이전트를 오래 돌려보면 일하는 시간보다 승인을 기다리는 시간이 길다.
자리를 비우면 그 대기가 전부 멈춤이 된다.
Collie 는 그 판에 폰을 붙인다. 밖에서 승인 번호를 누르고, 답을 보내고,
/compact 를 친다. 3일 동안 폰에서 문장을 21번 보내고 키를 10번 눌렀다.
그 키 10번 중 8번이 승인 대화상자 응답이었다.
무엇이 병목이었나
Herdr 로 판을 여러 개 띄우면 에이전트가 동시에 돈다.
그런데 동시에 돈다고 시간이 줄지 않는다. 이유가 하나 있다.
에이전트는 혼자 끝까지 못 간다. 중간에 묻는다. 파일을 지워도 되는지,
이 명령을 실행해도 되는지. Herdr 는 그 상태를 blocked 로 알아본다.
알아보기만 한다 — 답은 사람이 해야 한다.
그래서 이런 상태가 된다.
$ herdr workspace list
4 블로그 pane 1 blocked ← 3분 전부터 여기서 멈춰 있다
3분이면 괜찮다. 문제는 점심 먹으러 나갔을 때다. 1시간을 나가면 1시간이 통째로
날아간다. 에이전트가 느려서가 아니라 답할 사람이 자리에 없어서다.
판을 아무리 늘려도 이건 안 풀린다. 오히려 판이 많을수록 멈춤이 겹친다.
풀리려면 자리를 비운 채로 답할 수 있어야 한다.
Collie 는 무엇인가
터미널에서 도는 에이전트에 폰용 웹 UI를 붙이는 브리지다.
herdr.collie 라는 이름으로 launchd 에 등록되어 배경에 떠 있다.
소스는 AltanS/collie 에 MIT 로 공개돼 있다 —
이 글 뒷부분에서 몇 군데 열어본다.
구조는 네 층이라고 문서가 밝히고 있다.
| 층 | 하는 일 |
|---|---|
| Web | 폰에서 여는 PWA. 자기를 내려준 브리지와 대화한다 |
| Crew | 여러 머신을 한 URL 뒤에 묶는 선택적 연결 |
| Bridge | 머신 위의 서비스. PWA 를 내려주고 폰의 요청에 답한다 |
| Mux adapter | 이 설치가 비추는 멀티플렉서 하나 — Herdr · tmux · zellij |
내 경우 mux 는 Herdr 다. COLLIE_MUX=herdr 이고,
~/.config/herdr/herdr.sock 으로 붙는다.
여기서 Herdr 와 역할이 갈리는 지점을 짚어둔다.
- Herdr — 판을 나누고, 그 안에서 뭐가 도는지 안다
- Collie — 그 앎을 밖에서 열람하고 조작하게 한다
Collie 는 에이전트를 새로 띄우지 않는다. 이미 돌고 있던 판에 붙는다.
그래서 폰에서 보내는 것은 새 질문이 아니라 아까 그 대화의 다음 줄이다.
폰에서 누른 키가 판에 닿기까지
네 칸이지만 실제로 봐야 할 건 강조된 칸 하나다. 나머지는 통로다.
tailscale serve 로 프론트도어를 하나 연다. 내 경우는 이 주소다.
https://<내-머신>.<내-테일넷>.ts.net → http://127.0.0.1:8787
포트를 여는 게 아니다. 브리지는 127.0.0.1 에만 바인딩되고,
tailnet 안에서만 닿는다. 문서가 이 지점에 경고를 박아 뒀다.
🚫 Never use
tailscale funnelwith Collie. Funnel routes traffic to the
public internet, whereastailscale serverestricts access to your private tailnet.
funnel 은 공개 인터넷으로 낸다. serve 는 내 tailnet 안에서만 닿는다.
글자 하나 차이인데 한쪽은 인터넷 전체에 셸을 여는 것이다.
실제로 무엇을 했나 — 3일치 기록
Collie 는 들어온 쓰기를 전부 audit.log 에 남긴다.
추정이 아니라 파일에 적힌 것을 센다.
$ wc -l < ~/.local/state/collie/audit.log
59
2026-09-10 ~ 09-13, 59줄이다. 갈래를 세면 이렇다.
| 행위 | 횟수 |
|---|---|
reply — 텍스트를 보냄 |
39 |
keys — 키를 직접 누름 |
10 |
| 페어링 · crew · 기기 회수 | 9 |
| 탭 닫기 | 1 |
reply 39번 중 실제 텍스트가 실린 건 21번이고 나머지 18번은
텍스트 없이 submit 만 간 것이다. 입력과 전송이 따로 기록되기 때문이라
내가 폰에서 문장을 보낸 횟수는 21번으로 읽는 게 맞다.
판별로는 이렇게 갈렸다.
| 판 | 횟수 | 무엇이 돌고 있었나 |
|---|---|---|
wS:p2 |
31 | Collie 자체를 손보던 판 |
wE:p1 |
11 | 주간보고 자동화 |
wE:p2 |
4 | 옵시디언 볼트 정리 |
wS:p3 |
3 | 요약 받던 판 |
실제로 밖에서 보낸 문장 몇 개를 그대로 옮긴다.
주간보고 잘 썻어?
지금 옵시디언은 어때?? 잘 돌아가?
/compact
/compact 가 섞여 있는 게 이 도구의 성격을 잘 보여준다.
폰에서 한 게 읽기만이 아니라 세션 관리였다는 뜻이다.
실제로 그 판은 컨텍스트가 차서 밖에서 ctrl+k 로 입력줄을 비우고
다시 /compact 를 쳐 넣어야 했다.
승인 번호를 폰에서 누른다
keys 10번이 이 글의 핵심이다. 기록을 보면 대부분 이 모양이다.
{"action":"keys","paneId":"wE:p1","device":"마이폰",
"detail":{"keys":["5","Enter"]}}
Claude 가 띄운 번호 선택 대화상자에 폰에서 5번을 누른 것이다.
앞서 말한 blocked 가 여기서 풀린다. 자리에 없어도 풀린다.
10번 중 8번에는 promptBinding 이라는 필드가 같이 붙어 있다.
"promptBinding": {
"checked": true,
"passed": true,
"expected": " 결론 3줄 1. 주간보고 못 썼습니다. 09:07에 돌긴 했는데 …"
}
내 폰 화면에 보이던 프롬프트가 무엇이었는지를 같이 보낸다.
checked: true, passed: true 는 그게 판의 현재 화면과 대조되어 통과했다는 뜻이다.
8번 전부 통과했다.
왜 이게 필요한지는 이 도구를 써 보면 바로 안다. 폰 화면은 과거다.
1500ms 폴링이고, 지하철에서는 더 늦는다. 내가 “5번” 을 누를 때
그 판은 이미 다음 질문으로 넘어가 있을 수 있다. 그러면 다음 질문에
엉뚱한 답이 들어간다. 삭제 확인에 “예” 가 꽂히는 사고가 이 구조다.
promptBinding 은 그걸 막는다. 보낼 때 화면을 같이 실어서,
달라졌으면 안 넣는다.
나머지 2번은 이 필드가 없다. 둘 다 ctrl+k·Backspace·Enter 였다 —
번호 선택이 아니라 편집 키다. 고를 선택지가 없으니 대조할 프롬프트도 없다.
대조가 붙는 건 선택지를 고르는 순간뿐이라는 뜻으로 읽었다.
소스를 열어 확인했다
여기까지가 로그만 보고 읽어낸 것이다. promptBinding 은 지금도 공식 문서에 없다 —
docs/ 11개 파일 어디에도 안 나온다. 그런데 소스에는 있다.
bridge/prompt-binding.ts 가 그 구현이고, 추측했던 것보다 규칙이 더 촘촘했다.
대조는 문자열 일치가 아니다. 먼저 정규화를 한 뒤 비교한다.
| 무시하는 것 | 보존하는 것 |
|---|---|
| SGR(색상) escape 시퀀스 | 앞쪽 들여쓰기 |
| 줄 끝 공백 · CRLF/CR 차이 | 문장 중간 정렬 공백 |
| 빈 줄이 늘고 주는 것 | 단어가 바뀌는 것 |
왼쪽은 터미널이 다시 그리기만 해도 변한다 — 그걸로 막으면 멀쩡한 승인이 계속 튕긴다.
오른쪽을 지키는 이유는 주석에 적혀 있다. diff 와 명령어에서는 공백이 내용이기 때문이다.
실제로 " return value;" 와 " return value;" 를 다른 것으로 판정하는
테스트가 들어 있다. 들여쓰기 2칸짜리 수정과 4칸짜리 수정은 다른 수정이다.
내가 못 봤던 건 실패 이유가 하나가 아니라는 것이다.
{ ok: false, reason: "empty" } // 대조할 내용이 비었다
{ ok: false, reason: "not_found" } // 그 프롬프트가 화면에서 사라졌다
{ ok: false, reason: "not_in_tail" } // 있긴 한데 화면 위쪽으로 밀려났다
세 번째가 이 설계의 핵심이다. 프롬프트가 화면에 남아 있어도 마지막 6줄
바깥으로 밀려났으면 거부한다. 같은 질문이 이미 답변되고 아래에 새 질문이
그려진 상황을 걸러내기 위해서다. 문자열만 찾았다면 위쪽의 옛 프롬프트에
매치돼서 통과시켰을 것이다.
6이라는 숫자에도 근거가 붙어 있다.
커밋된 픽스처 전체에서 매치 뒤에 따라오는 정규화된 줄은 최대 1줄이다.
6줄이면 상태 표시나 스피너 갱신에는 넉넉하면서, 교체 프롬프트(정규화 후 20~32줄)가
들어오면 낡은 매치를 확실히 바깥으로 밀어낸다.
측정해서 고른 값이지 적당히 잡은 상수가 아니다.
💡 그리고 “대조가 붙는 건 선택지를 고르는 순간뿐” 이라고 읽었던 것 —
소스를 보니 맞았다. 편집 키에는 대조할 프롬프트 자체가 없다.
⚠️ 다만 설계자에게 물어본 것은 아니다. 위는 공개된 소스와 테스트에 적힌 것을
읽은 결과다. 코드가 무엇을 하는지는 확실하고, 왜 그렇게 정했는지는 주석이 말하는 범위까지다.
알림이 조용히 죽어 있었다
밖에서 답하려면 멈췄다는 걸 알아야 한다. 판을 계속 들여다볼 거면
폰을 붙인 의미가 없다. 그래서 Web Push 를 켰다 — 정확히는, 켠 줄 알았다.
09-11 에 폰에서 이 문장을 보냈다.
서버에 VAPID 키가 구성되지 않아 푸시 알림이 비활성화되었습니다 이건 머야
로그를 열어보니 브리지가 뜰 때마다 같은 줄을 찍고 있었다.
[push] disabled (no VAPID keys configured)
VAPID 는 Web Push 를 보낼 때 “이 알림은 이 서버가 보낸 게 맞다” 를
증명하는 키 쌍이다. 없으면 브라우저가 알림을 안 받는다.
문제는 이게 에러로 안 뜬다는 것이다. 브리지는 정상 기동했고,
폰 UI 도 멀쩡했고, 알림만 안 왔다. 알림이 안 오는 건 조용한 증상이다 —
안 왔다는 걸 알려면 와야 했던 순간을 알아야 하는데, 그걸 알려주는 게
알림이다. collie push-keys 로 키를 만들고 나서야 바뀌었다.
소스를 보면 이게 실수가 아니라 의도된 분기다. bridge/push.ts 의 초기화는
키가 없으면 그냥 로그 한 줄 찍고 돌아온다.
if (!this.cfg.vapidPublic || !this.cfg.vapidPrivate) {
console.log("[push] disabled (no VAPID keys configured)");
return; // throw 가 아니다 — 브리지는 계속 뜬다
}
throw 였다면 브리지가 안 떠서 내가 5분 만에 알아챘을 것이다.
return 이라서 나머지가 전부 정상 동작하고 알림만 없다. 푸시를 안 쓰는
사람까지 기동 실패로 막지 않으려면 이 선택이 맞다. 대신 쓰려던 사람은 조용히
놓친다 — 내가 그 경우였고, 알아챈 건 이틀 뒤 폰에서 물어봤을 때였다.
키를 넣고 다시 띄우면 enabled 로 바뀐다. 다만 아직 구독한 기기가 없다.
[push] enabled (0 saved subscription(s))
폰에서 알림을 허용하면 그때 구독이 하나 생긴다. 지금은 올라와 있다.
{"endpoint":"https://web.push.apple.com/QEMZ…",
"userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 …)"}
알림은 세 가지를 켜 뒀다.
{"blocked": true, "done": true, "updates": true}
blocked 가 이 글의 그 blocked 다. 에이전트가 물어보는 순간 폰이 울린다.
done 은 Herdr 글에서 정리한 대로
“내가 안 보는 사이에 끝났다” 는 뜻이고, 밖에 있을 때는 항상 그 경우다.
폰이 곧 셸이다 — 페어링을 먼저 한다
문서 첫 줄이 이렇게 시작한다.
Collie provides remote shell access to your machine by design.
Treat the URL as a root login.
과장이 아니다. 이 URL 에 닿는 기기는 판에 키를 넣을 수 있다.
샌드박스도 없고 명령 화이트리스트도 없다 — 걸면 도구가 죽으니 안 건다고
문서가 밝히고 있다.
그래서 기기를 짝지었다. 호스트에서 collie pair 를 치면 8자 코드와 QR 이 나오고,
10분 안에 폰에서 넣으면 토큰을 받는다. 호스트에는 해시만 남는다.
{"label":"마이폰",
"tokenHash":"4f1a2cfad794…",
"createdAt":1789032340091}
짝지은 기기는 하나다. 09-10 에 “내 핸드폰” 으로 한 번 짝짓고,
09-11 에 회수한 뒤 “마이폰” 으로 다시 짝지었다. 회수는 즉시 먹는다 —
재시작이 필요 없다고 문서에 적혀 있고, 실제로 그랬다.
관문을 하나 더 세웠다가 도로 내렸다
브리지가 뜰 때마다 찍는 경고가 하나 있다.
[bridge] WARNING: COLLIE_TRUSTED_USER is empty —
any tailnet device/user that reaches the bridge gets full write access.
관문이 둘인데 하나만 세워 뒀다는 뜻이다. 둘은 서로 다른 것을 묻는다.
| 관문 | 묻는 것 |
|---|---|
| 페어링 | 이 기기가 내가 발급한 토큰을 갖고 있나 |
COLLIE_TRUSTED_USER |
이 요청이 내 tailnet 로그인으로 들어왔나 |
페어링만으로도 쓰기는 막힌다. 다만 문서가 짚어두듯 읽기는 남는다 —
read operations remain accessible. tailnet 안의 다른 기기가 URL 을 알면
토큰 없이도 판 출력을 볼 수 있다는 뜻이다.
그래서 켜 봤다. tailscale status --json 이 알려주는 내 로그인 이름이다.
COLLIE_TRUSTED_USER=<내-계정>@github
고치고 collie restart 를 했다. 앞서 crew 에서 겪은 그대로,
파일만 고치면 도는 프로세스는 부팅 때 읽은 값을 계속 들고 있다.
재시작하니 경고 줄이 사라졌다. 그런데 경고가 사라진 건 값이 읽혔다는 증거일 뿐,
차단된다는 증거가 아니다. 직접 찔러 봤다.
$ curl … /api/snapshot # 헤더 없음
403 identity required
$ curl -H "Tailscale-User-Login: <내-계정>@github" … # 내 계정
200 {"bridge":"connected","agents":[…
$ curl -H "Tailscale-User-Login: someone@else" … # 남의 계정
403 identity not trusted
제대로 막혔다. 폰도 확인했다 — 헤더는 tailscale serve 가 넣어주는 것이라
그게 안 되면 내 폰까지 잠긴다.
$ curl https://<내-머신>.<내-테일넷>.ts.net/api/snapshot
200
막혔고, 폰은 살아 있다. 여기까지는 성공이었다.
그런데 doctor 가 눈이 멀었다
collie doctor 를 돌려보니 이렇게 나왔다.
skipped: agent-sessions the bridge did not answer `/api/snapshot`,
so no pane can be checked
doctor 도 헤더 없이 자기 브리지를 호출한다. 그래서 자기가 세운 관문에
자기가 막힌다. 브리지는 멀쩡한데 진단 도구만 아무것도 못 본다.
문서에 COLLIE_TRUSTED_USER_OPTIONAL=1 이 있지만 그건 헤더 없는 요청을
전부 통과시키는 스위치다. 방금 막은 걸 도로 여는 것이라 쓰지 않았다.
그래서 실제 위협이 뭔지를 다시 봤다. 문서가 경고한 건 “모든 로컬 UID 가
포트에 닿는다” 였다. 이 맥에서 그게 성립하는지 센다.
$ dscl . -list /Users UniqueID | awk '$2>=500 && $2<1000'
<내-계정> 501 # 일반 사용자는 나 하나뿐
$ lsof -nP -iTCP:8787 -sTCP:LISTEN
collie … TCP 127.0.0.1:8787 (LISTEN) # 루프백에만
tailnet 쪽도 셌다. tailscale status 에 노드 5개가 있고 전부 내 계정
소유의 내 기기다. 남이 없다.
즉 이 관문이 막아주는 시나리오가 이 머신에는 지금 존재하지 않는다.
반면 doctor 가 못 도는 건 매번 겪는 실질적 손해다. 되돌렸다.
$ curl … /api/snapshot # 다시 200
$ collie doctor # 판을 다시 읽는다
⚠️ 이건 “안 해도 된다”가 아니라 “이 머신에서는 지금 이득이 없다”이다.
tailnet 에 남의 기기가 들어오는 순간, 또는 이 맥에 계정이 하나 더 생기는
순간 판단이 뒤집힌다. 그때는 doctor 를 포기하고 관문을 세우는 게 맞다.
💡 헛다리를 하나 짚었던 것도 적어둔다. 처음엔 /api/state 로 찔러보고
셋 다 200 이 나와서 “설정이 안 먹었나” 했는데, 그런 경로가 없었다.
PWA 라 없는 경로는 전부 index.html 을 200 으로 돌려준다.
차단을 확인하려면 진짜 있는 엔드포인트로 찔러야 한다.
crew 는 포기했다
머신이 둘이다 — 회사 맥과 개인 맥미니. Collie 에는 crew 라는 게 있어서
여러 머신을 한 URL 뒤에 묶을 수 있다. 폰 앱 하나로 양쪽을 보는 그림이다.
09-10 에 붙였다.
09:10:46 crew.enroll <내-맥미니>
09:21:41 crew.remove <내-맥미니>
11분 만에 뺐다. 붙는 중에 이미 다른 문제가 하나 드러났다.
[crew] the trust store changed under this running process —
it still holds the roster it read at boot.
[crew] this machine is now a lead, but the process is still running as a solo
등록은 파일에 됐는데 도는 프로세스는 예전 명부를 들고 있다.
collie restart 전까지는 반영이 안 된다. 설정을 고쳤는데 안 먹는 상황이
여기서 나온다 — 흔한 함정이고, Collie 는 이걸 로그로 말해준다.
09-12 에 다시 붙였다. 이번엔 다른 데서 막혔다.
[crew] <내-맥미니>: unreachable (snapshot: timed out after 5000ms)
[crew] <내-맥미니>: dropped a stale hello reply, dial 104 of 105
[crew] <내-맥미니>: dropped a stale hello reply, dial 113 of 114
114번을 걸어서 붙지 못했다. lead 가 member 를 불러서(dial) 가져오는 구조라
member 쪽이 안 받으면 방법이 없다. 그날 폰에서 이렇게 물었다.
차라리 맥미니를 중심으로 회사 머신에 붙도록 바꾸는건 어때?
방향을 뒤집어도 같은 벽이었다. 왜 그런지는 나중에 소스를 보고 알았다.
설계 문서(ADR 0013)의 첫 줄이 이렇게 정의한다.
A peer is a Collie instance the lead dials.
부르는 쪽이 lead 로 정의되어 있다. 그러니 “맥미니를 중심으로” 바꾼다는 건
연결 방향만 바꾸는 게 아니라 맥미니를 lead 로 세운다는 뜻이고, 그러면 이번엔
회사 맥이 불려야 하는 쪽이 된다. 받지 못하는 머신이 어느 쪽이냐가 그대로 따라온다.
내가 겪은 dropped a stale hello reply, dial 113 of 114 는 이 구조의 증상이다.
lead 는 계속 걸 수 있고, 안 받는 member 를 끌어올 방법은 없다.
그래서 이렇게 끝냈다.
그냥 따로따로 해서 앱 2개 만들게
머신마다 Collie 를 따로 띄우고 폰에 PWA 를 두 개 깔았다.
한 URL 로 묶는 편의를 버리고 확실히 붙는 쪽을 골랐다.
지금 mode 는 solo 이고 명부는 비어 있다.
{"mode":"solo","roster":[]}
돌아보면 crew 는 내 상황에 필요한 기능이 아니었다. 두 머신을 동시에
보는 일이 거의 없다. 회사 맥을 볼 때와 맥미니를 볼 때가 애초에 다른 시간대다.
붙이느라 쓴 이틀은 기능이 있어서 붙인 것이지 필요해서 붙인 게 아니었다.
doctor 가 말해주는 것
앞에서 두 번 나왔으니 정리해 둔다. doctor 는
조용히 실패하는 것들을 잡아주는 명령이다. 지금 돌리면 하나가 빨간색으로 뜬다.
error: agent-sessions 10 agent pane(s) … 2 report NO session: w9:p2 (claude), wS:p2 (claude)
— their History link and icon are hidden, and the pane looks otherwise normal
w9:p2 는 지금 이 글을 쓰는 판이다. 훅이 깔리기 전에 시작한 세션이라
Herdr 에 자기 세션 ID 를 보고하지 못했다. 그래서 폰에서 이 판의 History 를
못 연다.
주목할 건 뒷부분이다 — “the pane looks otherwise normal”.
판은 멀쩡해 보인다. 목록에도 뜨고 출력도 보이고 키도 들어간다.
History 만 조용히 없다. doctor 를 안 돌리면 모를 종류의 고장이다.
고치는 법도 같이 알려준다 — 새 세션을 시작하면 된다. 훅은 세션 시작 때
로드되므로 이미 도는 세션에는 못 붙는다.
안 만들기로 한 것들
소스를 받아보고 가장 인상적이었던 건 기능 목록이 아니라 .adr/ 폴더였다.
ADR(Architecture Decision Record) 45개 — 무엇을 만들었는지가 아니라
무엇을 안 만들기로 했는지와 그 이유를 적어둔 문서다.
터미널을 제대로 그려주지 않는 이유
폰 화면의 터미널은 가끔 깨진다. TUI 표가 어긋나고 대화상자가 밋밋한 글자로 떨어진다.
당연히 “제대로 된 터미널 에뮬레이터를 넣자”는 제안이 나왔고,
ADR 0008 은 그걸 “이 레포에서 가장 자주 다시 제안되는 아이디어” 라고 부르며 거절한다.
이유가 내 예상과 달랐다. 성능이나 용량이 아니었다.
pane.read는 바이트 스트림을 반환하지 않는다. Herdr 가 이미 그려낸 화면을 반환한다.
에뮬레이션이 이미 한 단계 위에서 끝나 있다. 여기에 에뮬레이터를 또 넣으면
“두 번째 렌더러가 첫 번째와 불일치하는” 상태가 된다. 화질이 좋아지는 게 아니다.
그리고 실제 버그들을 표로 늘어놓고 하나씩 “에뮬레이터로 고쳐지나?” 를 묻는다.
답은 전부 아니오였다. 대화상자가 깨진 건 픽셀 문제가 아니라
“어느 줄이 입력창인가”라는 의미의 문제였기 때문이다.
TUI 는 셀을 칠하지, 구조를 칠하지 않는다.
격자가 더 잘 그려진다고 답이 나오지 않는다 — 격자에 애초에 답이 없었다.
폰이 터미널 크기를 건드리지 않는 이유
같은 문서의 개정 노트에 이게 있다. 폰에서 판이 좁게 보이니 자동으로 넓히자는 제안인데,
실측해보니 이런 결과였다.
| 상황 |
pane.zoom 효과 |
|---|---|
| 데스크톱에 아무도 없음 (= 폰이 유일한 클라이언트) | 아무 일도 안 일어남 |
| 누군가 데스크톱 앞에 있음 | 동작함 — 그런데 그 사람 화면이 튐 |
필요한 경우엔 안 먹고, 먹는 경우엔 남의 화면을 망친다. 그래서 안 한다.
“판의 배치는 조작자의 화면이고, Collie 는 거기에 쓰지 않는다”가 규칙이다.
아이들락은 보안장치가 아니라고 스스로 철회한 기록
ADR 0007 이 재미있다. 원래 문서가 아이들락을 보안 조치라고 설명해뒀는데,
그 설명을 스스로 내린다. 이유는 그게 실제로 그 일을 한 적이 없어서다.
클라이언트 React 상태라 새로고침 한 번이면 지나간다. 대신 비용은 진짜였다 —
탭을 30분 백그라운드에 두고 돌아오면 잠겨 있었고, 잠금 화면이 라우터를
언마운트해서 쓰다 만 답장이 조용히 날아갔다.
코드가 구현한 적 없는 방어를 문서가 주장하게 두는 건 방어가 없는 것보다 나쁘다.
그걸 믿고 진짜 필요한 조치를 건너뛰게 만들기 때문이다.
내가 앞에서 COLLIE_TRUSTED_USER 를 켰다 껐던 것도 같은 종류의 판단이다.
관문이 있다는 사실과 그게 막아준다는 사실은 다르다.
아무것도 수집하지 않는다
ADR 0034 는 텔레메트리를 정책으로 금지한다. 켜는 플래그조차 없다 — 켤 게 없으니까.
논리가 간결하다.
Collie 는 조작자 본인 머신에 대한 원격 셸 접근이다.
그만한 것을 쥔 프로그램이 동시에 집으로 전화를 걸 수는 없다.
여기서 눈에 띈 건 반대 의견을 같이 적어뒀다는 점이다. “신호가 없으면 몇 명이
쓰는지, 어떤 기능이 값어치를 했는지 아무도 모른다 — 그건 진짜 손실이고 감수한다.”
유일한 예외인 업데이트 확인도 범위를 못 박는다. GitHub 태그 목록을 묻는 익명 GET 하나,
설치 ID 도 쿠키도 없음. “무엇이 있냐고 묻는 요청은 무엇을 보고하는 요청이 아니다.”
이걸 왜 적어두나
세 편을 쓰면서 내가 계속 부딪힌 문제가 있다. 동작은 관찰할 수 있는데 의도는 모른다는 것이다.
앞에서 promptBinding 을 두고 “설계 의도를 확인한 건 아니다”라고 단서를 달아야 했던 이유다.
ADR 은 그 간극을 메우려고 있는 문서다. 코드는 결과만 보여주고,
버려진 선택지와 그 이유는 코드에 남지 않는다. 거절된 PR 번호, 실측해보고 뒤집은 가설,
“이건 또 제안될 것이다”라는 예고까지 적혀 있으니 같은 논의가 반복될 때 처음부터 다시 하지 않아도 된다.
⚠️ 물론 ADR 도 작성자의 서술이다. 코드와 일치하는지는 별개 문제이고,
위 내용은 내가 읽은 시점(1.9.0)의 것이다.
정리
- 에이전트 여러 개를 돌릴 때 진짜 병목은 처리 속도가 아니라 승인 대기다.
자리를 비우면 그 대기가 전부 멈춤이 된다 - Collie 는 그 판에 폰을 붙인다. 에이전트를 새로 띄우는 게 아니라
돌고 있던 대화에 이어 붙는다 - 3일간 폰에서 문장 21번, 키 10번. 그중 8번이 승인 대화상자 응답이고
여기서 멈춤이 풀렸다 - 폰 화면은 과거다.
promptBinding이 보낼 때 화면을 같이 실어 대조한다. 8번 다 통과 - 그 필드는 문서에 없지만 소스에는 있다. 색·줄끝공백은 무시하고 들여쓰기는 지킨다
(diff 에서는 공백이 내용이니까). 실패 이유가 3가지인데, 그중not_in_tail이 핵심이다 —
화면에 남아 있어도 마지막 6줄 밖이면 거부해서 “이미 답한 옛 질문”에 매치되는 걸 막는다 - ⚠️ VAPID 키가 없으면 푸시가 조용히 죽는다. 에러가 안 뜨고 알림만 안 온다.
알림이 안 온 걸 알려주는 게 알림이라 스스로는 못 알아챈다.
소스를 보면throw가 아니라return이라 의도된 분기다 — 안 쓰는 사람을
기동 실패로 막지 않는 대신, 쓰려던 사람이 조용히 놓친다 - ⚠️
funnel이 아니라serve다. 한 글자 차이로 인터넷 전체에 셸이 열린다 - URL 은 root 로그인이다. 관문은 둘이고 묻는 게 다르다 — 페어링은 토큰을,
COLLIE_TRUSTED_USER는 tailnet 로그인을 묻는다. 페어링만 세우면
쓰기는 막히고 읽기는 남는다 - 두 번째 관문은 켰다가 되돌렸다.
doctor도 헤더 없이 호출하므로
자기가 세운 관문에 자기가 막힌다. 이 맥은 사용자가 나 하나, tailnet 도 내 기기뿐이라
막아주는 시나리오가 없는데 진단만 잃는 거래였다.
⚠️ 조건이 바뀌면 판단도 뒤집힌다 - ⚠️ 경고가 사라진 건 차단됐다는 증거가 아니다. 값이 읽혔다는 것뿐이다.
없는 경로는 200 을 돌려주므로, 진짜 엔드포인트로 403 을 직접 확인해야 한다 - crew 는 114번 dial 끝에 포기하고 앱 2개로 갔다. 두 머신을 동시에 볼 일이
애초에 없었다 — 기능이 있어서 붙인 것이었다 - 방향을 뒤집어도 안 됐던 이유는 설계에 있다. “부르는 쪽이 lead” 라고 정의되어 있어서,
중심을 바꾸면 못 받는 머신이 반대편으로 옮겨갈 뿐이다 - 소스의
.adr/에 안 만들기로 한 결정 45개가 이유와 함께 있다. 터미널 에뮬레이터를
거절한 근거(“에뮬레이션은 이미 Herdr 에서 끝났다”), 아이들락을 보안장치라고 한 자기 문서를
철회한 기록. 코드는 결과만 남기고 버려진 선택지는 안 남는다 — 그걸 적어둔 문서다 - 설정 파일을 고쳐도 도는 프로세스는 부팅 때 읽은 걸 들고 있다.
restart전까지 반영 안 된다 -
doctor는 멀쩡해 보이는 고장을 잡는다. 내 판 둘이 History 만 조용히 없다
📌 버전 주의. 측정과 로그는 내가 돌리는 1.8.2 에서 나온 것이고,
소스와 ADR 은 1.9.0 을 읽었다. 1.9.0 변경 목록에 promptBinding 이나
위에서 인용한 결정을 뒤집는 항목은 없지만, 화면 구성은 꽤 달라졌다
(대시보드 정렬, 키보드 위 액션 바 등). 두 버전을 나란히 비교한 것은 아니다.
같이 읽을 글:
Herdr 란 ·
문서에 없는 API 를 바이너리에서 꺼냈다 ·
SSH 로는 되는데 Herdr 만 못 찾는다 ·
내가 돌리는 워크플로우 설계