Skip to content

Commit 95ea484

Browse files
committed
docs(devlog): fold the A-gate audit into the bug-bundle roadmap
Independent read-only review returned GO-WITH-FIXES with 4 blockers. Retires WP4 outright. Its premise was inverted: skipSystemPromptPrefix means Claude Code does NOT prepend its prefix, so the proposed dedup guard could never fire — while the unit tests would have passed by feeding the constant to itself. The compared strings differ anyway, and a maintainer had already ruled on issue lidge-jun#545, from an outbound capture, that removing the OAuth identity block is not a safe fix. Also folds back: - the ssh -g / devcontainer residual risk the research doc carried but the plan had dropped, now stated as accepted risk - the CODEOWNERS security-review boundary on src/server/auth-cors.ts, so WP2 is NEEDS_HUMAN-eligible rather than self-merging - OcxConfig imports from src/types, not src/config (tsc TS2459) - a tsconfig-independent error code cast - a range-diff precondition before skipping 1ba588e on PR lidge-jun#527 - server-auth.test.ts:582-602 named as WP2's must-not-break oracle
1 parent 641338b commit 95ea484

6 files changed

Lines changed: 194 additions & 8 deletions

File tree

‎devlog/_plan/260728_bug_bundle_resolution/000_plan.md‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,16 @@ PABCD 다중 사이클로 해결한다. 사용자가 커밋·푸시·머지를
5151
| --- | --- | --- | --- |
5252
| WP2 | `010_ssh_loopback_gate.md` | SSH 원격 프록시 — `auth-cors.ts` | 서버 인증 게이트 (최하부) |
5353
| WP3 | `020_tls_altname_diagnosis.md` | 이슈 #553 — `responses/core.ts` | 응답/오류 계층 |
54-
| WP4 | `030_claude_system_dedup.md` | 이슈 #545 — `adapters/anthropic.ts` | 어댑터 계층 |
54+
| ~~WP4~~ | `030_claude_system_dedup.md` | ~~이슈 #545~~ | **A 게이트에서 폐기** |
5555
| WP5 | `040_pr527_rebase.md` | PR #527 리베이스+리타깃 | PR 정리 |
5656
| WP6 | `050_pr557_boundary.md` | PR #557 머지 + #533 클로즈 | PR 정리 |
5757

