- name
- cannot-do-x-must-be-narrowed-by-a-control-you-own
- description
- "에이전트라서 못 한다 / OS 경계다 / 권한이 없다" 는 능력을 통째로 부정하는 진단이고, 거의 항상 너무 넓다. 반증은 싸다 — 같은 종류의 자원을 **내가 만들고 내가 통제하는 인스턴스**로 하나 세워 똑같은 연산을 해봐라. 성공하면 진단은 즉시 '그 객체가, 그 상태에서' 로 좁아진다. 트리거 - "권한이 없어서 못 합니다", "비대화형이라 안 됩니다", "OS/샌드박스 제약입니다", 인수인계가 여러 세션 이월한 '사람 손이 필요한 것', 사람이 직접 해도 같은 에러가 났을 때.
# "못 한다"는 범위 주장이다 — 내가 통제하는 대조군으로 좁혀라
## Problem
막힌 연산을 만나면 진단이 **능력 전체**로 점프한다:
> *"Keychain 쓰기는 에이전트 불가 (OS 경계)"*
> *"비대화형 세션이라 macOS 가 거부한다"*
그럴듯하다. 에러 메시지도 거들어준다 — `User interaction is not allowed.` 는
**정말로** "상호작용이 필요한데 못 한다"고 말한다.
그리고 이 진단은 **자기를 검증할 방법이 없어 보인다.** 그래서 인수인계로 넘어가고,
다음 세션이 그대로 인용하고, 규율로 승격되고, **사람이 할 일 목록**에 남는다.
실측: 이 형태의 오진이 **세 세션** 이월됐다.
### 사람이 돌려도 같은 에러가 나면 — 그건 반증이 아니라 **다른 질문**이다
*"에이전트라서 못 한다"* 를 검증하려고 사람에게 시켰고, 사람도 같은 에러를 받았다.
여기서 *"그럼 에이전트 탓이 아니구나"* 까지는 맞다. 그런데 **그 다음이 비어 있었다** —
원인 후보가 통째로 사라졌는데 새 후보를 세우지 않았다.
⚠️ 사람이 **같은 셸/같은 세션**에서 돌렸다면 그건 대조군이 아니다. 바꾼 변수가
*"누가 타이핑했나"* 뿐이고, 그건 대개 아무것도 아니다.
## Solution
### 대조군: 같은 종류의 자원을 **내가 만들고 내가 통제한다**
막힌 것이 `login.keychain-db` 였다면, **keychain 을 하나 만들어서** 같은 연산을 해라:
```bash
security create-keychain -p probepass bsvibe-probe.keychain # rc=0
security unlock-keychain -p probepass bsvibe-probe.keychain # rc=0
security add-generic-password -U -s probe-svc -a probe@x -w s bsvibe-probe.keychain # rc=0 ✅
security find-generic-password -s probe-svc -a probe@x -w bsvibe-probe.keychain # 값 나옴 ✅
security delete-keychain bsvibe-probe.keychain # 정리
```
**전부 성공했다.** 같은 프로세스, 같은 세션, 같은 바이너리다.
⇒ 진단이 그 자리에서 **무너지고 동시에 좁아진다**:
~~"이 세션은 keychain 을 못 쓴다"~~ → **"`login.keychain-db` 가 잠겨 있다"**
⇒ 그리고 좁아진 진단은 **처방이 다르다.** 넓은 쪽의 처방은 *"사람이 GUI 로 해주세요"*
였다. 좁은 쪽의 처방은 *"그 keychain 을 잠금 해제하라"* 이고, 그건 사람의 **다른**
한 줄이다. 오진은 사람에게 **틀린 일**을 시키고 있었다.
### 이 대조군의 일반형
| 막힌 것 | 내가 통제하는 대조군 |
|---|---|
| 특정 keychain 에 쓰기 | 새 keychain 만들고 잠금 해제해서 쓰기 |
| 특정 DB/테이블 쓰기 | 스크래치 스키마에 임시 테이블 만들어 쓰기 |
| 특정 디렉터리 쓰기 | `mktemp -d` 에 같은 연산 |
| 특정 API 키로 호출 | 내 키/스크래치 엔드포인트로 같은 호출 |
| 특정 포트 바인드 | 임의 고포트 바인드 |
| 특정 리포 push | 내가 만든 bare repo 에 push |
판별 질문: **"이 연산을, 내가 처음부터 끝까지 만든 대상에 대고 하면 되는가?"**
* 된다 → 막힌 것은 **연산이 아니라 그 대상(또는 그 상태)** 이다
* 안 된다 → 그제서야 능력/경계 이야기를 할 자격이 생긴다
### 두 번째 축: **어느 세션에서 쟀나** — 내 실패는 데몬의 실패를 예측하지 않는다
macOS 에서 실측:
```
$ launchctl managername
Background ← 내 셸
$ launchctl print gui/501/com.blas1n.<job>
type = LaunchAgent ← 야간 데몬은 Aqua(GUI) 세션
```
**두 세션은 keychain·권한·UI 접근이 서로 다르다.** 그러니 내 Background 셸이
실패했다는 사실은 **04:20 에 도는 LaunchAgent 가 실패한다는 뜻이 아니다.**
⇒ 능력을 판정하기 전에 **재는 세션과 쓰는 세션이 같은지** 확인해라.
다르면 판정은 *"내 세션에서 안 된다"* 까지만 유효하다.
(같은 계열: `the-detector-must-run-where-the-config-lives`)
### 세 번째: 실패한 명령의 **rc 를 사람이 아니라 코드가 읽게 해라**
이 오진이 세 세션을 산 진짜 이유는, 그 실패를 매일 밤 기록하는 로그가 **이유를
버리고 있었기** 때문이다(`2>/dev/null`). 종료코드는 두 원인을 완벽히 갈랐는데
(`44` 부재 vs `36` 잠김) 아무도 안 봤다.
⇒ 능력 진단을 넘기기 전에, **그 실패를 자동으로 겪는 자리가 이유를 보존하는지**
확인해라. 보존 안 하면 다음 세션도 같은 추측을 한다.
(`a-failed-read-degraded-to-empty-becomes-a-measurement` 의 셸 절)
## Key Insights
- **"못 한다"는 범위 주장이다.** 범위 주장은 **한 건의 성공**으로 무너진다 —
그래서 반증이 싸다. 안 재는 이유는 비용이 아니라 **재볼 생각을 안 해서**다.
- **에러 메시지는 자기가 아는 만큼만 말한다.** `User interaction is not allowed` 는
참이지만 *왜* 상호작용이 필요한지는 안 말한다. 문장을 원인으로 승격하지 마라.
- **사람이 같은 에러를 받아도 원인이 확정되지 않는다.** 바꾼 변수가 "누가 쳤나"뿐이면
대조군이 아니다. **자원 쪽 변수**를 바꿔라.
- **좁아진 진단은 처방이 다르다.** 오진은 사람을 놀리는 게 아니라 **틀린 일을 시킨다** —
이 건에서는 GUI 로 항목을 추가하라고 시켰는데, 진짜 필요한 것은 잠금 해제였다.
- **여러 세션 이월된 "사람 손이 필요한 것"은 재측정 대상 1순위다.** 오래 남았다는 것은
검증이 어려웠다는 뜻이 아니라 **아무도 반증을 시도하지 않았다**는 뜻일 때가 많다.
GitHubで見る