@milcho0604/velog-mcp 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ko.md +366 -0
- package/README.md +381 -0
- package/dist/auth.d.ts +57 -0
- package/dist/auth.js +124 -0
- package/dist/auth.js.map +1 -0
- package/dist/capabilities.d.ts +50 -0
- package/dist/capabilities.js +60 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/client.d.ts +113 -0
- package/dist/client.js +322 -0
- package/dist/client.js.map +1 -0
- package/dist/format.d.ts +31 -0
- package/dist/format.js +66 -0
- package/dist/format.js.map +1 -0
- package/dist/graphql.d.ts +29 -0
- package/dist/graphql.js +82 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/me.d.ts +23 -0
- package/dist/me.js +35 -0
- package/dist/me.js.map +1 -0
- package/dist/ownership.d.ts +42 -0
- package/dist/ownership.js +62 -0
- package/dist/ownership.js.map +1 -0
- package/dist/plugin-env.d.ts +67 -0
- package/dist/plugin-env.js +102 -0
- package/dist/plugin-env.js.map +1 -0
- package/dist/ratelimit.d.ts +48 -0
- package/dist/ratelimit.js +79 -0
- package/dist/ratelimit.js.map +1 -0
- package/dist/render/chrome.d.ts +77 -0
- package/dist/render/chrome.js +287 -0
- package/dist/render/chrome.js.map +1 -0
- package/dist/render/cover.d.ts +29 -0
- package/dist/render/cover.js +195 -0
- package/dist/render/cover.js.map +1 -0
- package/dist/render/icons.d.ts +22 -0
- package/dist/render/icons.js +158 -0
- package/dist/render/icons.js.map +1 -0
- package/dist/render/index.d.ts +32 -0
- package/dist/render/index.js +137 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/page.d.ts +89 -0
- package/dist/render/page.js +761 -0
- package/dist/render/page.js.map +1 -0
- package/dist/render/tones.d.ts +30 -0
- package/dist/render/tones.js +46 -0
- package/dist/render/tones.js.map +1 -0
- package/dist/slug.d.ts +42 -0
- package/dist/slug.js +79 -0
- package/dist/slug.js.map +1 -0
- package/dist/tools/discover.d.ts +6 -0
- package/dist/tools/discover.js +106 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/drafts.d.ts +23 -0
- package/dist/tools/drafts.js +227 -0
- package/dist/tools/drafts.js.map +1 -0
- package/dist/tools/export.d.ts +21 -0
- package/dist/tools/export.js +132 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/images.d.ts +34 -0
- package/dist/tools/images.js +556 -0
- package/dist/tools/images.js.map +1 -0
- package/dist/tools/posts.d.ts +14 -0
- package/dist/tools/posts.js +82 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/profile-edit.d.ts +15 -0
- package/dist/tools/profile-edit.js +216 -0
- package/dist/tools/profile-edit.js.map +1 -0
- package/dist/tools/profile.d.ts +9 -0
- package/dist/tools/profile.js +133 -0
- package/dist/tools/profile.js.map +1 -0
- package/dist/tools/publish.d.ts +16 -0
- package/dist/tools/publish.js +424 -0
- package/dist/tools/publish.js.map +1 -0
- package/dist/tools/stats.d.ts +32 -0
- package/dist/tools/stats.js +154 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/docs/PRD.md +146 -0
- package/docs/api-reference.md +329 -0
- package/docs/architecture.md +112 -0
- package/docs/decisions/0001-why-build-our-own.md +89 -0
- package/docs/decisions/0002-draft-only-write.md +84 -0
- package/docs/decisions/0003-token-env-only.md +109 -0
- package/docs/decisions/0004-capability-model.md +123 -0
- package/docs/decisions/0005-render-in-server.md +117 -0
- package/docs/decisions/0006-ship-as-plugin.md +532 -0
- package/docs/security.md +384 -0
- package/docs/tools.md +404 -0
- package/npm-shrinkwrap.json +2345 -0
- package/package.json +61 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# ADR 0006 — Claude Code 플러그인으로 배포한다
|
|
2
|
+
|
|
3
|
+
- 날짜: 2026-07-31
|
|
4
|
+
- 상태: 채택
|
|
5
|
+
|
|
6
|
+
## 맥락
|
|
7
|
+
|
|
8
|
+
ADR 0003 에서 토큰을 **환경변수로만** 받기로 했다. 디스크에 안 쓰고, 키체인도 안 본다.
|
|
9
|
+
그 결정은 지금도 유효하다 — 서버는 여전히 토큰을 파일로 쓰지 않는다.
|
|
10
|
+
|
|
11
|
+
문제는 **그 환경변수를 누가 어디에 적어두느냐**였다. 실제로는 이렇게 됐다:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
~/.claude.json → mcpServers.velog.env.VELOG_REFRESH_TOKEN = <30일짜리 계정 전권 토큰, 평문>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
파일 권한이 `-rw-------` 이고 git 저장소 밖이라 당장 새는 건 아니다. 그래도
|
|
18
|
+
30일짜리 계정 전권 자격증명이 홈 디렉터리 JSON 에 평문으로 있는 상태다.
|
|
19
|
+
|
|
20
|
+
Claude Code 플러그인에는 `userConfig` 가 있고, `sensitive: true` 로 선언한 값은
|
|
21
|
+
**macOS 키체인**에 저장된다(키체인이 없는 플랫폼은 `~/.claude/.credentials.json`).
|
|
22
|
+
그 값은 `${user_config.KEY}` 로 `.mcp.json` 에 치환돼 서버에 env 로 들어온다.
|
|
23
|
+
서버 입장에서는 여전히 환경변수 하나다 — ADR 0003 을 바꾸지 않고 저장 위치만 옮긴다.
|
|
24
|
+
|
|
25
|
+
## 결정
|
|
26
|
+
|
|
27
|
+
저장소 하나가 마켓플레이스이자 플러그인 배포처가 된다.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
velog-mcp/
|
|
31
|
+
├── .claude-plugin/marketplace.json ← 마켓플레이스 `milcho`
|
|
32
|
+
└── plugins/velog/ ← 플러그인 `velog`
|
|
33
|
+
├── .claude-plugin/plugin.json ← userConfig 4개
|
|
34
|
+
├── .mcp.json ← npx 로 npm 패키지 실행
|
|
35
|
+
└── README.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
이름은 공식 디렉터리의 서드파티 15개 관례를 따랐다 — `asana` · `github` · `linear`
|
|
39
|
+
처럼 **서비스 맨이름**이다. `velog-mcp` 같은 이름은 사용자가 설치하는 것이
|
|
40
|
+
'MCP' 가 아니라 '플러그인'이라 군더더기다.
|
|
41
|
+
|
|
42
|
+
## 실측이 내 전제를 뒤집었다
|
|
43
|
+
|
|
44
|
+
설계할 때 이렇게 봤다 — **"치환이 실패하면 `${user_config.x}` 가 글자 그대로 남는다."**
|
|
45
|
+
근거는 Claude Code 2.1.220 실행 파일 안의 이 검사였다:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
p = l.includes("${user_config.") ? "user_config_missing" : "url_invalid"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
리터럴이 살아남는다는 전제 위에서만 성립하는 검사다. 그래서 그걸 막는 코드를 먼저 썼다.
|
|
52
|
+
|
|
53
|
+
그리고 **탐침 플러그인을 만들어 실제로 쟀다.** userConfig 를 아무것도 안 채우고
|
|
54
|
+
`claude --plugin-dir` 로 띄운 뒤, MCP 서버가 받는 env 를 그대로 파일에 적게 했다.
|
|
55
|
+
|
|
56
|
+
| `.mcp.json` 의 env 값 | 서버가 실제로 받는 것 |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `${user_config.문자열}` (값 없음) | `""` — 키는 존재한다 |
|
|
59
|
+
| `${user_config.불린}` (`default: false`) | `"false"` |
|
|
60
|
+
| `${user_config.불린}` (default 없음, 값 없음) | `""` |
|
|
61
|
+
| `prefix-${user_config.x}-suffix` | `prefix--suffix` |
|
|
62
|
+
| **선언하지 않은 키를 참조** | **서버가 기동하지 않는다** |
|
|
63
|
+
|
|
64
|
+
전제가 틀렸다. 위의 `includes` 검사는 **URL 형태 서버 경로에만** 있고,
|
|
65
|
+
stdio 서버의 env 는 빈 문자열로 치환된다.
|
|
66
|
+
|
|
67
|
+
마지막 줄이 제일 아프다. `.mcp.json` 이 `plugin.json` 에 없는 키를 참조하면
|
|
68
|
+
Claude Code 가 그 MCP 서버를 **아무 말 없이 안 띄운다**. 대조군으로 확인했다 —
|
|
69
|
+
같은 매니페스트에서 참조를 선언된 키로만 바꾸자 서버가 정상 기동했다.
|
|
70
|
+
증상은 "플러그인은 깔렸는데 도구가 하나도 없음"이다. 오타 하나로 그렇게 된다.
|
|
71
|
+
|
|
72
|
+
## 그래서 코드가 하는 일이 바뀌었다
|
|
73
|
+
|
|
74
|
+
`src/plugin-env.ts` 는 이제 **"빈 값을 없음으로 굳히는 것"**이 본업이다.
|
|
75
|
+
|
|
76
|
+
지금도 소비 지점 셋은 빈 문자열을 알아서 걸러낸다 — `auth.ts` 의 `|| undefined`,
|
|
77
|
+
`chrome.ts` 의 `if (override)`, `capabilities.ts` 의 TRUTHY 집합. 셋 다 맞다.
|
|
78
|
+
그런데 **셋 다 따로** 맞는다. 그중 하나만 나중에 `?? undefined` 로 바뀌어도
|
|
79
|
+
빈 문자열이 토큰 행세를 하고, 그때 증상은 "토큰이 만료됐나?" 로 나타난다.
|
|
80
|
+
한 곳에서 지워 두면 그 회귀가 성립하지 않는다.
|
|
81
|
+
|
|
82
|
+
자리표시자 검사는 남겼다. 관찰한 건 **버전 하나의 동작**이지 보장된 계약이 아니고,
|
|
83
|
+
URL 서버 경로에는 그 사고가 실재한다. 비용은 정규식 하나다. 다만 주석에
|
|
84
|
+
"관찰된 적 없음"이라고 못 박아 뒀다 — 근거 없는 방어가 근거 있는 척하면 안 된다.
|
|
85
|
+
|
|
86
|
+
## 시끄럽지 않기로 한 것
|
|
87
|
+
|
|
88
|
+
플러그인은 설정 네 개를 **항상** 넘긴다. 토큰만 넣은 사용자에게 나머지 셋이
|
|
89
|
+
빈 값으로 온다. 그걸 매번 경고하면 경고를 읽지 않게 된다.
|
|
90
|
+
|
|
91
|
+
그래서 기동 로그가 말하는 건 두 경우뿐이다:
|
|
92
|
+
|
|
93
|
+
- **토큰이 비어서 왔다** → 읽기 전용이 된 이유를 짚는다. 나머지 빈 값은 언급하지 않는다.
|
|
94
|
+
- **자리표시자가 살아 왔다** → 관찰된 적 없는 일이다. 크게 알린다.
|
|
95
|
+
|
|
96
|
+
그리고 크롬 가용 여부는 **매번** 말한다. 크롬이 없으면 그림을 그리는 두 도구
|
|
97
|
+
(`velog_render_diagram`·`velog_render_cover`)가 안 되는데, 그걸 처음 알게 되는 시점이
|
|
98
|
+
대개 "글 쓰다가 그림이 필요해진 순간"이다. 설치 직후에 알면 그때 해결할 수 있다.
|
|
99
|
+
(⚠️ 이 문서도 한때 "3종"이라고 적고 있었다 — 아래 정정 절 참고.)
|
|
100
|
+
|
|
101
|
+
## 배포 게이트 — `version` 이 핵심이다
|
|
102
|
+
|
|
103
|
+
`plugin.json` 에 `version` 을 **반드시** 적는다. 안 적으면 커밋 SHA 가 버전이 되어
|
|
104
|
+
**푸시하는 족족 사용자에게 흘러간다.** 적어두면 그 값을 올릴 때만 나간다.
|
|
105
|
+
|
|
106
|
+
PR 로 작업하는 것과는 다른 축이다. PR 은 master 를 깨끗하게 유지하고,
|
|
107
|
+
`version` 은 사용자에게 나가는 순간을 정한다. 둘 다 필요하다.
|
|
108
|
+
|
|
109
|
+
버전이 네 곳에 있다 — `package.json` · `plugin.json` · `.mcp.json` 의 npx 핀 ·
|
|
110
|
+
`SERVER_VERSION`. 손으로 맞추지 않는다. 테스트 P8 이 넷을 대조한다.
|
|
111
|
+
|
|
112
|
+
## 안 고른 것
|
|
113
|
+
|
|
114
|
+
**`.mcp.json` 에 `dist/` 를 동봉하고 `node ${CLAUDE_PLUGIN_ROOT}/dist/index.js` 로 실행**
|
|
115
|
+
— `dist/` 를 git 에 넣어야 한다. 소스 한 줄 고칠 때마다 빌드 산출물 커밋이 따라붙어
|
|
116
|
+
저장소가 지저분해진다.
|
|
117
|
+
|
|
118
|
+
**marketplace 의 `npm` 소스 타입** — 플러그인 자체가 npm 패키지가 되어 오프라인에도
|
|
119
|
+
강하고 아티팩트가 하나로 준다. 매력적인데, 그때 **런타임 의존성 두 개
|
|
120
|
+
(`@modelcontextprotocol/sdk`, `zod`)가 설치되는지 확인하지 못했다.** 문서에 명시가
|
|
121
|
+
없다. 확인되면 옮길 만하다.
|
|
122
|
+
|
|
123
|
+
지금은 **`npx -y @milcho0604/velog-mcp@<정확한 버전>`** 이다. 의존성 설치가 확실하고,
|
|
124
|
+
정확한 버전을 핀하므로 `npx` 가 캐시에서 해결한다. 오프라인 첫 설치는 안 된다 —
|
|
125
|
+
이건 문서에 적어 사용자가 알게 한다.
|
|
126
|
+
|
|
127
|
+
`velog-mcp` 라는 이름은 npm 에 이미 있다(`stoneHee99`, 0.2.2). 그래서 스코프
|
|
128
|
+
패키지 `@milcho0604/velog-mcp` 를 쓴다.
|
|
129
|
+
|
|
130
|
+
## 코덱스가 뒤집은 것 — `npx` 는 토큰 경계를 넓힌다
|
|
131
|
+
|
|
132
|
+
교차검증에서 이 지적이 나왔고, 재보니 맞았다.
|
|
133
|
+
|
|
134
|
+
`.mcp.json` 의 `env` 는 최종 서버에만 가는 게 아니다. **먼저 실행되는 `npx`/`npm`
|
|
135
|
+
프로세스에도 그대로 들어간다.** 그러니 "A6 가 토큰이 velog.io 밖으로 못 나가게
|
|
136
|
+
강제한다"는 설명은 **이 실행 방식 전체에 대해서는 성립하지 않는다.** A6 는 이
|
|
137
|
+
저장소의 코드만 본다.
|
|
138
|
+
|
|
139
|
+
세 가지를 했다:
|
|
140
|
+
|
|
141
|
+
1. **문구를 정확히 고쳤다.** 매니페스트 설명과 README 에서 "어디로도 안 나간다"를
|
|
142
|
+
"이 서버가 싣는 목적지는 velog.io 뿐"으로 좁히고, npx 경계를 명시했다.
|
|
143
|
+
2. **실측했다.** 운영 의존성 트리에 `install`·`postinstall` 스크립트가 하나도 없다.
|
|
144
|
+
(`prepare` 는 여럿 있지만 게시된 tarball 설치에서는 돌지 않는다.)
|
|
145
|
+
3. **막았다.** `.mcp.json` 에 `npm_config_ignore_scripts=true` 를 넣었다.
|
|
146
|
+
이 환경변수가 실제로 `ignore-scripts` 를 켜는 것은 실행해서 확인했다.
|
|
147
|
+
전이 의존성이 나중에 스크립트를 추가해도 돌지 않는다. (테스트 P17)
|
|
148
|
+
|
|
149
|
+
완전히 없애려면 npm 을 실행 경로에서 빼야 한다 — 의존성 번들이 필요하고,
|
|
150
|
+
그건 런타임 의존성 2개를 유지하는 이 저장소의 방식과 맞바꿔야 한다. 지금은 안 했다.
|
|
151
|
+
|
|
152
|
+
## 코덱스가 뒤집은 것 — `access(X_OK)` 는 디렉터리도 통과한다
|
|
153
|
+
|
|
154
|
+
크롬 경로 설정이 `file` 타입이라 macOS 파일 선택기가 `.app` **번들 자체**를 돌려줄 수
|
|
155
|
+
있다. 번들은 디렉터리이고, POSIX 에서 디렉터리의 `X_OK` 는 '실행 가능'이 아니라
|
|
156
|
+
'탐색 가능'이다. 실측:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
X_OK 통과 | isFile=false | /Applications/Google Chrome.app
|
|
160
|
+
X_OK 통과 | isFile=true | /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
증상이 고약하다 — 기동 로그는 "그림 도구 사용 가능"이라고 말해놓고, 정작 그리려
|
|
164
|
+
할 때 `spawn` 이 실패한다. **말과 실제가 어긋나는 종류의 버그**다.
|
|
165
|
+
|
|
166
|
+
`stat().isFile()` 을 함께 본다. 그리고 번들을 골랐으면 안쪽 실행 파일로 바꿔준다 —
|
|
167
|
+
번들을 고르는 건 사용자 실수가 아니라 파일 선택기의 자연스러운 결과다.
|
|
168
|
+
|
|
169
|
+
## 테스트가 약했던 것 — 코덱스의 반례 둘
|
|
170
|
+
|
|
171
|
+
**게이트 두 개를 서로 바꿔도 통과했다.** `allow_public` ↔ `allow_profile` 을 맞바꾸면
|
|
172
|
+
키 집합은 그대로다. 그런데 사용자는 프로필 수정만 켰는데 공개 발행이 열린다.
|
|
173
|
+
토큰과 크롬 경로를 바꿔도 통과했고, 그 경우 토큰이 크롬 오류 메시지에 실려
|
|
174
|
+
stderr 로 나갈 수 있다. → **매핑 전체를 그대로 못 박았다**(P6). 명령·인자도(P16).
|
|
175
|
+
|
|
176
|
+
**`server.connect()` 를 통째로 지워도 통과했다.** 기동 로그가 먼저 나오기 때문이다.
|
|
177
|
+
즉 "말은 하는데 서버가 아닌" 상태를 못 잡았다. → **실제 JSON-RPC `initialize` 와
|
|
178
|
+
`tools/list` 를 던져 응답을 받는다**(P18). 서버 이름·버전·도구 수까지 확인한다.
|
|
179
|
+
|
|
180
|
+
## 3차에서 나온 것 — 정확한 버전을 핀해도 의존성은 안 고정된다
|
|
181
|
+
|
|
182
|
+
`.mcp.json` 이 `@milcho0604/velog-mcp@0.4.0` 을 정확히 핀한다. 그런데 **그 안의
|
|
183
|
+
의존성은 `^1.30.0`·`^4.4.3` 캐럿 범위**다. 나중에 콜드 설치하면 같은 0.4.0 이어도
|
|
184
|
+
더 새 의존성을 받고, 그 코드는 **토큰이 든 같은 프로세스에서** 돈다.
|
|
185
|
+
`npm_config_ignore_scripts` 는 설치 스크립트만 막지 런타임 코드는 못 막는다.
|
|
186
|
+
|
|
187
|
+
`npm-shrinkwrap.json` 을 쓴다. `package-lock.json` 과 달리 **발행물에 실려**
|
|
188
|
+
트리 전체를 고정한다(운영 93개). 대가는 명확하다 — 의존성 보안 패치가 자동으로
|
|
189
|
+
안 온다. 릴리스를 우리가 쥐는 대신 갱신 책임도 우리가 진다. 30일짜리 계정 전권
|
|
190
|
+
자격증명을 들고 도는 CLI 라서 이쪽을 택했다.
|
|
191
|
+
|
|
192
|
+
⚠️ 함정 하나: `files` 화이트리스트가 있으면 shrinkwrap 도 **안 실린다**(실측).
|
|
193
|
+
`files` 에 명시해야 한다. 넣기 전/후로 tarball 을 실제로 풀어 확인했다(96→97개).
|
|
194
|
+
|
|
195
|
+
## 3차에서 나온 것 — `dist/` 는 어떤 검사도 안 받고 있었다
|
|
196
|
+
|
|
197
|
+
테스트는 `src/index.ts` 를 띄운다. 사용자가 실행하는 건 `dist/index.js` 다.
|
|
198
|
+
그런데 `dist/` 는 `.gitignore` 대상이라 lint 도 테스트도 닿지 않는다.
|
|
199
|
+
즉 **dist 에서만 깨진 상태는 테스트가 전부 통과해도 안 잡힌다.**
|
|
200
|
+
|
|
201
|
+
`prepublishOnly` 에 관문을 걸었다 — `npm publish` 가 반드시 거치고 사람이 건너뛸 수
|
|
202
|
+
없다. 실제 `dist/index.js` 를 띄워 MCP 핸드셰이크를 하고, 버전이 `package.json` 과
|
|
203
|
+
같은지(낡은 산출물 탐지), 도구가 다 뜨는지, **stdout 에 프로토콜 아닌 줄이 없는지**
|
|
204
|
+
본다. 마지막 것도 3차 지적이다 — 원래 파싱 실패한 줄을 조용히 버려서,
|
|
205
|
+
기동 경로에 `console.log` 를 넣어도 통과했다.
|
|
206
|
+
|
|
207
|
+
관문이 실제로 막는지 두 가지로 확인했다: dist 버전을 0.3.0 으로 바꾸면 "빌드
|
|
208
|
+
산출물이 낡았습니다", `console.log` 를 넣으면 "stdout 에 프로토콜 아닌 줄" 로 중단된다.
|
|
209
|
+
|
|
210
|
+
## 3차에서 나온 것 — "그림 도구 3종"이 틀렸다
|
|
211
|
+
|
|
212
|
+
`velog_upload_image` 는 로컬 파일을 읽어 올릴 뿐 크롬을 안 쓴다. 크롬이 필요한 건
|
|
213
|
+
`velog_render_diagram` 과 `velog_render_cover` **둘**이고 나머지 19개는 없어도 된다.
|
|
214
|
+
크롬 없는 사용자가 멀쩡히 되는 업로드까지 못 쓴다고 오해할 안내였다.
|
|
215
|
+
|
|
216
|
+
**개수를 세지 않고 이름을 적는다.** 숫자는 코드와 따로 놀지만 이름은 테스트가
|
|
217
|
+
실물(`tools/list`)과 대조할 수 있다. 렌더 호출 지점 수도 함께 세서, 세 번째 도구가
|
|
218
|
+
렌더를 쓰기 시작하면 목록이 낡았다고 실패한다.
|
|
219
|
+
|
|
220
|
+
## 테스트가 자기 자신을 잡은 이야기
|
|
221
|
+
|
|
222
|
+
개인정보 차단(P21)을 **git 추적 파일 전체**로 넓혔더니 두 번 걸렸다.
|
|
223
|
+
|
|
224
|
+
1. 코덱스가 든 예시 주소를 주석에 적었는데 그게 잡혔다.
|
|
225
|
+
2. 규칙 자체를 시험하려고 나쁜 주소·경로를 fixture 로 넣었더니 그것도 잡혔다.
|
|
226
|
+
|
|
227
|
+
두 번째는 진짜 딜레마다 — 스캔에서 이 파일을 빼면 구멍이 되고, 두면 fixture 가 걸린다.
|
|
228
|
+
**실행할 때 조립하는 것**으로 풀었다(`String.fromCodePoint(0x40)`, 배열 `join('/')`).
|
|
229
|
+
소스에는 온전한 주소도 홈 경로도 없고, 규칙은 여전히 시험된다.
|
|
230
|
+
|
|
231
|
+
그리고 규칙을 시험하는 사례가 왜 필요한지도 변이 검증이 알려줬다 — 예외를
|
|
232
|
+
'문자열에 noreply 가 들어 있으면 통과'로 되돌려도, **지금 그 허점을 찌르는 값이
|
|
233
|
+
파일에 없어서** 테스트가 통과했다. 실제 파일만 훑는 검사는 규칙의 느슨함을 못 본다.
|
|
234
|
+
|
|
235
|
+
## 4차에서 나온 것 — 검사가 "있다"와 "먹힌다"는 다르다
|
|
236
|
+
|
|
237
|
+
4차는 새 버그를 찾은 게 아니라 **3차에 넣은 방어들이 우회 가능하다**고 지적했다.
|
|
238
|
+
전부 반례로 확증했다.
|
|
239
|
+
|
|
240
|
+
| 무엇 | 반례 (실제로 통과시켜 확인) |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| P21 이 추적 파일 '전체'를 안 봄 | 확장자 필터가 `LICENSE`·`.gitignore` 를 빼서 **64개 중 62개**만 검사 |
|
|
243
|
+
| P22 가 대응관계를 안 봄 | `CHROME_TOOLS` 를 `[render_diagram, upload_image]` 로 바꿔도 통과 |
|
|
244
|
+
| P23 이 개수만 봄 | `zod` lock 항목을 지워도 92개라 `>50` 통과 |
|
|
245
|
+
| P24 가 낱말만 봄 | `npm run build; npm run verify:dist` 통과 — **빌드가 실패해도 발행이 계속된다** |
|
|
246
|
+
| 관문이 핸드셰이크 직후 죽임 | 응답 뒤 `process.exit(7)`·지연 stdout 오염을 못 봄 |
|
|
247
|
+
| 관문이 npm 표면을 안 봄 | `dist/` 는 git 밖이고 `tsc` 는 옛 산출물을 안 지운다 |
|
|
248
|
+
|
|
249
|
+
고친 방향은 하나로 모인다 — **"존재"가 아니라 "대응"과 "실행 의미"를 본다.**
|
|
250
|
+
|
|
251
|
+
- P21: 확장자 필터 제거(바이너리는 NUL 바이트로 판별). 이메일 예외도 `endsWith` 에서
|
|
252
|
+
**`@` 뒤 정확 일치**로 좁혔다(하위도메인으로 흉내낼 수 있었다).
|
|
253
|
+
- P22: `registerTool(` 블록을 잘라 **어느 도구가 렌더를 부르는지** 집합으로 대조.
|
|
254
|
+
- P23: 개수 대신 **직접 의존성마다 항목·정확한 버전·integrity** 를 확인.
|
|
255
|
+
- P24: 정확한 명령 문자열을 못 박음 — `;` 나 `|| true` 는 관문이 아니다.
|
|
256
|
+
- 관문: 응답 뒤 2초 더 지켜보고(조기 종료·지연 오염), 꼬리 버퍼도 오염으로 세고,
|
|
257
|
+
서버 이름까지 보고, **실제 발행물 97개를 훑어 개인정보를 검사**하고,
|
|
258
|
+
소스 없는 낡은 산출물을 잡는다. `prebuild` 가 `dist` 를 비우고 시작한다.
|
|
259
|
+
|
|
260
|
+
검사 규칙은 `scripts/shipping-checks.ts` 한 곳에 있다 — 나가는 경로가 둘(git 클론 ·
|
|
261
|
+
npm 발행)이라 양쪽에 따로 적으면 한쪽만 느슨해져도 아무도 모른다.
|
|
262
|
+
|
|
263
|
+
## ★★ 5차에서 나온 것 — 발행했으면 도구가 하나도 안 떴다
|
|
264
|
+
|
|
265
|
+
**[높음] 하나가 나왔다. 재현해서 확증했다.**
|
|
266
|
+
|
|
267
|
+
`npx` 와 `npm i -g` 는 `node_modules/.bin/velog-mcp` **심볼릭 링크**를 만들어 실행한다.
|
|
268
|
+
그때 `process.argv[1]` 은 **링크 경로**이고 `import.meta.url` 은 **실제 파일**이다.
|
|
269
|
+
`isDirectRun()` 이 이 둘을 문자열로 비교하고 있었으니 어긋나서 `main()` 이 안 돌았다.
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
심볼릭 링크로 실행 → 출력 0줄, 종료코드 0 ← 아무 말도 없이 끝난다
|
|
273
|
+
실제 경로로 실행 → 정상 기동
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
증상은 **"플러그인은 깔렸는데 도구가 하나도 없음"**이다. 그리고 실제 경로로 실행하는
|
|
277
|
+
테스트는 272개가 전부 통과하므로 아무도 못 본다.
|
|
278
|
+
|
|
279
|
+
고친 것은 두 줄이다 — 양쪽을 `realpathSync` 로 풀어 대조한다(링크·상대경로·`..` 흡수).
|
|
280
|
+
그런데 **진짜 교훈은 고친 방법이 아니라 왜 못 봤는가**다:
|
|
281
|
+
|
|
282
|
+
> **관문이 실물과 다른 모양을 보고 있었다.** `verify-dist` 는 정규화된
|
|
283
|
+
> `dist/index.js` 를 직접 띄웠다. npm 이 하는 방식이 아니었다.
|
|
284
|
+
|
|
285
|
+
그래서 관문도 **임시 심볼릭 링크를 만들어 그걸 실행**하도록 바꿨다. 이제 이 버그를
|
|
286
|
+
되돌리면 관문이 "스스로 종료했습니다(종료코드 0)"로 막는다. 소스 쪽에도 같은
|
|
287
|
+
불변식을 P25 로 박아 뒀다 — 관문은 발행할 때만 돌기 때문이다.
|
|
288
|
+
|
|
289
|
+
## 5차의 나머지 — 우회는 한 단계 안쪽에 남아 있었다
|
|
290
|
+
|
|
291
|
+
| 무엇 | 반례 |
|
|
292
|
+
| --- | --- |
|
|
293
|
+
| P24 가 `prepublishOnly` 만 고정 | `verify:dist` 쪽에 `\|\| true` 를 넣으면 통과 |
|
|
294
|
+
| P23 이 직접 의존성만 확인 | `ajv` 를 지워도 92개라 통과 / `zod.version = "9.9.9-not-real"` 도 통과 |
|
|
295
|
+
| 관문이 JSON 이면 다 인정 | `{"level":"debug"}` 한 줄이 "stdout 순수"로 통과 |
|
|
296
|
+
| `looksTextual` 이 조용히 건너뜀 | UTF-16 텍스트가 **검사 없이** 발행된다 |
|
|
297
|
+
| 소문자 드라이브 | 드라이브 문자가 소문자면 놓친다 (`c:` 로 시작하는 홈 경로) |
|
|
298
|
+
| orphan 검사가 `.js` 만 | 낡은 `.d.ts`·`.js.map` 이 그대로 발행된다 |
|
|
299
|
+
| P22 가 한 파일·고정 이름만 | `renderCover as makeCover` 별칭이면 안내가 조용히 낡는다 |
|
|
300
|
+
|
|
301
|
+
전부 닫았다. 특히 두 가지가 반복해서 나온다:
|
|
302
|
+
|
|
303
|
+
1. **"검사했다"와 "검사 못 했다"를 구분해 말해야 한다.** `continue` 로 넘기면 둘이
|
|
304
|
+
같아 보인다. `scanFiles` 는 건너뛴 목록을 돌려주고, 그게 비어 있지 않으면 막는다.
|
|
305
|
+
2. **규칙은 규칙에 직접 나쁜 값을 먹여야 시험된다.** 실제 파일만 훑는 검사는 규칙이
|
|
306
|
+
느슨해진 걸 못 본다 — 저장소에 그 허점을 찌르는 값이 없기 때문이다.
|
|
307
|
+
|
|
308
|
+
## 6차 — 관문이 여전히 실물과 달랐다 ([높음] 둘)
|
|
309
|
+
|
|
310
|
+
5차에서 "관문이 실물과 다른 모양을 보고 있었다"를 배웠는데, **한 겹 더 있었다.**
|
|
311
|
+
|
|
312
|
+
**① 링크를 '열었지' '실행하지' 않았다.** 관문이 `spawn(process.execPath, [link])` 로
|
|
313
|
+
띄웠다. 그건 node 를 우리가 직접 부르는 것이라 **shebang 을 안 탄다.** npm 은
|
|
314
|
+
링크를 **직접 실행**하고, 그때 커널이 `#!/usr/bin/env node` 를 읽는다.
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
tarball 안 dist/index.js → -rw-r--r-- (실행 권한 없음)
|
|
318
|
+
npm 이 설치할 때 → 실행 권한을 붙이고 링크 직접 실행
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
즉 `#!/usr/bin/env node` 를 지워도 274개 테스트와 관문이 전부 통과하는데
|
|
322
|
+
실제 `npx` 는 기동에 실패한다. 관문을 `spawn(link, [])` 로 바꾸고 대상에 실행
|
|
323
|
+
권한을 붙였다. 이제 shebang 을 지우면 `ENOEXEC` 로 막는다 — 스택 트레이스가 아니라
|
|
324
|
+
**"`#!/usr/bin/env node` 가 있는지 보세요"** 라고 말한다.
|
|
325
|
+
|
|
326
|
+
**② MCP SDK 가 거부하는 JSON 을 정상으로 셌다.** 관문은 `jsonrpc:'2.0'` + `id`/`method`
|
|
327
|
+
만 봤는데, `{"jsonrpc":"2.0","id":999,"level":"debug"}` 는 그 검사를 통과하고
|
|
328
|
+
**SDK 의 `deserializeMessage()` 는 거부한다**(실측). 실제 클라이언트는 파싱 오류로
|
|
329
|
+
연결을 끊는다. → **SDK 가 쓰는 바로 그 `JSONRPCMessageSchema`** 로 판정한다.
|
|
330
|
+
프로토콜 판정을 우리가 흉내 낼 이유가 없다.
|
|
331
|
+
|
|
332
|
+
## ★ 그리고 관문 자체에는 테스트가 없었다
|
|
333
|
+
|
|
334
|
+
6차 변이를 돌리다 알았다 — `verify-dist.ts` 를 **아무리 망가뜨려도 274개가 통과한다.**
|
|
335
|
+
테스트 스위트가 그 파일을 실행하지 않기 때문이다. **검사하는 물건이 검사받지
|
|
336
|
+
않고 있었다.**
|
|
337
|
+
|
|
338
|
+
그래서 관문 전용 변이 검증을 따로 만들었다. 방식이 다르다 — 둘을 함께 건다:
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
① 관문의 검사 하나를 없앤다
|
|
342
|
+
② 그 검사가 잡아야 할 알려진 불량 dist 를 넣는다
|
|
343
|
+
→ 불량이 통과하면 그 검사가 유일하게 막고 있었다는 뜻 (= 제 몫을 한다)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
셋 다 확인했다 — shebang 검증 · SDK 스키마 판정 · 발행물 개인정보 검사.
|
|
347
|
+
|
|
348
|
+
⚠️ 처음엔 이 스크립트의 **판정 문구를 거꾸로** 적었다("통과함 = 테스트가 약하다").
|
|
349
|
+
변이 검증은 대상마다 성공의 뜻이 다르다. 무엇이 좋은 결과인지 먼저 적고 시작할 것.
|
|
350
|
+
|
|
351
|
+
## 6차의 나머지
|
|
352
|
+
|
|
353
|
+
| 무엇 | 반례 |
|
|
354
|
+
| --- | --- |
|
|
355
|
+
| P23 이 트리 완전성을 증명 못 함 | `ajv` 항목을 지워도 92개라 통과 / `zod` 에 `dev:true` 만 붙이면 검사 대상에서 빠짐 |
|
|
356
|
+
| integrity 정규식이 헐렁 | `sha512-A`·`sha512-====` 통과, 반대로 SHA-256/384 는 거부 |
|
|
357
|
+
| NUL 검사가 앞 8KiB 뿐 | `A` 8,192자 뒤의 UTF-16 홈 경로가 **검사한 척** 통과 |
|
|
358
|
+
| `Users` 대소문자 | `c:\\users\\...` 를 놓침 |
|
|
359
|
+
| P22 가 named import 만 | `import * as R` · 동적 import 를 못 따라감 |
|
|
360
|
+
|
|
361
|
+
앞의 둘은 **node_modules 해석 규칙대로 위로 올라가며 전이 의존성이 풀리는지** 확인하고,
|
|
362
|
+
직접 의존성이 `dev` 로 위장되지 않았는지 본다. P22 는 못 따라가는 import 모양을
|
|
363
|
+
만나면 **조용히 넘기지 않고 실패**한다 — 감당 못 하는 걸 감당하는 척하지 않는다.
|
|
364
|
+
|
|
365
|
+
## 7차 — 스키마를 '외피까지만' 봤다, 그리고 관문 검증이 반쪽이었다
|
|
366
|
+
|
|
367
|
+
**[높음] 하나.** 6차에서 SDK 의 `JSONRPCMessageSchema` 를 도입했는데, 그건 **전송
|
|
368
|
+
외피만** 본다. 실제 SDK 클라이언트는 그 위에 결과 스키마를 한 번 더 적용한다.
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
{jsonrpc,id,result:{serverInfo}} 외피 통과 · InitializeResult 거부
|
|
372
|
+
{tools:[{name}]} (inputSchema 없음) 외피 통과 · ListToolsResult 거부
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
즉 `inputSchema` 가 빠진 도구를 내보내도 관문은 통과하고 **진짜 클라이언트는 도구
|
|
376
|
+
조회에서 연결을 거부한다.** → `InitializeResultSchema`·`ListToolsResultSchema` 도 적용한다.
|
|
377
|
+
실증: 도구에서 `inputSchema` 를 벗기는 dist 를 만들어 관문이 막는 것 확인.
|
|
378
|
+
|
|
379
|
+
**관문 검증이 반쪽이었다.** 6차에 만든 관문 변이 스크립트를 세션 임시 폴더에서만
|
|
380
|
+
돌렸고, **한 방향만** 봤다 — "검사를 빼면 불량이 통과하는가". 그것만으로는
|
|
381
|
+
**원래 관문도 그 불량을 못 잡던 경우**를 "제 몫을 했다"고 오판한다.
|
|
382
|
+
|
|
383
|
+
`scripts/gate-mutation.sh` 로 저장소에 넣고 **양방향**으로 바꿨다:
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
① 온전한 관문 + 불량 dist → 반드시 막아야 한다
|
|
387
|
+
② 그 검사만 없앤 관문 + 같은 불량 → 통과해야 한다
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
여섯 검사 전부 양방향 통과(shebang · 외피 스키마 · 결과 스키마 · 발행물 개인정보 ·
|
|
391
|
+
조기 종료 · 낡은 산출물). P26 이 이 스크립트가 사라지거나 한 방향만 보게 되는 걸 막는다.
|
|
392
|
+
|
|
393
|
+
⚠️ ③번을 처음 돌렸을 때 `~`(중복) 판정이 나왔는데, 알고 보니 **변이 설계 문제**였다 —
|
|
394
|
+
검사를 지우니 `listResult.data` 가 undefined 라 크래시로 막힌 것이지 다른 검사가
|
|
395
|
+
잡은 게 아니었다. 스키마 호출 자체를 무력화하는 변이로 바꾸니 제대로 나왔다.
|
|
396
|
+
|
|
397
|
+
**의존성 검사가 또 헐렁했다.** `dev:true` 를 붙이면 검사 대상에서 빠지고,
|
|
398
|
+
`sha512-AAAAAAAAAAAAAAAAAAAA`(20자)가 통과하고, 선언 범위와 안 맞는 버전도 통과했다.
|
|
399
|
+
→ 해석 시 `dev` 항목이면 실패로 보고, **선언 범위와의 호환성**을 확인하고,
|
|
400
|
+
integrity 는 **알고리즘별 실제 digest 길이**(sha512=88)를 요구한다.
|
|
401
|
+
|
|
402
|
+
범위 판정은 최소 구현을 직접 썼다 — 이 저장소는 런타임 의존성 2개를 유지하므로
|
|
403
|
+
테스트를 위해 `semver` 를 들이지 않는다. 대신 **모르는 표기를 만나면 던진다.**
|
|
404
|
+
조용히 true 를 돌려주면 검사가 있는 척만 한다.
|
|
405
|
+
|
|
406
|
+
## 8차 — 흉내를 그만두고, 내 검증 습관의 구멍을 찾았다
|
|
407
|
+
|
|
408
|
+
코덱스 판정: **제품 자체 결함 없음**(실제 SDK Client 로 연결·도구 21개 확인).
|
|
409
|
+
남은 것은 전부 **검증 장치** 문제였다. 그중 둘이 특히 아프다.
|
|
410
|
+
|
|
411
|
+
### ★ "lint 0" 이라고 보고했는데 사실이 아니었다
|
|
412
|
+
|
|
413
|
+
검증을 이렇게 돌리고 있었다:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
npm run lint 2>&1 | tail -1 && npm test ...
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`| tail -1` 을 붙이면 **파이프라인 종료코드가 `tail` 의 것**이라 항상 0 이다.
|
|
420
|
+
`&&` 가 그대로 진행했고, `tail -1` 이 보여준 마지막 줄은 공백이었다.
|
|
421
|
+
**검사가 도는 것처럼 보였지만 아무것도 막지 않았다** — 이 문서가 내내 다룬 그 병을
|
|
422
|
+
내 손이 저지르고 있었다. 실제로 `verify-dist.ts` 에 lint 오류 2건이 있었다.
|
|
423
|
+
|
|
424
|
+
→ 파이프 없이 `&&` 로만 잇는 `npm run verify` 하나를 뒀다.
|
|
425
|
+
|
|
426
|
+
### ★ 흉내를 정교하게 만드는 건 끝이 없다
|
|
427
|
+
|
|
428
|
+
프로토콜 판정이 세 라운드 연속으로 부족했다:
|
|
429
|
+
|
|
430
|
+
| 회차 | 그때의 검사 | 통과해버린 것 |
|
|
431
|
+
| --- | --- | --- |
|
|
432
|
+
| 6차 | `jsonrpc:'2.0'` + id/method | SDK 가 거부하는 JSON |
|
|
433
|
+
| 7차 | + 외피 스키마 | `inputSchema` 없는 도구 |
|
|
434
|
+
| 8차 | + 결과 스키마 | 지원하지 않는 `protocolVersion` |
|
|
435
|
+
|
|
436
|
+
매번 한 겹 더 쌓았는데 매번 한 겹이 더 있었다. 그래서 방향을 바꿨다 —
|
|
437
|
+
**진짜 SDK 클라이언트를 붙여본다.** `Client` + `StdioClientTransport` 로 실제 연결하고
|
|
438
|
+
`listTools()` 를 부른다. 이제 그 부류가 통째로 닫힌다:
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
protocolVersion 을 못 쓰는 값으로 → ❌ "Server's protocol version is not supported"
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
같은 판단을 **최소 semver** 에도 적용했다. 직접 쓴 판정은 이 lock 의 280쌍과 전부
|
|
445
|
+
일치했지만, 코덱스가 lock 에 없는 모양(`^0.0.3`, prerelease)으로 네 개의 불일치를
|
|
446
|
+
찾았다. 실제 `semver` 를 devDependency 로 들여 교체했다(런타임 의존성은 그대로 2개).
|
|
447
|
+
|
|
448
|
+
### 나머지
|
|
449
|
+
|
|
450
|
+
- **dist 의 도구 이름 하나를 바꿔도 통과했다.** 개수와 크롬 도구 둘만 봤으니까.
|
|
451
|
+
→ 이름 목록을 적어두면 또 손으로 맞춰야 하므로 **소스를 직접 띄워 대조**한다.
|
|
452
|
+
- **integrity 가 문자열 길이만 봤다.** `sha512-` + `A` 88개는 디코딩하면 66바이트다.
|
|
453
|
+
→ **디코딩된 digest 바이트 수**(sha512=64)를 본다.
|
|
454
|
+
- **관문 변이 스크립트가 `fail == 0` 만 요구했다.** 검사를 통째로 건너뛰어도
|
|
455
|
+
`0/0` 으로 성공한다. → 실행된 검사 수를 함께 요구한다. `chmod` 불변식도 추가.
|
|
456
|
+
- **판정 기준을 또 고쳤다.** 실제 클라이언트 검사를 넣은 뒤 shebang·결과스키마가
|
|
457
|
+
'겹침' 으로 나왔는데, 겹침은 결함이 아니다 — 좁은 검사는 **더 나은 오류 메시지**를
|
|
458
|
+
위해 남긴다. ①(온전한 관문이 못 잡음)만 실패로 센다.
|
|
459
|
+
|
|
460
|
+
## shrinkwrap 갱신 절차
|
|
461
|
+
|
|
462
|
+
의존성을 올릴 때는 이 순서로 한다. 건너뛰면 고정의 의미가 사라진다.
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
npm install <패키지>@<버전> # 또는 npm update
|
|
466
|
+
npm shrinkwrap # package-lock → npm-shrinkwrap 재생성
|
|
467
|
+
npm run typecheck && npm run lint && npm test
|
|
468
|
+
npm run build && npm run verify:dist
|
|
469
|
+
npm pack --pack-destination /tmp # 실제 tarball 을 풀어 눈으로 확인
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`npm install` 은 `package-lock.json` 을 다시 만들 수 있다. 그때는 `npm shrinkwrap`
|
|
473
|
+
을 한 번 더 돌려 이름을 되돌린다 — 둘이 공존하면 npm 이 shrinkwrap 을 쓰지만
|
|
474
|
+
저장소에 낡은 lock 이 남는다.
|
|
475
|
+
|
|
476
|
+
## 9차 — 새 검사를 만들면, 그 검사도 변이 대상에 넣어야 한다
|
|
477
|
+
|
|
478
|
+
코덱스 판정: 제품 결함 0, 높음 0. 남은 다섯이 전부 **검증 장치의 검증** 문제였다.
|
|
479
|
+
8차에서 만든 핵심 관문 두 개(실제 클라이언트·소스 대조)가 정작 **관문 변이 목록에
|
|
480
|
+
없었다** — 그 관문을 약화해도 7/7 이 계속 초록이었다. 만들 때 넣지 않으면 영원히
|
|
481
|
+
안 들어간다.
|
|
482
|
+
|
|
483
|
+
- **조건부 분기는 아무도 안 봤다.** 소스-dist 대조가 기본 분기(도구 21개)만 띄웠다.
|
|
484
|
+
`VELOG_ALLOW_PROFILE=1` 에서만 등록되는 5개와 `VELOG_ALLOW_PUBLIC=1` 의
|
|
485
|
+
`is_private` 스키마는 깨져도 통과했다. → 대조를 **두 모드(기본·모든 옵션)**로
|
|
486
|
+
돌리고, 이름 집합만이 아니라 **도구 스냅샷(스키마·설명 포함)** 전체를 비교한다.
|
|
487
|
+
- **관문 변이가 timeout 도 "잡았다"로 셌다.** `caught != 0` 이면 전부 성공이었다 —
|
|
488
|
+
timeout(124)·`timeout` 부재(127)·크래시도. 관문 전체가 매달려도 7/7 이 된다.
|
|
489
|
+
→ 관문의 fail() 은 exit 1 이고 npm run 은 그대로 전파한다(실측). **정확히 1** 만
|
|
490
|
+
"잡았다"로 센다. chmod 불변식도 기준선 rc=0 을 함께 요구한다.
|
|
491
|
+
- **integrity 가 두 번째 `-` 뒤를 조용히 버렸다.** `sha512-<정상>-garbage` 통과
|
|
492
|
+
(ssri strict 는 거부). → 첫 `-` 에서만 가르고 나머지 전부를 digest 로 본다.
|
|
493
|
+
- **README 의 검증 수치가 낡았다.** 234·20 인 채로 발행될 뻔했다. → 276·54+12 로.
|
|
494
|
+
수치는 또 낡을 것이다 — 갱신 목록에 README 를 넣는 것 말고는 약이 없다.
|
|
495
|
+
|
|
496
|
+
관문 전용 변이는 7 → **11** (이름 약화·스냅샷 드리프트·protocolVersion·조건부 모드
|
|
497
|
+
추가). P26 이 네 항목의 존재와 rc 구분을 함께 강제한다.
|
|
498
|
+
|
|
499
|
+
### 9차 반영분의 반영 검토 — 고치는 손도 검사받아야 한다
|
|
500
|
+
|
|
501
|
+
위 반영 커밋을 코덱스에 다시 물었더니 셋이 나왔고, 셋 다 맞았다.
|
|
502
|
+
|
|
503
|
+
- **protocolVersion 변이의 '겹침'은 가짜였다.** 루프를 비우는 변이가 (당시 루프
|
|
504
|
+
밖에 있던) 개수 대조까지 깨뜨려 **정상 dist 도 막는 관문**을 만들었고, 그 rc!=0 을
|
|
505
|
+
"다른 검사가 겹쳐 잡는다"로 오판했다. → 개수 대조를 루프 안(기본 모드)으로 옮겨
|
|
506
|
+
루프 제거가 일관된 변이가 되게 하고, check() 에 **기준선**을 넣었다: 변이 관문은
|
|
507
|
+
정상 dist 를 통과시켜야 한다(rc=0). 못 시키면 검사 제거가 아니라 관문 파괴 — 판정 무효.
|
|
508
|
+
- **{0,0}·{1,1} 두 조합은 독립 플래그를 못 가른다.** dist 가 조건을 `PROFILE || PUBLIC`
|
|
509
|
+
으로 잘못 묶어도 두 조합에선 소스와 같다. → **네 조합 전부** 대조. 변이도
|
|
510
|
+
ALLOW_PUBLIC 쪽을 따로 추가(12종).
|
|
511
|
+
- **fail() 의 exit 1 은 node 크래시와 같은 코드다.** 관문이 구문 오류로 죽어도
|
|
512
|
+
"잡았다"가 된다. → fail() 을 **exit 2** 로 올려 크래시(1)·timeout(124)·부재(127)와
|
|
513
|
+
전부 구분한다.
|
|
514
|
+
|
|
515
|
+
## 결과
|
|
516
|
+
|
|
517
|
+
- 토큰이 `~/.claude.json` 평문에서 macOS 키체인으로 간다
|
|
518
|
+
- 설치가 두 줄이 된다
|
|
519
|
+
- 의존성 트리 93개가 발행물에 고정된다
|
|
520
|
+
- `npm publish` 가 **npm 과 같은 모양(심볼릭 링크)으로** dist 실물 검증을 거친다
|
|
521
|
+
- 새 테스트 42개(P1~P26), **변이 검증 54/54 + 관문 전용 12/12(양방향+기준선)**
|
|
522
|
+
- 발행 관문이 **실제 SDK 클라이언트**로 붙어보고, 플래그 4조합 전부에서 소스와
|
|
523
|
+
도구 스냅샷(스키마 포함)을 대조한다
|
|
524
|
+
- `claude plugin validate --strict` 통과
|
|
525
|
+
|
|
526
|
+
## 남은 것
|
|
527
|
+
|
|
528
|
+
공식 디렉터리(`claude-plugins-official`)는 **신청 절차가 없다** — Anthropic 재량이다.
|
|
529
|
+
서드파티가 갈 수 있는 곳은 `claude-plugins-community` 이고 Console 폼으로 제출한다.
|
|
530
|
+
거기 올리면 승인 후 CI 가 커밋을 따라 핀을 올리므로, `version` 게이트가 더 중요해진다.
|
|
531
|
+
제출 전에 정해야 할 것은 하나 — **모르는 사람에게 "브라우저 쿠키에서 토큰을 꺼내라"고
|
|
532
|
+
시키는 것을 어떻게 다룰지**다. 고칠 수 없고(벨로그에 API 가 없다) 드러내는 수밖에 없다.
|