- name
- a-cached-config-layer-keeps-the-first-credential-it-saw
- description
- 런마다 다른 크리덴셜을 긴 수명 프로세스의 **설정 층**으로 주입하면, 그 층은 런보다 **거친 키**(디렉터리·프로젝트·워크스페이스)로 캐시돼서 **처음 본 토큰을 계속 쓴다** — 그런데 기능은 캐시로 멀쩡히 돌아가므로 표면에서는 절대 안 보인다. 유일한 센서는 **받는 쪽**이다: 토큰을 바꾼 뒤 서버에 요청이 **새로 오는가**를 봐라. 트리거 - per-run 토큰을 설정 파일/설정 API 로 주입, 긴 수명 데몬(serve·LSP·language server·브라우저 프로파일)에 자격증명 넘기기, 공유 워크스페이스 디렉터리, "설정 층에 넣으면 된다"는 인수인계, 멀티테넌트 워커.
# 캐시된 설정 층은 **처음 본 크리덴셜**을 계속 쓴다
## Problem
런마다 다른 토큰을 줘야 하는데, 툴이 크리덴셜을 **설정으로만** 받는다. 자연스러운 설계:
> "데몬은 세션을 만들 때 `?directory=<per-task 워크스페이스>` 를 받는다.
> 그 디렉터리에 `config.json` 을 쓰면 런마다 다른 토큰을 줄 수 있다."
읽는 것까지는 맞다 — 실측했고, 원격 서버에 `Authorization` 헤더까지 정확히 도달했다.
**두 번째 런에서 무너진다.**
- **증상**: 없다. 기능은 완벽히 동작한다. 툴 목록도 그대로 뜨고, 에러도 경고도 없다
- **근본 원인**: 데몬이 그 설정을 **디렉터리 단위로, 프로세스 수명 내내 캐시**한다.
같은 디렉터리의 다음 런은 **첫 런의 토큰**으로 붙는다. (실측: `disconnect` + `connect`
로 강제 재연결해도 **여전히 첫 토큰**이었다 — 캐시된 건 연결이 아니라 **설정 스냅샷**)
- **흔한 오해**: *"설정 파일을 새로 썼으니 새 값이 읽힌다"*. 파일은 새것이다.
**읽는 쪽이 다시 읽지 않는다.**
### 왜 이게 보안 사고인가
per-run 토큰은 그 런에만 권한이 있다 — 그게 존재 이유다. 캐시가 살아 있으면
**런 B 가 런 A 의 토큰으로 서버에 쓴다.** 워커의 샌드박스 디렉터리가 모든 태스크·
모든 테넌트가 공유하는 한 경로라면(흔하다 — 경로 유일성은 "아무도 안 쓴다"는 이유로
자주 제거된다, [[collapsing-per-item-dirs-merges-state-keyed-on-the-path]]),
그 상속은 **테넌트 경계를 넘는다.**
### 왜 초록으로 지나가는가
* 유닛 테스트는 **한 런**만 돌린다. 캐시 버그는 **두 번째 런**에만 있다
* 통합 테스트도 대개 프로세스를 새로 띄운다 — 캐시는 **프로세스 수명** 스코프라 사라진다
* 기능 확인("툴이 뜨는가")은 **캐시가 답해 준다.** 초록이 곧 "새 토큰이 쓰였다"가 아니다
## Solution
1. **받는 쪽을 계측해라.** 보내는 쪽의 성공은 증거가 아니다. 토큰을 바꾼 다음,
**서버에 요청이 새로 왔는가**를 세라. 0건이면 캐시가 답한 것이다
```python
# 프로브 서버: 받은 Authorization 을 줄마다 적는다
_note("post", method=req.get("method"), authorization=self.headers.get("Authorization"))
```
```
1회차: ['Bearer FIRST-TOKEN'] ← 정상
토큰 바꾸고 2회차: [] ← 🚨 요청이 아예 안 왔다. 기능은 멀쩡했다
```
2. **설정 파일 말고 런타임 등록 API 를 찾아라.** 대개 있다(`POST /mcp`, `/connect`,
`session.configure`). 실측 기준 셋을 다 만족하는지 확인:
**(a) 캐시를 덮는가 (b) 토큰이 디스크에 안 남는가 (c) 스코프가 맞는가**
3. **키가 공유되면 이름을 런마다 유일하게.** 캐시 키(디렉터리)를 내가 못 바꾼다면,
**그 안에서의 이름**(서버 이름·프로파일 이름)을 런 id 로 만들어라. 동시 실행이
서로를 덮는 것까지 같이 막힌다
4. **런이 끝나면 등록을 내려라.** 안 내리면 다 쓴 크리덴셜을 든 연결이
태스크마다 하나씩 쌓인다
5. **테스트는 "두 번째 런"을 돌려라.** 같은 키로 연속 두 번 실행해서
**두 번째가 자기 토큰을 쓰는지** 단언한다. 한 번만 도는 테스트는 이 결함에 대해
구조적으로 아무 말도 못 한다
## Key Insights
- **"동작한다"와 "내 값이 쓰였다"는 다른 명제다.** 캐시는 둘을 갈라놓는 장치다 —
기능은 캐시가 답해 주고, 내 값은 아무 데도 안 간다. [[a-failed-read-degraded-to-empty-becomes-a-measurement]]
의 사촌: 저기선 빈 값이 주장이 되고, 여기선 **낡은 값이 주장이 된다**
- **크리덴셜 주입을 검증하는 유일한 센서는 받는 쪽이다.** 보내는 쪽 로그·설정 파일·
기능 확인은 셋 다 캐시를 통과한다. 프로브 서버 30줄이 전부 가른다
- **캐시 키가 런보다 거칠면, 크리덴셜은 런보다 오래 산다.** 설계 리뷰에서 물을 것 하나:
*"이 설정의 캐시 키가 무엇이고, 그 키를 몇 개의 런이 공유하나?"*
- **재연결이 재-읽기는 아니다.** `disconnect`+`connect` 가 새 값을 집어올 거라는 건
추측이다 — 실측하니 **스냅샷은 그대로**였다. 캐시 무효화 경로는 문서에 없고,
덮어쓰는 API 만 있었다
- 인수인계가 *"X 층으로 하면 된다"* 고 적어 놓았을 때, 그 층이 **읽히는지**만 확인하고
넘어가지 마라. 읽힌다는 건 **첫 번째 읽기**에 대한 사실이다.
관련: [[a-rejection-records-a-verdict-but-the-reason-is-what-expires]] ·
[[an-isolation-flag-covers-a-mechanism-not-its-goal]]
在 GitHub 查看