콘텐츠로 이동

프라이버시 일시정지

이 페이지는 CLAUDE.md의 구현 노트를 미러합니다. 서브시스템 변경 시 양쪽 다 업데이트하세요.

프라이버시 일시정지는 Cue의 first-class "Cue가 보고 있지 않음" 상태입니다. 수동 핫키, 자동 비밀번호 필드 감지, 설정 가능한 앱 / 윈도우 / URL 필터, 시스템 슬립 / 깨어남을 커버합니다. 활성 시 스트리밍 레코더 kill, 최근 캡처 프레임 purge, 빨간 테두리 오버레이로 상태 시각 확인.

상태 모델

일시정지는 사유의 frozenset[str]로 키잉. 집합이 비어있지 않으면 일시정지 활성.

stateDiagram-v2
    [*] --> Watching
    Watching --> Paused : 사유 추가 → 집합 비어있지 않음
    Paused --> Paused : 사유 추가 / 제거지만 여전히 ≥ 1
    Paused --> Watching : 마지막 사유 제거
    Paused --> [*] : 프로세스 종료 (atexit)

다섯 사유 값:

사유 소스 신호
manual 핫키 또는 트레이 메뉴 토글.
secure_input macOS Carbon IsSecureEventInputEnabled latch + 포커스된 요소의 AX kAXSecureTextField subrole. Windows UIA IsPasswordPropertyId on 포커스된 요소.
blocked_app 최상단 앱 이름이 차단 목록에 있거나 윈도우 타이틀이 설정된 정규식과 매치. store.TERMINAL_APPS 안의 터미널 클래스 앱이 foreground일 때 타이틀 regex 매치는 skip (Terminal / iTerm2 / Warp / 터미널 / ターミナル / 终端 / 終端機 / Console / 콘솔 / WindowsTerminal / Cmd / PowerShell / ConEmu / Cmder) — 로그인 셸 배너 (Last login: …, bash --login)나 login-1 같은 SSH 호스트명이 false-trigger되지 않게.
browser_url 최상단 브라우저 활성 탭 URL이 설정된 정규식과 매치 (브라우저가 authorized 상태일 때만).
system_transition NSWorkspaceWillSleep / DidWake (mac) 또는 WM_POWERBROADCAST (Windows) — 일반 감지 사이클이 동작하기 전 lock-screen / unlock 윈도우를 덮는 1.5초 펄스.

reason_label()이 generic, UI-safe 문자열 반환 — "브라우저 콘텐츠", "차단된 앱", "시스템 슬립/깨어남 전환" — 절대 특정 앱 이름이나 도메인 노출 안 함.

두 컴포넌트 분리

graph LR
    Hooks[NSWorkspace / SetWinEventHook<br/>AXObserver / AX poll<br/>fast 200ms + slow 1s] -- "set() wake" --> Monitor[PrivacyMonitor]
    Monitor -- "submit(reasons)" --> Controller[PauseController]
    Controller -- "queue drain<br/>최신으로 coalesce" --> Worker[Worker thread]
    Worker --> Recorder[recorder.stop_for_pause<br/>recorder.resume_after_pause]
    Worker --> Overlay[overlay.show / hide]
  • PrivacyMonitor가 폴링 루프 (200ms fast / 1s slow)와 OS 이벤트 훅 listening 실행. Hook 콜백은 wake event에 set()만 호출 — set_reasons나 COM/UIA 호출 절대 안 함 — 그래서 OS 이벤트 delivery 스레드를 절대 막지 않음.
  • PauseController가 레코더 + 오버레이 사이드 이펙트 소유. Worker 스레드가 coalescing Queue 드레인하고 한 곳에서만 recorder.stop_for_pause / resume_after_pause / overlay.show / hide 호출. put_nowaitqueue.Full 시 drop — 콜백 storm이 모니터를 막지 않음.

PrivacyMonitor가 작은 신호 표면 노출:

  • request_rescan() — 즉시 폴링 wake.
  • request_rescan_debounced(min_interval_s=0.1) — burst coalesce (이벤트 기반 title/focus 리스너에서 사용).
  • pulse_reason(reason, duration_s)_compute_reasons 안에서 만료되는 transient 사유. system_transition 1.5초 블랙아웃에 사용.

일시정지 진입 시퀀스

