브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유
Cursor나 GrokBot 같은 브라우저 에이전트는 혼자 일하는 개발자에게 매력적인 선택지다. 일일이 클릭하기 귀찮은 외부 대시보드 점검이나 경쟁사 모니터링을 알아서 처리해 준다.
문제는 이 도구를 호스트 머신의 브라우저 세션 그대로 연동할 때 터진다. 신뢰할 수 없는 웹페이지를 긁어오다가 간접 프롬프트 주입(Indirect Prompt Injection)을 당하면, 로컬 스토리지에 남아 있던 세션 쿠키와 인증 토큰이 외부 서버로 그대로 빠져나간다. 여기에 에이전트가 DOM 요소를 찾지 못해 같은 요청을 끝없이 되풀이하기 시작하면, 아침에 일어났을 때 수백 달러짜리 API 청구서를 마주하게 된다.
자연어 프롬프트가 알아서 예외를 처리해 줄 거라는 기대는 접어야 한다. 런타임 자체를 물리적으로 격리하고, 돈과 시간이 새어나가지 않도록 안전장치를 걸어두는 편이 훨씬 안전하다.
세션 오염과 자격 증명 탈취 차단하기
에이전트가 사용하는 브라우저 프로필은 본인이 업무용으로 쓰는 환경과 완전히 떼어내야 한다. Chromium 136 버전부터 기본 사용자 데이터 디렉터리(--user-data-dir)에 대한 원격 디버깅 연결이 차단되는 등 격리 기준이 엄격해졌다.
운영체제의 기본 자격 증명 서브시스템을 끄고, 독립된 디렉터리로 브라우저를 띄워야 한다.
google-chrome \
--user-data-dir="/opt/grokbot/isolated_profiles/worker_01" \
--profile-directory="AgentContext" \
--disable-save-password-bubble \
--disable-fill-on-account-select \
--credentials_enable_service=false \
--no-first-run \
--no-default-browser-check
계정 권한도 손봐야 한다. 구글 관리 콘솔(admin.google.com)의 보안 > 액세스 및 데이터 제어 > Google 세션 제어로 들어간다. 에이전트 전용 보조 조직 단위(Agent-Sandboxed-OU)를 만들고, 웹 세션 지속 시간을 기본 14일에서 24시간(1,440분)으로 줄여서 저장한다.
결제 대시보드를 확인할 때도 전체 관리자 권한을 가진 비밀 키(sk_live_...)를 쓰면 위험하다. Charges: Read, Subscriptions: Read 권한만 넣은 제한된 키(rk_live_...)를 따로 뽑아서 넘겨야 한다.
| 연동 서비스 |
허용 권한 |
금지 권한 |
주입 방식 |
차단되는 위험 |
| Stripe |
Charges: Read, Subscriptions: Read |
Charges: Write, Payouts: Read/Write |
rk_live_... (환경 변수) |
무단 환불 및 정산 계좌 변경 |
| Google Workspace |
gmail.readonly, drive.metadata.readonly |
gmail.send, gmail.modify |
Scoped OAuth 2.0 Access Token |
사칭 피싱 메일 발송 및 파일 변조 |
| Vercel / AWS |
Read-only Monitoring, Logs View |
Deployments, Secrets Edit |
Scoped Token / IAM Role |
운영 인스턴스 임의 종료 |
설정이 끝났다면 에이전트에게 Stripe 정산 계좌 관리 주소([https://dashboard.stripe.com/settings/payouts](https://dashboard.stripe.com/settings/payouts))로 들어가 보라고 시켜보자. 브라우저 화면에 재인증 창이 뜨거나 HTTP 403 에러가 떨어지면 격리가 제대로 된 상태다.
무한 루프와 API 크레딧 과소비 끊어내기
브라우저 에이전트는 버튼 하나를 못 찾으면 프롬프트를 조금씩 바꿔가며 핑퐁 루프를 돈다. 128k 토큰 컨텍스트를 쥐고 수십 번 재시도를 반복하는 순간 토큰 비용이 무섭게 치솟는다. 프롬프트에 "실패하면 멈춰"라고 적어두는 정도로는 부족하다. 모델은 종종 그 지시를 무시한다.
코드 레벨에서 강제로 프로세스를 죽이는 서킷 브레이커를 두어야 한다. 먼저 오케스트레이션 설정 파일(.cursorrules 또는 GrokBot 환경 설정)에 재시도 상한을 박아둔다.
AGENT ORCHESTRATION CONSTRAINTS
Execution Thresholds:
MAX_RETRIES_PER_TASK = 2
PER_STEP_TIMEOUT_SECONDS = 300
DAILY_TOKEN_BUDGET_HARD_CAP_USD = 5.00
Deterministic Termination Rules:
동일한 DOM 셀렉터 실패가 2회 연속 기록되면 즉시 프로세스를 종료한다.
표현을 바꿔 3회차 재시도를 시도하지 않는다.
단일 세션 비용이 $5.00에 도달하면 즉각 모든 작업을 중단하고 종료 코드를 반환한다.
파이썬 스크립트로 연속 실패를 감시하고, 한계에 닿으면 텔레그램으로 로그를 쏜 뒤 프로세스를 즉각 종료시킨다.
import os
import sys
import logging
import requests
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
class AgentCircuitBreaker:
def __init__(self, max_consecutive_failures=2):
self.consecutive_failures = 0
self.max_failures = max_consecutive_failures
def record_failure(self, task_name: str, error_trace: str, current_url: str):
self.consecutive_failures += 1
logging.warning(f"Task '{task_name}' failed ({self.consecutive_failures}/{self.max_failures})")
if self.consecutive_failures >= self.max_failures:
self.trigger_kill_switch(task_name, error_trace, current_url)
def record_success(self):
self.consecutive_failures = 0
def trigger_kill_switch(self, task_name: str, error_trace: str, current_url: str):
message = (
f"[CIRCUIT BREAKER TRIGGERED]\n"
f"Task: {task_name}\n"
f"URL: {current_url}\n"
f"Cause: Consecutive Failures >= {self.max_failures}\n"
f"Trace: {error_trace[:400]}"
)
api_url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
payload = {"chat_id": TELEGRAM_CHAT_ID, "text": message}
try:
requests.post(api_url, json=payload, timeout=5.0)
except Exception as e:
logging.error(f"Failed to post webhook: {e}")
sys.exit(1)
에이전트 인스턴스 하나당 검사 대상은 최대 50건으로 묶어둔다. 50건을 채우면 브라우저 컨텍스트(context.close())를 완전히 날리고 새로 띄운다. 배치 작업 사이에 60초 정도 쉬는 구간을 두면 Stripe 기준 초당 요청 한도(25 req/sec)에 걸려 계정이 잠기는 일도 피할 수 있다.
동작을 확인하려면 존재하지 않는 DOM 셀렉터(#phantom-settlement-modal)를 클릭하라는 테스트 작업을 던져본다. 1회 재시도(총 2회 시도) 후 즉시 멈추고 텔레그램 알림이 도착한다면 하루 지출은 $5 안쪽으로 묶인다.
SPA 하이드레이션 지연 다루기
React나 Next.js로 만든 대시보드는 HTML이 로드된 뒤에도 자바스크립트 번들을 풀고 하이드레이션을 거친다. 에이전트가 window.onload 이벤트만 보고 바로 진입하면 스켈레톤 로더를 실제 데이터로 착각해 에러를 뿜는다. 그렇다고 무작정 time.sleep(5)을 걸어두는 건 시간을 너무 낭비한다.
명시적 앵커 요소를 두고 폴링 함수를 실행해야 한다.
import logging
from playwright.sync_api import Page, TimeoutError
def wait_for_dashboard_hydration(
page: Page,
anchor_selector: str,
max_attempts: int = 3,
interval_ms: int = 3000
) -> bool:
for attempt in range(1, max_attempts + 1):
try:
page.wait_for_selector(anchor_selector, state="visible", timeout=interval_ms)
if not page.locator(".dashboard-skeleton-loader").is_visible():
return True
except TimeoutError:
logging.info(f"Hydration waiting: Attempt {attempt}/{max_attempts}")
return False
접근성 트리를 통해 타겟 노드의 텍스트(Total Volume: $12,450.00)를 긁어오고, 멀티모달 모델에 현재 화면 캡처를 넘겨 카드 UI 속 숫자와 대조한다. 두 값이 다르면 아직 렌더링이 덜 끝난 것으로 보고 3초 뒤에 다시 확인한다.
크롬 개발자 도구 프로토콜(CDP)로 가상 네트워크 지연 환경을 만들어서 이 로직을 점검할 수 있다.
cdp_session = page.context.new_cdp_session(page)
cdp_session.send("Network.emulateNetworkConditions", {
"offline": False,
"latency": 500,
"downloadThroughput": 400 * 1024 / 8,
"uploadThroughput": 400 * 1024 / 8
})
RTT 500ms, 다운로드 400kbps의 지연을 건 상태에서도 에이전트가 섣부르게 실패를 띄우지 않고 3회(총 9초) 대기하며 데이터를 온전히 긁어오는지 확인하면 된다.
3분 만에 끝내는 아침 로그 점검
에이전트가 밤새 작업하다 멈췄을 때 수천 줄짜리 텍스트 로그를 뒤지는 것만큼 피곤한 일도 없다. 모니터링을 수습하느라 매주 서너 시간을 날리면 자동화를 한 의미가 사라진다.
모든 하위 에이전트가 액션을 마칠 때마다 timestamp, url, action 세 가지만 담아 단일 라인 JSONL 파일로 떨구도록 맞춘다.
{"timestamp": "2026-03-31T08:15:02Z", "url": "https://dashboard.stripe.com/payments", "action": "CLICK", "target": "button[data-testid='filter']", "status": "SUCCESS"}
{"timestamp": "2026-03-31T08:15:05Z", "url": "https://dashboard.stripe.com/payments", "action": "WAIT_FOR", "target": "div[data-testid='metrics-card']", "status": "RETRY_1", "error": "Timeout 3000ms"}
{"timestamp": "2026-03-31T08:15:08Z", "url": "https://dashboard.stripe.com/payments", "action": "EXTRACT_TEXT", "target": "div[data-testid='metrics-card']", "status": "FAIL", "screenshot_path": "artifacts/errors/metrics_fail.png"}
작업이 끝나면 리드 봇이 FAIL 태그가 붙은 항목만 골라내 마크다운 요약본(daily_failure_digest.md)을 뽑는다.
# Daily Agent Failure Digest (2026-03-31)
## Summary Metrics
Total Jobs: 50 | Succeeded: 48 | Failed: 2 | Total Token Cost: $0.84
## Critical Failure Cases
### Case 1: Stripe Payout Audit
- Timestamp: 2026-03-31T08:22:11Z
- URL: https://dashboard.stripe.com/payouts
- Last Action: CLICK -> button#export-csv
- Error: DOM_ELEMENT_NOT_FOUND
- Artifact: artifacts/errors/payouts_20260331_fail.png
- Note: 버튼 셀렉터가 #export-csv-v2로 변경된 것으로 보임.
출근해서 다이제스트를 열고 실패 건수 확인에 1분, 스크린샷 열어서 바뀐 셀렉터 확인에 1분, 수정 승인 체크박스를 누르는 데 1분이면 끝난다. 매일 아침 로그를 붙잡고 씨름할 필요가 없다.
모든 작업에 브라우저 에이전트를 쓸 필요는 없다
브라우저 에이전트는 어디까지나 비정형 환경을 다루는 수단이다. 공식 API가 잘 뚫려 있는 곳에 굳이 브라우저 에이전트를 투입할 이유는 없다.
| 구분 |
공유 클라우드 브라우저 |
격리형 무상태 컨테이너 |
공식 REST API |
| 초기 설정 |
프롬프트 입력으로 바로 구동 |
도커 이미지와 네트워크 격리 필요 |
엔드포인트 인증 및 매핑 필요 |
| 보안 격리 수준 |
Chromium 프로필 및 세션 정책 필수 |
작업 완료 시 컨테이너 파기 |
브라우저 쿠키 탈취 위험 없음 |
| 실행 비용 |
비전 추론 및 DOM 직렬화로 높음 |
호스팅 비용과 LLM 추론 비용 발생 |
JSON 파싱만 거치므로 매우 낮음 |
| UI 변경 복원력 |
시각 정보 기반으로 우회 가능 |
셀렉터 수정 코드 배포 필요 |
UI 변경에 전혀 영향받지 않음 |
| 적합한 작업 |
API 없는 외부 대시보드 점검 |
대규모 웹 스크래핑 |
Stripe 결제 확인, 데이터베이스 변경 |
돈이 오가는 결제나 계좌 변경 작업은 공식 API를 써야 안전하다. 반면 API를 주지 않는 파트너사 어드민 확인이나 화면 모니터링처럼 사람 손을 타던 영역에는 브라우저 에이전트가 제 몫을 한다. 물리적인 브라우저 격리와 서킷 브레이커를 걸어두면 밤새 에이전트를 돌려도 마음이 편하다.