"권한이 네 코드한테 거부된 게 아냐. 네 코드의 identity 한테 거부된 거고 — 벌거벗은 인터프리터엔 그게 거의 없어."
같은 Python, 다른 두 답
두 번째 전쟁 이야기, 지난번과 같은 미치는 모양. executor 는 마운트된 볼륨에서 영상 아카이브를 읽어야 해 — 그건 현대 macOS 에선 유저가 full-disk 파일 접근을 승인해야 해. 승인하고, SSH 로 들어가, Python 을 돌리면, 아카이브를 완벽히 읽어. 그다음 서비스 매니저가 정확히 같은 인터프리터를, 같은 기계에서 띄우면, 운영체제가 파일 접근을 거부해.
이유는 권한 시스템이 코드에 접근을 승인하는 게 아니라서야. identity — 코드 서명된 애플리케이션, 책임 프로세스에 귀속된 — 에 승인해. 인터랙티브 셸 세션은 시스템이 이미 신뢰하는 identity 를 지니고, 네 Python 이 그 컨텍스트를 상속해. 서비스 매니저가 띄운 벌거벗은 인터프리터 바이너리는 그런 승인 가능한 identity 를 안 지녀서, 요청이 유저가 승인한 적 없는 것에 착지하고, 거부돼.
그리고 놓치기 쉬운 방식으로 더 나빠져: 그 Python 바이너리에 직접 접근을 승인해봐도, 일상적 환경 업데이트가 그 바이너리를 교체할 수 있고 — 그 특정 바이너리에 붙었던 승인이 증발해. 다음에 툴링을 업데이트하면 조용히 만료되는 권한을 지은 거야. 갈아끼울 수 있는 파일에 준 승인은 네가 고르지도 않았고 통보받지도 않을 만료일이 붙은 승인이야.
고침: 승인이 살 안정된 identity 를 줘
Recall 의 답은 OS 한테 교체 가능한 파일을 믿으라고 하길 그만두는 거야. 서비스 매니저가 안정된 bundle identifier 를 가진 작고, 기계-로컬이고, 코드 서명된 애플리케이션 번들을 host 해. 그 launcher 가 시스템이 보는 거고; 책임 부모 프로세스고, 런타임 Python 을 자식으로 spawn 해. full-disk 접근은 안정된 앱 identity 에 한 번 승인돼 — 다음 주에 conda 가 교체할 수 있는 인터프리터가 아니라. 이제 권한이 그 밑 툴체인의 모든 일상 업데이트보다 오래 살아, 안 흔들리는 것에 붙었으니까.
그게 옮겨 쓸 원리야: 권한 승인은 identity 에 붙으니, identity 는 안정되고 일부러 소유돼야 해. 네 승인이 일상적으로 재생성되거나, 업그레이드되거나, 재설치되는 것에 고정돼 있으면, 접근을 승인받은 게 아냐 — 다음 업데이트까지 빌린 거야.
나중에 무는 디테일: 신호를 전달해
서비스 매니저랑 진짜 작업 사이에 launcher 를 끼우면 새 의무가 하나 생기고, 그걸 건너뛰면 고약한 부류의 버그가 나와. 서비스 매니저가 job 을 멈출 때, 자기가 아는 프로세스 — launcher — 에 신호를 보내. launcher 가 그냥 죽으면, 그게 spawn 한 Python 자식이 고아가 될 수 있어: 아직 도는 전사, 아직 lease 를 쥔 채, 네가 통제한다고 생각한 lifecycle 엔 안 보이게. 그래서 launcher 는 종료 신호를 전체 worker 프로세스 그룹에 전달해. restart 는 restart 를 뜻해야지, 'restart 더하기 아직 일하는 유령' 이 아니라. 진짜 작업 앞에 supervisor 를 둘 때마다, 그 일은 시작하는 것만이 아냐 — 멈춤이 자기가 시작한 전부를 실제로 멈추게 하는 거야.