58+
> **WP4 폐기 (2026-07-28 A 게이트, Critical).** 전제가 반전됐다 —
59+
> `skipSystemPromptPrefix`는 "이미 넣었다"가 아니라 "붙이지 않는다"는 뜻이라
60+
> 제안한 가드는 영원히 발화하지 않는다. 게다가 메인테이너가 이미 아웃바운드
61+
> 캡처를 근거로 "OAuth identity 제거는 안전한 수정이 아니다"라고 판정했다.
62+
> 근거 전문은 `030_claude_system_dedup.md` §폐기 근거. 이슈 #545는 열어둔다.
63+
5864
> goalplan의 wp2~wp6 번호와 decade doc 번호가 1:1 대응한다. 단 goalplan 초기
5965
> 등록 순서(#527 먼저)는 **의존 순서로 재배열**됐다 — 로드맵 락은 이 문서다
6066
> (LOOP-DOCS-FIRST-01: 초기 등록은 스켈레톤, 락은 docs-only D).
@@ -78,7 +84,7 @@ PABCD 다중 사이클로 해결한다. 사용자가 커밋·푸시·머지를
7884
| c1 | 이 유닛에 000 + 모든 decade doc이 diff-level로 존재하고 커밋됨 | `ls` + 커밋 해시 |
7985
| c5 | 포트가 다른 루프백 Host가 게이트를 통과하고 비루프백은 여전히 거부 | 신설 테스트 출력 |
8086
| c3 | `ERR_TLS_CERT_ALTNAME_INVALID`가 별도 메시지 + 복구 명령 | 분기 진입 assertion |
81-
| c4 | 인바운드 system이 Claude Code 정체성을 가질 때 prepend 안 함 / 없을 때 함 | 양쪽 케이스 |
87+
| ~~c4~~ | ~~Claude system 중복 가드~~ | **폐기 — WP4와 함께** |
8288
| c2 | PR #527이 base=dev, mergeable, enforce-target pass | `gh pr view` + `gh pr checks` |
8389
| c6 | PR #557 머지 + #533 클로즈, 또는 NEEDS_HUMAN 기록 | `gh pr view --json state` |
8490

@@ -95,3 +101,28 @@ PABCD 다중 사이클로 해결한다. 사용자가 커밋·푸시·머지를
95101
- `DONE` — 커밋 + 검증 증거 + 실제 상태 변화(PR 생성/머지, 이슈 클로즈)
96102
- `BLOCKED` — 업스트림/리포터 등 외부 의존
97103
- `NEEDS_HUMAN` — 보안 경계 판단 등 오너만 내릴 수 있는 결정
104+
105+
**WP2와 WP6은 둘 다 `NEEDS_HUMAN` 가능이다.** WP2는 `src/server/auth-cors.ts`가
106+
`.github/CODEOWNERS:13`의 인증 경계라 두 메인테이너 리뷰 대상이고, WP6은
107+
의존성 설치 경계다. 두 경우 모두 PR을 올리는 데까지가 우리 몫이고 머지는
108+
사람의 결정이다 — 회피가 아니라 정책 준수다.
109+
110+
## A 게이트 이력
111+
112+
2026-07-28, 독립 리뷰어 1회 (read-only, 코드베이스 실측).
113+
`VERDICT: GO-WITH-FIXES (blockers=4)`.
114+
115+
| # | 심각도 | 지적 | 처리 |
116+
| --- | --- | --- | --- |
117+
| 1 | Critical | WP4 분기가 도달 불가 + 문자열 불일치 + 메인테이너 기판정 | **WP4 폐기** |
118+
| 2 | High | `ssh -g`/devcontainer 잔여 위험이 009→계획으로 오면서 누락 | `010`에 명시적 수용 위험 절 추가 |
119+
| 3 | High | WP2가 CODEOWNERS 보안 리뷰 경계를 선언하지 않음 | `010` + 이 문서에 `NEEDS_HUMAN` 가능 명시 |
120+
| 4 | High | `OcxConfig` import 경로 오류 (`src/config`는 재export 안 함) | `../src/types`로 수정, `tsc` 실측 확인 |
121+
| 5 | Medium | `NodeJS.ErrnoException` 해석 미보장 | `(err as { code?: unknown }).code`로 교체 |
122+
| 7 | Medium | `1ba588eff` ≡ `9dd3c42da` 동등성이 미증명 | `040`에 `range-diff` 선행 증거 요구 추가 |
123+
| 7b | Medium | #557 "전 매트릭스 초록"이 과장 (8 SUCCESS + 1 null) | `050` 정정 |
124+
| 8 | Medium | WP2의 must-not-break 오라클이 무명 | `server-auth.test.ts:582-602` 명시 |
125+
126+
리뷰어가 확인해준 것: WP3의 세 호출 지점과 오류 도달 경로(Bun 실측), Bun이
127+
생성 `Request`의 `Host` 헤더를 보존한다는 것(WP2 테스트 형태 유효),
128+
`030`의 out-of-scope 판단(vision/web-search 경로)이 옳다는 것.

‎devlog/_plan/260728_bug_bundle_resolution/010_ssh_loopback_gate.md‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,55 @@ export function isLoopbackRequestHost(value: string | null): boolean {
6969
4. **형제 함수가 이미 그렇다.** Origin 쪽은 07-05부터 포트 무관이다. Host
7070
쪽만 조이는 건 방어 효과 없이 정상 구성만 깬다.
7171

72+
독립 리뷰가 위 네 근거를 전수 추적해 확인했다: `isLoopbackRequestHost`의 호출자는
73+
`auth-cors.ts:73` 하나뿐이고 `if (!isApiAuthRequired(config))` 안에서만 도달한다.
74+
비루프백 바인드는 `:76`으로 가서 `requireApiAuth`/`requireResponsesApiAuth`와
75+
`assertServerAuthConfig`로 별도 게이트를 받는다. `/v1/*`·관리 API 전 호출 지점
76+
(`src/server/index.ts` 322·336·373·443·475·494·518·558·573·595·618·646)이 모두
77+
`isAllowedRequestOrigin`을 거치므로 포트 검사가 유일한 방어인 경로는 없다.
78+
79+
### 받아들이는 잔여 위험 (명시)
80+
81+
**이 변경은 보안 중립이 아니다.** 루프백 모드는 인증이 아예 없다. 따라서:
82+
83+
```
84+
ssh -g -L 20100:localhost:10100 remote
85+
```
86+
87+
`-g` 옵션은 **클라이언트 쪽 `0.0.0.0`에** 리스너를 연다. 그 LAN의 누구나
88+
`Host: localhost:20100`으로 관리 API에 인증 없이 닿는다. 지금은 포트 불일치가
89+
**우연히** 이걸 막고 있고, 변경 후에는 막지 않는다.
90+
91+
같은 성질의 토폴로지: devcontainer 포트 포워딩, Codespaces 포워딩.
92+
93+
이걸 받아들이는 이유는 포트 검사가 **의도된 방어가 아니었기 때문**이다.
94+
`-g` 없는 평범한 `ssh -L`은 클라이언트 루프백에만 열리므로 노출이 없고,
95+
`-g`/devcontainer 토폴로지는 포트를 10100으로 맞추기만 하면 지금도 그대로
96+
뚫린다. 즉 현재 상태는 방어가 아니라 **일관성 없는 반쪽 차단**이다.
97+
98+
진짜 해법은 별개다 — 포워딩된 루프백에 인증을 요구할지 여부는 제품 결정이며
99+
이 work-phase의 스코프가 아니다. 여기서는 위험을 기록하고 넘어간다.
100+
101+
### 보안 리뷰 경계 (STRICT)
102+
103+
`.github/CODEOWNERS:13`:
104+
105+
```
106+
/src/server/auth-cors.ts @lidge-jun @Ingwannu
107+
```
108+
109+
"Authentication, credentials, and management API" 항목이다. `MAINTAINERS.md:29-32`:
110+
111+
> Authentication, credential handling, GitHub Actions, release automation,
112+
> dependency installation, and other security-boundary changes require explicit
113+
> security review.
114+
> Security-sensitive and release-related changes should be reviewed by both
115+
> maintainers when practical.
116+
117+
**따라서 이 work-phase는 자체 머지할 수 없다.** dev 대상 PR로 올리고
118+
CODEOWNERS 리뷰를 받는다. 터미널 판정은 `NEEDS_HUMAN` 가능이며, 그건 실패가
119+
아니라 경계를 지킨 정상 종료다 — `050`의 #557과 같은 성질이다.
120+
72121
## 변경 (diff-level)
73122

74123
### MODIFY `src/server/auth-cors.ts`
@@ -100,7 +149,7 @@ B 단계에서 확인한다. 없으면 미사용 export가 되므로 그대로
100149
```ts
101150
import { describe, expect, test } from "bun:test";
102151
import { isLoopbackRequestHost, isAllowedRequestOrigin } from "../src/server/auth-cors";
103-
import type { OcxConfig } from "../src/config";
152+
import type { OcxConfig } from "../src/types";
104153

105154
const loopbackConfig = { hostname: "127.0.0.1" } as OcxConfig;
106155

@@ -177,5 +226,13 @@ SSH 로컬 포워딩이 지원된다는 사실을 명시한다. 현재 이 절
177226

178227
- `bun run typecheck` 통과
179228
- `bun test tests/server-loopback-host-gate.test.ts` 전건 통과
180-
- `bun test tests/server-auth.test.ts` 회귀 없음 (기존 인증 경로 보존)
229+
- `bun test tests/server-auth.test.ts` 회귀 없음 — 특히
230+
**`tests/server-auth.test.ts:582-602`의 Host 헤더 rebinding 테스트**가 이
231+
work-phase의 must-not-break 오라클이다. 비루프백 이름(`attacker.test`)을 쓰므로
232+
통과해야 정상이며, 깨지면 방어가 무너진 것이다.
181233
- `bun run privacy:scan` 초록
234+
- PR은 dev 대상. CODEOWNERS 보안 리뷰 없이 머지하지 않는다.
235+
236+
> `import type { OcxConfig } from "../src/types"` — `src/config.ts`는
237+
> `OcxConfig`를 재export하지 않는다(리뷰 지적, `tsc` 실측 TS2459). 저장소의
238+
> 기존 테스트도 전부 `../src/types`를 쓴다.

‎devlog/_plan/260728_bug_bundle_resolution/020_tls_altname_diagnosis.md‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function describeUpstreamConnectFailure(err: unknown, connectMs: number):
5858
return `Provider connect timeout after ${connectMs}ms`;
5959
}
6060
const detail = err instanceof Error ? err.message : String(err);
61-
const code = err instanceof Error ? (err as NodeJS.ErrnoException).code : undefined;
61+
const code = err instanceof Error ? (err as { code?: unknown }).code : undefined;
6262
if (code === "ERR_TLS_CERT_ALTNAME_INVALID" || detail.includes("ERR_TLS_CERT_ALTNAME_INVALID")) {
6363
const host = extractHostname(detail);
6464
const target = host ?? "the provider host";
@@ -186,3 +186,25 @@ OUT: 이슈 #553 자체의 클로즈 판단 — 리포터 환경 확인이 남
186186
- `bun test tests/upstream-connect-error.test.ts` 4건 통과
187187
- `bun test tests/responses*.test.ts` 회귀 없음
188188
- `bun run privacy:scan` 초록
189+
> `(err as { code?: unknown }).code` — `NodeJS.ErrnoException`은 이 tsconfig
190+
> (`types: ["bun-types"]`, `@types/node` 직접 의존 없음)에서 해석이 보장되지
191+
> 않는다. 저장소 관용구는 `src/lib/upstream-retry.ts:213`이다.
192+
193+
## 독립 리뷰 확인 사항 (A 게이트)
194+
195+
리뷰어가 이 절을 실측으로 검증했다:
196+
197+
- 세 호출 지점(1197·1746·1788)이 정확하다.
198+
- 오류가 실제로 거기까지 **도달한다.** Bun 실측:
199+
`fetch("https://wrong.host.badssl.com/")` → `name=Error`,
200+
`code=ERR_TLS_CERT_ALTNAME_INVALID`. 중간에서 삼켜지지 않는다 —
201+
`fetchWithHeaderTimeout`은 시그널만 감싸고 rethrow,
202+
`isConnectionResetError`는 `ECONNRESET`/`EPIPE`만 매칭,
203+
`fetchWithTransientRetry`는 반환된 `Response` 상태만 검사,
204+
`providerFetch`는 통과다.
205+
- 제안된 헬퍼를 실제 Bun 오류와 #553 원문 문자열 양쪽에 돌려 두 경우 모두
206+
TLS 분기를 타고 호스트명 추출도 정확함을 확인했다.
207+
208+
검증한 핸들러: #553 리포터의 URL은 `/v1/responses`이고, 세 호출 지점이 그
209+
경로를 덮는다. `/v1/chat/completions`는 `src/server/index.ts:588`에서
210+
`handleChatCompletions`로 분기하므로 별도 경로이며 이 변경 범위 밖이다.

‎devlog/_plan/260728_bug_bundle_resolution/030_claude_system_dedup.md‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,62 @@
1-
# 030 — WP4: Claude Code system 블록 중복 삽입 가드 (이슈 #545)
1+
# 030 — WP4: **폐기** — Claude Code system 중복 가드 (이슈 #545)
2+
3+
> **A 게이트에서 폐기됨 (2026-07-28, Critical 블로커 1).**
4+
> 이 문서의 초안은 잘못된 전제 위에 있었다. 아래 §폐기 근거를 먼저 읽을 것.
5+
> WP4는 실행하지 않는다. 로드맵에서 제외되고 goalplan wp4는 폐기 처리한다.
6+
7+
## 폐기 근거
8+
9+
초안의 전제: "`skipSystemPromptPrefix`는 클라이언트가 **이미 정체성 문구를
10+
넣었으니 더 붙이지 말라**는 신호다."
11+
12+
**뒤집혔다.** 이 플래그는 Claude Code가 CLI 공용 system 프리픽스를 **붙이지
13+
않는다**는 뜻이다. 즉 분류기 요청은 정체성 문구를 **갖고 오지 않는다.**
14+
`hasClaudeCodeIdentityPrefix(system)`은 영원히 false를 반환하고 새 분기는 한
15+
번도 발화하지 않는다 — 그런데 제안된 단위 테스트는 상수를 자기 자신에게
16+
먹이므로 **통과한다.** C-ACTIVATION-GROUNDING-01이 정확히 막으려는 위양성이다.
17+
18+
독립적인 두 번째 반증: 문자열이 애초에 다르다.
19+
20+
| | 값 |
21+
| --- | --- |
22+
| 우리 상수 (`src/oauth/anthropic.ts:15`) | `You are a Claude agent, built on Anthropic's Claude Agent SDK.` |
23+
| Claude Code 실제 프리픽스 | `You are Claude Code, Anthropic's official CLI for Claude.` |
24+
25+
초안의 위험 절이 "우리 상수와 클라이언트 문구가 완전히 같아야" 판정이 맞는다고
26+
썼는데, 바로 그 조건이 성립하지 않는다.
27+
28+
## 결정적 근거 — 메인테이너가 이미 판정했다
29+
30+
이슈 #545에서 리포터가 제안한 것이 정확히 이 수정이다("B (완화): inbound
31+
system에 이미 Claude Code identity가 있으면 중복 prepend 생략"). 메인테이너
32+
(`Ingwannu`)가 **아웃바운드 캡처를 받은 뒤** 답한 내용:
33+
34+
> The capture confirms that OpenCodex preserves both caller controls all the way
35+
> to Anthropic: `max_tokens: 64` and `stop_sequences: ["</block>"]` are present on
36+
> every outbound request. (…) The 66-character system difference is also accounted
37+
> for by **the required Claude OAuth identity block** plus block joining; it is not
38+
> an unexplained truncation or dropped field.
39+
40+
> We cannot safely fix that by raising the caller's explicit budget, **removing the
41+
> OAuth identity instruction**, or pretending an incomplete response completed.
42+
43+
즉 정체성 블록은 **Claude OAuth가 요구하는 필수 요소**로 확인됐고, 그걸 빼는
44+
방향은 이미 안전하지 않다고 판정됐다. 우리 계획은 판정된 방향을 다시 하려던
45+
것이었다.
46+
47+
## 이슈 #545의 실제 잔여 문제
48+
49+
메인테이너 정리: Claude Desktop 3P의 Auto Mode 분류기가 클라이언트가 명시한
50+
64토큰 예산 안에 닫는 태그를 못 내는 경우가 있고, Desktop이 같은 요청을 5회
51+
반복한다. 이건 **호환성 문제**지 우리 번역 손실이 아니다. 라벨도
52+
`provider-compatibility`로 좁혀져 있다.
53+
54+
우리가 안전하게 할 수 있는 일이 남아 있는지는 별도 조사 대상이며, 이번 유닛의
55+
스코프가 아니다. 이슈는 열린 채로 둔다.
56+
57+
---
58+
59+
## (이하 폐기된 초안 — 이력 보존용)
260

361
대상: `src/adapters/anthropic.ts`
462
이슈: #545 `Claude Desktop 3P Auto Mode classifier retries after 64-token Anthropic OAuth outputs`

‎devlog/_plan/260728_bug_bundle_resolution/040_pr527_rebase.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ git cherry-pick a64aa5856
5959

6060
`1ba588eff`는 **의도적으로 건너뛴다** — dev의 `9dd3c42da`가 같은 일을 한다.
6161

62+
> **선행 증거 필수 (A 게이트 지적).** "같은 일을 한다"는 아직 제목 대조일
63+
> 뿐이다. 건너뛰기 전에 실제 동등성을 증명한다:
64+
>
65+
> ```bash
66+
> git range-diff 9dd3c42da~1..9dd3c42da 1ba588eff~1..1ba588eff
67+
> # 또는 파일별
68+
> git diff 1ba588eff~1 1ba588eff -- src/codex/ | diffstat
69+
> git diff 9dd3c42da~1 9dd3c42da -- src/codex/ | diffstat
70+
> ```
71+
>
72+
> 동등하지 않으면 `a64aa5856`가 미묘하게 다른 베이스 위에 얹히고 누락된 델타가
73+
> **조용히 사라진다.** 차이가 있으면 건너뛰지 말고 충돌로 해소한다.
74+
6275
### 3. 충돌 해소 규칙
6376
6477
두 테스트 파일에서 충돌이 나면:

‎devlog/_plan/260728_bug_bundle_resolution/050_pr557_boundary.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
| mergeable | **MERGEABLE / CLEAN** | UNKNOWN |
1414
| head | `b0434ea58` | — |
1515
| 리뷰 | 미해결 스레드 0 | CHANGES_REQUESTED |
16-
| CI | 전 매트릭스 초록 | Windows 2건 실패 이력 |
16+
| CI | 8 SUCCESS + 1 null(pending/skipped) | Windows 2건 실패 이력 |
17+
| reviewDecision | 공란 (승인 0) | CHANGES_REQUESTED |
1718
| 파일 | 23개 | 23개 (동일) |
1819

1920
#557은 #533의 **메인테이너 인수본**이다. 조상 관계가 아니라 더 새 dev 위에
@@ -26,9 +27,13 @@
2627

2728
## 왜 이건 코드 작업이 아닌가
2829

29-
미해결 리뷰 0건, 전 매트릭스 초록. **기술적으로 남은 게 없다.** 남은 것은
30+
미해결 리뷰 스레드 0건, 실패 체크 0건. **기술적으로 남은 게 없다.** 남은 것은
3031
오너만 내릴 수 있는 판단 하나다.
3132

33+
단, "전 매트릭스 초록"은 과장이었다(A 게이트 지적). 실제로는 8건 SUCCESS에
34+
1건이 null — pending이거나 skip이다. c6 증거를 캡처할 때 그 null 체크가
35+
무엇인지 명시한다. 승인은 0건이다.
36+
3237
## 보안 경계 사실관계
3338

3439
diff가 소유하는 것:

0 commit comments

Comments
 (0)