recorder.stop_for_pause(purge_lookback_s)가 다음을 함:

  1. _pause_requested set으로 레코더 watchdog가 ocap 재실행 멈춤.
  2. ocap 서브프로세스 즉시 SIGKILL (graceful EOS 없음 — ocap가 잡고 있는 pynput 키보드 tap이 SIGINT grace 기간 동안 macOS auth 다이얼로그를 막을 수 있어서). 최대 1초 wait, 그 후 return.
  3. inclusive >= 파일명 timestamp cutoff로 purge cascade 실행:
  4. 현재 세션의 MCAP + .log 파일 — 통째로 삭제.
  5. pruner.evict_newer_than(cutoff_ns) — lookback 안의 키프레임.
  6. digest.evict_entries_newer_than(cutoff_ns) — cutoff ≥ 디지스트 row를 SQL에서 제거; digest.md를 가장 최신 살아남은 row에서 재작성.
  7. 모든 화면에 빨간 테두리 오버레이 표시.

해제

recorder.resume_after_pause()_pause_requested 클리어 후 새 ocap 세션 spawn. 녹화 catch up까지 ~1초의 빈 공간.

브라우저 권한 tristate

<config root>/browser_auth.json:

  • authorized — 사용자가 macOS Automation 프롬프트 승인; URL 자동 일시정지 활성.
  • denied — 사용자가 명시적으로 거부. fail_closed_on_denied_browsers: True (기본) 시 이 브라우저를 최상단으로 가져오면 일시정지 트리거.
  • unknown — 사용자가 결정 안 함. fail-closed knob 무관 허용적. "Skip-all → 모든 브라우저 brick" 함정을 막는 critical fix.

수동 일시정지 영속성

기본적으로 재시작 시 유지 안 됨 (persist_manual_pause: False). 수동은 ephemeral ("지금 일시정지"); 자동 사유는 다음 launch에 라이브 신호에서 재적용.

스레드 모델 + 락 순서

  • PauseController_lock 소유. enter_pause / exit_pause만이 레코더/오버레이 호출 사이트.
  • Hook 콜백 (NSWorkspace, SetWinEventHook)은 wake event에 set()만 — COM/UIA 호출 또는 lock 절대 안 함.
  • PauseController.submit()put_nowait() 사용 + queue.Full 시 drop. Monitor가 다음 폴링에 재제출; 손실 무해.
  • Worker가 큐 드레인 후 transition 전에 latest 명령으로 coalesce.
  • _pause_lock (recorder)이 outermost; _digest_lock이 innermost. _digest_lock holding 중 module-outside-digest 호출 금지.

atexit 순서

LIFO: recorder.stop 먼저 등록, privacy.stop 두 번째 등록. 종료 시:

  1. privacy.stop 먼저 — 오버레이 숨김, 리스너 unhook, worker join.
  2. recorder.stop이 ocap 정리.

알려진 한계

  • Windows cmd.exe / ssh 안 콘솔 비밀번호 프롬프트가 별도 다이얼로그 윈도우 없이: UIA 표면 없음, 시스템 차원 latch 없음. Wrapping 프로세스 이름 (runas 등)은 차단된 앱이라 권한 상승 플로우 자체가 일시정지 트리거하지만, 별도 프롬프트 윈도우 없이 plain cmd 윈도우에 비밀번호 입력하면 missed. 수동 일시정지 사용.
  • Windows 레거시 Win32 #32770 + ES_PASSWORD 다이얼로그 (이전 VPN 클라이언트, SAP GUI, runas wrapper)는 foreground HWND의 자식 윈도우를 walking하여 ES_PASSWORD 스타일을 가진 visible Edit로 감지. UIA의 IsPasswordPropertyId가 동작할 때 선호.
  • Windows UAC consent 프롬프트secure desktop에서 실행; 사용자 모드 코드는 그 데스크톱을 enumerate 못 하므로 오버레이가 거기에 그릴 수 없음. 포커스가 일반 데스크톱으로 돌아오면 일시정지 fire.
  • macOS loginwindow는 차단 목록 — 잠금 화면 / 빠른 사용자 전환 동안 foreground "앱"이 됨. 의도된 동작.
  • Purge horizon: 감지 전 purge_lookback_s (기본 5초)보다 더 이전에 캡처된 바이트는 살아남음. knob을 늘리거나 수동 일시정지 더 빨리 트리거.
  • 오디오 / 클립보드: 오늘 Cue는 둘 다 캡처 안 함. 미래에 그런 기능 추가 시 작성 사이트에서 privacy.is_paused()를 반드시 consult 해야 함.

더 보기