deel-local-cli 1.20.13 → 2.0.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.
Files changed (155) hide show
  1. package/README.ko.md +96 -78
  2. package/README.md +119 -81
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +23 -3
  7. package/src/acp/serve.js +333 -25
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +253 -26
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +300 -0
  58. package/src/commands/model.js +852 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +452 -39
  64. package/src/configexplain.js +125 -14
  65. package/src/doctor.js +164 -26
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +507 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +11 -1
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +444 -36
  113. package/src/tools/fig.js +21 -4
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1029 -145
  118. package/src/tools/jobs.js +325 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +366 -56
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +312 -43
  127. package/src/tools/webfetch.js +184 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
  150. /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
  151. /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
  152. /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
  153. /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
@@ -209,6 +209,8 @@ export const 갈래 = [
209
209
  { id: 'slack', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/g },
210
210
  { id: 'aws', re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
211
211
  { id: 'google', re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
212
+ // `stripe.setKey("sk_live_…")` 처럼 이름이 비밀을 말하지 않는 자리는 값의 꼴로만 안다(2.0.0 3회차 사냥).
213
+ { id: 'stripe', re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
212
214
  {
213
215
  // 머리.몸통.서명 세 토막. 안에 무엇이 들었는지 우리는 안 열어 본다.
214
216
  id: 'jwt',
@@ -218,9 +220,79 @@ export const 갈래 = [
218
220
  // https://사람:토큰@호스트 — git remote -v 가 이 모양으로 찍는다.
219
221
  // 사람 이름은 남긴다. 무슨 계정인지 모르면 사람이 손을 못 쓴다.
220
222
  id: '주소속열쇠',
221
- re: /([a-z][a-z0-9+.-]*:\/\/)([^\s/:@]{1,64}):([^\s/@]{1,256})@/gi,
223
+ re: /([a-z][a-z0-9+.-]*:\/\/)([^\s/:@]{1,64}):([^\s/?#]{1,256})@/gi, // 비밀번호는 마지막 @ 까지 — 날것 @ 가 든 비밀번호의 뒷조각이 샜다 (6회차 Gemini 가림6r T3)
222
224
  바꾸기: (m, 앞, 사람) => `${앞}${사람}:${표('주소속열쇠')}@`,
223
225
  },
226
+ {
227
+ /*
228
+ * `curl -u 사람:암호` — 주소 밖에 적은 같은 한 쌍이다. 이름(`-u`)도 값도 비밀이라고 말하지
229
+ * 않아서 어느 규칙에도 안 걸리고 감사기록에 그대로 남았다(2.0.0 3회차 사냥).
230
+ * 위 주소속열쇠와 같이 사람 이름은 남긴다. `$TOKEN` · `%PASS%` 는 셸이 채울 자리표라 안 가린다.
231
+ *
232
+ * ── 빈칸 **하나 없다고** 평문으로 나갔다 (사냥5 R5-M2) ──────────────────
233
+ *
234
+ * 짧은 옵션은 값을 붙여 적어도 뜻이 같다 — `curl -uadmin:암호` 가 `curl -u admin:암호` 다. 그런데
235
+ * `-u` 뒤에 빈칸이나 `=` 를 **요구해서** 붙여 적은 꼴이 암호째 모델·audit.jsonl·세션에 남았다.
236
+ * 그래서 사이는 있어도 없어도 된다. 스위치를 몰아 적은 `-sSu` 도 같다(값을 받는 짧은 옵션은
237
+ * 몰아 적은 것의 **맨 끝**에만 올 수 있다). `-U` · `--proxy-user` 는 프록시 쪽의 같은 한 쌍이다.
238
+ *
239
+ * ── 따옴표로 감쌌는데 빈칸이 들었다고 평문으로 나갔다 (2.0.0 6회차 · Gemini 지킴5) ──
240
+ *
241
+ * 따옴표 한 쌍을 받으면서 값에는 빈칸을 안 받아서 `curl -u 'admin:pass word'` 가 통째로 샜다.
242
+ * 따옴표를 쓰는 까닭이 바로 빈칸·특수문자다. 그래서 두 갈래로 나눈다 — 따옴표 안이면 닫는
243
+ * 따옴표 말고는 다 받고, 맨 값이면 전처럼 빈칸에서 끝난다.
244
+ *
245
+ * ── 따옴표를 **암호에만** 씌운 꼴이 통째로 샜다 (2.0.0 8회차) ─────────
246
+ *
247
+ * `curl -u 유저:"암호"`. 손으로 적을 때 제일 흔한 모양이다 — 특수문자가 든 쪽은
248
+ * 암호뿐이니 거기만 감싼다. 그런데 위 두 갈래는 따옴표가 **사람 앞**에 올 때만 봤고,
249
+ * 맨 값 갈래는 값에 따옴표를 아예 안 받아(`[^\s"']`) 첫 글자에서 어긋났다.
250
+ * 그래서 셋째 갈래를 둔다 — 사람은 맨 값, 암호만 따옴표 안.
251
+ */
252
+ id: '명령속열쇠',
253
+ re: /(\bcurl\b[^\n|;&]*?\s(?:-[A-Za-z]*[uU]|--(?:proxy-)?user)(?:\s+|=)?)(?:(["'])([^\s:"'$%]{1,64}):(?![$%«])((?:(?!\2)[^\n]){4,256})\2|([^\s:"'$%]{1,64}):(["'])(?![$%«])((?:(?!\6)[^\n]){4,256})\6|([^\s:"'$%]{1,64}):(?![$%«])([^\s"']{4,256}))(?=\s|$)/g,
254
+ 바꾸기: (m, 앞, 따, 사람, 암호, 반사람, 반따, 반암호, 맨사람) => {
255
+ if (따) return `${앞}${따}${사람}:${표('명령속열쇠')}${따}`;
256
+ if (반따) return `${앞}${반사람}:${반따}${표('명령속열쇠')}${반따}`;
257
+ return `${앞}${맨사람}:${표('명령속열쇠')}`;
258
+ },
259
+ },
260
+ {
261
+ /*
262
+ * ── 이름이 붙임표 뒤에 있어서 아무 갈래에도 안 걸린 암호 (사냥5 R5-M3) ──
263
+ *
264
+ * wget --password=암호 http://x 평문
265
+ * wget --http-password=암호 http://x 평문
266
+ * mysql -h db -p암호 dbname 평문
267
+ * mysql --password=암호 dbname 평문
268
+ *
269
+ * 아래 환경변수 갈래는 이름을 `password` 로 알아보면서도 `(?<!-)` 로 붙임표 뒤를 뺀다
270
+ * (`id-token` 의 `token` 을 안 집으려는 것이다). 그리고 맨 값은 줄 끝이나 쌍반점에서 끝나야
271
+ * 하는데 명령줄에서는 뒤에 주소나 DB 이름이 온다. 두 턱에 다 걸려 명령에 적은 암호가 통째로 샜다.
272
+ *
273
+ * 세 꼴만 본다.
274
+ *
275
+ * - `--…password=값` · `--…passphrase=값` — 긴 옵션에 `=` 로 붙인 것은 **어느 명령에서나** 값이다.
276
+ * 빈칸으로 띄운 `--password 값` 은 안 본다. psql·mysql 의 `--password` 는 값 없이 「물어라」
277
+ * 이고, 뒤 낱말은 DB 이름이다. `--password-file=` · `--password-stdin` 은 이름이 password 로
278
+ * 끝나지 않아 안 걸린다.
279
+ * - wget 은 예외로 띄운 꼴도 본다. wget 의 `--password` 계열은 값을 **반드시** 받는다.
280
+ * - `-p값` 은 **mysql·mariadb 계열 명령일 때만**. 다른 데서 `-p` 는 포트(`ssh -p 2222`)·
281
+ * 게시(`docker run -p 8080:80`)·부모까지(`mkdir -p`)다. 맨 `-p` 는 묻겠다는 뜻이라 붙은 값이
282
+ * 있어야 걸린다. 명령 이름 뒤에 빈칸을 요구해 `mysql_host=db ssh -p2222` 같은 것은 안 닿는다.
283
+ *
284
+ * 따옴표 안에 빈칸이 든 암호(`--password="a b c"`)도 받는다. `$PASS` · `%PW%` 는 자리표라 안 가린다.
285
+ *
286
+ * - `sshpass -p 값` (6회차 · Gemini 지킴5) — sshpass 의 `-p` 는 붙여도 띄워도 암호다. 그런데 위
287
+ * 세 꼴 어디에도 없어 통째로 샜다. sshpass 옵션은 감쌀 명령 **앞**에만 오므로 `-p` 앞에는
288
+ * 붙임표로 시작하는 스위치만 허락한다. 그래야 `sshpass -f 파일 ssh -p 2222` 의 ssh 포트까지
289
+ * 넘어가 집지 않는다. `-P 프롬프트` 처럼 값을 받는 옵션이 앞에 있으면 못 보고 지나간다 —
290
+ * 가리지 못하는 쪽이지 멀쩡한 글을 망가뜨리는 쪽이 아니다.
291
+ */
292
+ id: '명령속열쇠',
293
+ re: /((?:^|\s)--(?:[A-Za-z0-9]+-)*(?:password|passwd|passphrase)=|\bwget(?=\s)[^\n|;&]*?\s--(?:(?:http|ftp|proxy)-)?password\s+|\b(?:mysql\w*|mariadb[\w-]*)(?=\s)[^\n|;&]*?\s-p|\bsshpass(?:\s+-[^\sp-]\S*)*\s+-p\s*)(?:(["'])(?![$%«])((?:(?!\2)[^\n]){1,256})\2|(?![$%«])[^\s"'`]{1,256})(?=\s|$)/g,
294
+ 바꾸기: (m, 앞, 따) => (따 ? `${앞}${따}${표('명령속열쇠')}${따}` : `${앞}${표('명령속열쇠')}`),
295
+ },
224
296
  {
225
297
  /*
226
298
  * 줄 끝까지 가린다.
@@ -232,9 +304,57 @@ export const 갈래 = [
232
304
  * 이미 가린 자리는 다시 안 건드린다. 두 번 가리면 무엇이 가려졌는지가
233
305
  * 뭉개지고 셈도 부풀려진다.
234
306
  */
307
+ /*
308
+ * ── 줄 끝까지 가리면 **어디로 갔는지**까지 지워진다 ──────────────────
309
+ *
310
+ * 위 머리말은 「줄 끝까지 가린다」 고 적어 뒀다. 그 까닭('Bearer' 만
311
+ * 가리고 값을 남기면 안 가린 것만 못하다)은 지금도 맞다. 그런데 이 값은
312
+ * 흔히 **따옴표 안에** 있고, 그 뒤에 주소가 온다:
313
+ *
314
+ * curl -H "Authorization: Bearer tok" https://gw.corp/v1
315
+ * → curl -H "Authorization: «가림:헤더» ← 주소까지 사라졌다
316
+ *
317
+ * 감사기록에 남는 것이 이 줄이다. 열쇠는 가려야 하지만 **주소는 봐야
318
+ * 하는 쪽**이다 — 열쇠가 어느 서버로 갔는지가 감사에서 묻는 바로 그
319
+ * 한 가지다. 그런데 기록은 온전한 한 줄처럼 생겼으니, 지워진 줄
320
+ * 알아챌 방법도 없다.
321
+ *
322
+ * 그래서 **따옴표에서 멈춘다.** 값이 따옴표에 싸여 있으면 그 안만
323
+ * 가리고 따옴표는 남긴다. 안 싸여 있으면 여는 따옴표 앞까지만 간다.
324
+ * 열쇠에 따옴표가 들어가는 일은 없다.
325
+ *
326
+ * 이름 뒤의 따옴표도 받는다. 짜임새 있는 글에서는 머리글 이름 자체가
327
+ * 따옴표에 싸인다 — `{ 'Authorization': 'Bearer …' }` 가 파이썬·JS 에서
328
+ * 제일 흔한 꼴인데, 이름 바로 뒤에 `:` 가 올 것만 보고 있어서 통째로
329
+ * 지나갔다.
330
+ */
235
331
  id: '헤더',
236
- re: /\b(Authorization|Proxy-Authorization|X-Api-Key|Api-Key|X-Auth-Token)(\s*[:=]\s*)(?!«)([^\r\n]+)/gi,
237
- 바꾸기: (m, 이름, 사이) => `${이름}${사이}${표('헤더')}`,
332
+ /*
333
+ * 틀 낱말 뒤에 따옴표 값이 오는 꼴(`Bearer "tok"`)은 따로 받는다. 맨 값
334
+ * 갈래는 따옴표 앞에서 멈추니 'Bearer ' 만 가리고 토큰을 남기면서 가렸다고
335
+ * 셌다(2.0.0 2차 리뷰). 따옴표 사이에 빈칸이 있으면 토큰이 아니라 코드다
336
+ * (`"Bearer " + token, "X"`) — 그건 안 먹는다.
337
+ *
338
+ * 틀 낱말은 목록으로 두지 않는다. Bearer·Basic 만 적어 뒀더니 회사마다 다른
339
+ * `ApiKey "…"` · `SSWS '…'` 가 그대로 샜다(3회차). 낱말 하나(서른 자까지)면 된다.
340
+ */
341
+ /*
342
+ * 맨 값은 **쉼표·닫는 중괄호에서도** 멈춘다. 줄 끝까지 먹었더니 명령 출력에 실린 코드
343
+ * `{ Authorization: Bearer_token, timeout: 5000 }` 가 `{ Authorization: «가림:헤더»` 로 뒤가
344
+ * 통째로 사라졌다(2.0.0 6회차 · Gemini 비밀6). Bearer·Basic 토큰에는 쉼표·중괄호가 안 든다.
345
+ */
346
+ re: /\b(Authorization|Proxy-Authorization|X-Api-Key|Api-Key|X-Auth-Token)(["']?\s*[:=]\s*)(?!«)(?:(["'])(?!«)((?:(?!\3)[^\r\n])+)\3|([A-Za-z][\w.-]{0,30}\s+)(["'])(?!«)([^\s"'`\r\n]+)\6|([^\r\n"',}]+))/gi,
347
+ 바꾸기: (m, 이름, 사이, 따, 값안, 틀, 틀따) => {
348
+ /*
349
+ * 따옴표 안이 **틀 낱말과 빈칸뿐**이면 비밀이 아니라 코드다 —
350
+ * `Authorization: "Bearer " + token` 을 `"«가림:헤더»" + token` 으로 뭉갰다(6회차 · Gemini 비밀6).
351
+ * 토큰은 따옴표 밖 변수에 있고, 여기 가릴 것이 없다.
352
+ */
353
+ if (따 && /^[A-Za-z][\w.-]{0,30}\s+$/.test(값안)) return m;
354
+ if (따) return `${이름}${사이}${따}${표('헤더')}${따}`;
355
+ if (틀따) return `${이름}${사이}${틀}${틀따}${표('헤더')}${틀따}`;
356
+ return `${이름}${사이}${표('헤더')}`;
357
+ },
238
358
  },
239
359
  {
240
360
  /*
@@ -584,7 +704,7 @@ export const 갈래 = [
584
704
  * 것보다 낫다(1.19.0). 검사로 못 박아 둔다.
585
705
  */
586
706
  id: '환경변수',
587
- re: /\b(?<!-)(_?(?:[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?)|[A-Z0-9_]*[A-Z0-9]_?KEY|[A-Z0-9_]*_PWD|[A-Za-z][A-Za-z0-9]*(?:[-_][A-Za-z0-9]+)*(?:[-_](?:key|token|secret|password|passwd|pwd|credentials?|KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIALS?|Key|Token|Secret|Password|Passwd|Pwd|Credentials?)|Key|Token|Secret|Password|Passwd|Pwd|Credentials?)|(?:password|secret|token|passwd|credentials?)))(?:(["']?[ \t]*(?::[^\n,;]{0,200}?|:[^\n]{0,200}?[>\])}][^\n,;]{0,80}?)[ \t]*=(?!=|>)\s*)((?:\(\s*)+)?(["'`])(?!«)((?:\\[^\n]|(?!\4)[^\n]){4,}?)\4((?:\s*\))+)?|(["']?\s*(?::(?![^\n]{0,200}?=[ \t]*«가림:)|=)\s*)((?:\(\s*)+)?(?:(["'`])(?!«)((?:\\[^\n]|(?!\9)[^\n]){4,}?)\9|(?!«)(?![\w$.]+\s*[([])(?!(?:string|number|boolean|bigint|symbol|object|unknown|never|void|null|undefined|true|false)\b)(?![^\n]{0,200}?}[ \t]*(?:=(?!=|>)|\)))(?!\(*\s*(?:process\.env|import\.meta\.env|globalThis\.process\.env|globalThis\.env|os\.environ|os\.getenv|ENV(?=\s*[[.(])|Deno\.env|Bun\.env)\b)([^\s"'()[\]{},;<>]{4,})(?=[ \t]*(?:[;,)\]}\r\n#]|$)))((?:\s*\))+)?)/g,
707
+ re: /\b(?<!-)(_?(?:[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|CREDENTIALS?)|[A-Z0-9_]*[A-Z0-9]_?KEY|[A-Z0-9_]*_(?:PWD|PASS)|[A-Za-z][A-Za-z0-9]*(?:[-_][A-Za-z0-9]+)*(?:[-_](?:key|token|secret|password|passwd|pwd|passphrase|pass|credentials?|KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|PASSPHRASE|PASS|CREDENTIALS?|Key|Token|Secret|Password|Passwd|Pwd|Passphrase|Pass|Credentials?)|Key|Token|Secret|Password|Passwd|Pwd|Credentials?)|(?:password|secret|token|passwd|passphrase|credentials?)))(?:(["']?[ \t]*(?::[^\n,;]{0,200}?|:[^\n]{0,200}?[>\])}][^\n,;]{0,80}?)[ \t]*=(?!=|>)\s*)((?:\(\s*)+)?(["'`])(?!«)((?:\\[^\n]|(?!\4)[^\n]){4,}?)\4((?:\s*\))+)?|(["']?\s*(?::(?![^\n]{0,200}?=[ \t]*«가림:)|=)\s*)((?:\(\s*)+)?(?:(["'`])(?!«)((?:\\[^\n]|(?!\9)[^\n]){4,}?)\9|(?!«)(?![\w$.]+\s*[([])(?!(?:string|number|boolean|bigint|symbol|object|unknown|never|void|null|undefined|true|false)\b)(?![^\n]{0,200}?}[ \t]*(?:=(?!=|>)|\)))(?!\(*\s*(?:process\.env|import\.meta\.env|globalThis\.process\.env|globalThis\.env|os\.environ|os\.getenv|ENV(?=\s*[[.(])|Deno\.env|Bun\.env)\b)([^\s"'()[\]{},;<>]{4,})(?=[ \t]*(?:[;,)\]}\r\n#]|$)))((?:\s*\))+)?)/g,
588
708
  바꾸기: (m, 이름, 타입사이, 타입여는, 따A, 값A, 타입닫는,
589
709
  민사이, 여는, 따B, 값B, 값C, 닫는, 자리, 전체) => {
590
710
  // 따옴표 없는 맨 값만 「코드가 가리키는 이름인가」 를 되묻는다.
@@ -610,12 +730,60 @@ export const 갈래 = [
610
730
  */
611
731
  const 값자리 = 자리 + m.lastIndexOf(값C);
612
732
  if (!점값을가릴까(전체, 자리, 값C) || !점값을가릴까(전체, 값자리, 값C)) return m;
733
+ /*
734
+ * 글자뿐인 PascalCase 맨 값 뒤에 `;` 가 오면 타입 멤버다 — `interface Config { API_KEY: Buffer; }`
735
+ * 가 가려졌다(2.0.0 6회차 · Gemini 비밀6). 기본형 이름만 빼 두었었다. `.env`·YAML 의 맨 값
736
+ * 뒤에는 `;` 가 안 오므로 `PASSWORD=Welcome` 은 그대로 가린다. 쉼표(`{ API_KEY: Value, … }`)는
737
+ * 위에 적어 둔 대로 물건 속 이름씨라 계속 가린다 — 검사 「물건 안의 이름씨는 여전히 가린다」.
738
+ */
739
+ if (/^[A-Z][A-Za-z]+$/.test(값C) && /^[ \t]*;/.test(전체.slice(값자리 + 값C.length))) return m;
613
740
  }
614
741
  return 타입사이 === undefined
615
742
  ? `${이름}${민사이}${여는 ?? ''}${표('환경변수')}${닫는 ?? ''}`
616
743
  : `${이름}${타입사이}${타입여는 ?? ''}${표('환경변수')}${타입닫는 ?? ''}`;
617
744
  },
618
745
  },
746
+ {
747
+ /*
748
+ * ── 명령 **앞에 붙여 적은** 환경변수 ─────────────────────────────────
749
+ *
750
+ * NPM_TOKEN=abc123456789 npm ci
751
+ * DB_PASSWORD=s3cr3tpw psql -h db
752
+ * GITHUB_TOKEN=mytoken12345 gh pr list
753
+ *
754
+ * 위 환경변수 규칙은 이걸 **하나도** 안 가렸다. 이름은 멀쩡히 맞는데
755
+ * 맨 값의 끝을 `(?=[ \t]*(?:[;,)\]}\r\n#]|$))` 로 못박아 둔 탓이다 —
756
+ * 빈칸 뒤에 낱말이 하나만 와도 그 갈래가 통째로 죽는다. 그래서
757
+ * `NPM_TOKEN=abc123456789` 한 줄은 가려지는데, 그 뒤에 `npm ci` 를
758
+ * 붙이는 순간 안 가려졌다.
759
+ *
760
+ * 하필 이 모양이 감사기록의 첫머리에 적혀 있는 것이다 — safety/audit.js
761
+ * 머리말이 「모델이 `DEEL_KEY_WORK=… curl …` 같은 명령을 부른 그 줄이
762
+ * 감사기록에 그대로 적힌다」 고 이 꼴을 콕 집어 적어 뒀다. 우리 열쇠
763
+ * (`DEEL_*`)만 아는 값 목록으로 지워지고 있었고, 남의 토큰은 그대로
764
+ * 모델에게 가고 `.deel/sessions/*.jsonl` 과 `.deel/audit.jsonl` 에
765
+ * 남았다. 감사기록은 보안 담당자에게 그대로 넘기는 파일이다.
766
+ *
767
+ * 화면에는 아무 표시도 안 났다. `가린것` 이 빈 배열이라 가렸다는 말도
768
+ * 봤다는 말도 안 나온다 — 가리개가 「깨끗합니다」 라고 답한 셈이다.
769
+ *
770
+ * 끝을 안 못박고 **빈칸 + 낱말**이 뒤따를 때만 본다. 위 규칙과 겹치지
771
+ * 않는다(그쪽은 그 자리에서 끝나는 것만 본다). 이름은 셸 관례대로
772
+ * 대문자만 받는다 — 소문자까지 받으면 코드 속 `monkey=…` 같은 것이
773
+ * 걸린다.
774
+ *
775
+ * 따옴표에 싸인 값(`NPM_TOKEN="abc…" npm ci`)은 위 규칙이 이미 가린다 —
776
+ * 그쪽 따옴표 갈래에는 끝 못박기가 없다. 그래서 여기서는 **맨 값만**
777
+ * 본다. 두 벌로 보면 겹쳐서 세는 자리가 또 생긴다.
778
+ *
779
+ * 앞머리 없는 `TOKEN=` · `SECRET=` 도 받는다 — 앞머리를 요구해서 샜다
780
+ * (2.0.0 2차 리뷰, `.env` 규칙의 17차 리뷰와 같은 구멍). 민 이름에서 빼는
781
+ * 것도 그쪽과 같다: 민 `KEY` 는 안 보고, `PWD` 는 밑줄이 앞에 있어야 본다.
782
+ */
783
+ id: '환경변수',
784
+ re: /(^|[\s;&|(])((?:[A-Z][A-Z0-9_]*)?(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|CREDENTIALS)|[A-Z][A-Z0-9_]*(?:_PWD|_PASS|KEY))=(?!«)[^\s"'`;&|]{4,}(?=\s+\S)/g,
785
+ 바꾸기: (m, 앞, 이름) => `${앞}${이름}=${표('환경변수')}`,
786
+ },
619
787
  ];
620
788
 
621
789
  /**
@@ -662,7 +830,7 @@ export function 가리기(글, { 열쇠들 = [] } = {}) {
662
830
  s = s.replace(g.re, (...인자) => (g.바꾸기 ? g.바꾸기(...인자) : 표(g.id)));
663
831
  }
664
832
 
665
- return { 글: s, 가린것: 남은표세기(s) };
833
+ return { 글: s, 가린것: 남은표세기(s, 원본) };
666
834
  }
667
835
 
668
836
  /*
@@ -672,12 +840,33 @@ export function 가리기(글, { 열쇠들 = [] } = {}) {
672
840
  * 가리고 헤더가 그 위를 다시 가린다. 바꾼 횟수로 세면 "2군데를 가렸습니다"
673
841
  * 가 되는데 글에는 표가 하나뿐이다. 사람이 세어 보면 안 맞는다.
674
842
  */
675
- function 남은표세기(글) {
843
+ /*
844
+ * ── 들어올 때 이미 있던 표는 **내가 가린 것이 아니다** ──────────────────
845
+ *
846
+ * 결과의 표를 세기만 했더니, 앞 단계가 이미 가려 놓은 표까지 제 몫으로
847
+ * 셌다. 한 판에 두 번 지나는 자리가 실제로 있다 — 도구후 훅의 출력을
848
+ * 가리고(loop.js), 그 가린 글을 도구 결과에 붙인 다음, 그 전체를 다시
849
+ * 가린다. 둘째 판은 아무것도 안 바꾸는데 「1군데 가렸습니다」 라고 답한다.
850
+ *
851
+ * 그러면 모델은 같은 비밀 하나에 「가렸습니다」 를 두 번 듣고, 감사기록에는
852
+ * 그 비밀이 **엉뚱한 도구 이름으로** 한 줄 더 남는다. 파일을 읽는 길에서는
853
+ * 더 나쁘다 — 표 글자가 그냥 적혀 있는 파일 하나로 「비밀로 보이는 값이
854
+ * 대화에 들어갔습니다」 가 뜬다.
855
+ *
856
+ * 그래서 들어온 글에 있던 표는 빼고 센다. 이 함수 머리말이 「결과를 보고
857
+ * 센다」 고 한 까닭(규칙끼리 겹쳐서 바꾼 횟수는 못 믿는다)은 그대로다 —
858
+ * 겹침은 결과로 세서 풀고, 앞 판의 몫은 빼서 푼다.
859
+ */
860
+ function 남은표세기(글, 원본 = '') {
676
861
  const 센것 = new Map();
677
862
  for (const m of String(글).matchAll(/«가림:([^»]+)»/g)) {
678
863
  센것.set(m[1], (센것.get(m[1]) ?? 0) + 1);
679
864
  }
680
- return [...센것].map(([종류, 몇번]) => ({ 종류, 몇번 }));
865
+ for (const m of String(원본).matchAll(/«가림:([^»]+)»/g)) {
866
+ const 있던것 = 센것.get(m[1]);
867
+ if (있던것) 센것.set(m[1], 있던것 - 1);
868
+ }
869
+ return [...센것].filter(([, 몇번]) => 몇번 > 0).map(([종류, 몇번]) => ({ 종류, 몇번 }));
681
870
  }
682
871
 
683
872
  /**
@@ -732,16 +921,31 @@ export function 아는열쇠(것들) {
732
921
  * 그래서 물음표 뒤의 **값은 전부** 가린다. 이름은 남긴다 — 어떤 것이 붙어
733
922
  * 있는지는 사람이 봐야 하고, 이름만으로는 아무것도 못 연다.
734
923
  *
735
- * 예외를 두지 않는다. `api-version` 만 남기고 나머지를 가리는 식으로 목록을
736
- * 관리하기 시작하면, 새로 나온 이름 하나가 조용히 평문으로 새어 나간다.
924
+ * ── 예외는 `api-version` 하나뿐이고, 그 목록은 안 늘린다 ─────────────────
925
+ *
926
+ * 여기 「예외를 두지 않는다」 고 적혀 있었는데 아래 코드는 처음부터 `api-version`
927
+ * 을 남기고 있었다. 두 말 중 **코드가 맞다** — 그 값은 Azure 가 정한 날짜라
928
+ * 비밀이 아니고, 그게 안 보이면 Azure 의 400 을 아무도 못 푼다(1등 실패 원인이
929
+ * 판 번호다). 그래서 말을 코드에 맞춘다.
930
+ *
931
+ * 다만 목록으로 만들지는 않는다. 「이것도 비밀 아니잖아」 로 한 줄씩 늘리기
932
+ * 시작하면 새로 나온 이름 하나가 조용히 평문으로 새어 나간다. 값이 아니라
933
+ * **판 번호**인 이 한 칸으로 끝이다.
737
934
  */
738
935
  export function 주소가리기(주소) {
739
- const s = String(주소 ?? '');
740
- const i = s.indexOf('?');
741
- if (i < 0) return s;
742
- const 앞 = s.slice(0, i);
743
- const 뒤 = s.slice(i + 1);
744
- const 가린 = 뒤.split('&').map((쌍) => {
936
+ /*
937
+ * 주소 **앞머리**의 사람:열쇠(`http://user:pw@proxy:8080`)도 가린다. 물음표 뒤만 봐서, 사내 프록시
938
+ * 주소가 doctor 의 프록시 줄·진단 보고서·대화 기록 base 에 암호째 적혔다(2.0.0 6회차 · Gemini 비밀6).
939
+ * 호스트·포트는 남긴다 — 어디로 가는지는 사람이 봐야 한다.
940
+ *
941
+ * 자격은 경로·물음표·# 앞의 **마지막 `@`** 까지다. 첫 `@` 에서 끊었더니 비밀번호에 날것 `@` 가 든 주소
942
+ * (`http://u:pw@tail@proxy`)에서 `tail` 이 호스트 앞에 그대로 적혔다 — 주소를 푸는 쪽(WHATWG URL · curl)은
943
+ * 마지막 `@` 를 자격 끝으로 본다 (6회차 Gemini 가림6r T3).
944
+ */
945
+ const s = String(주소 ?? '').replace(/^([a-z][a-z0-9+.-]*:\/\/)[^\s/?#]*@/i, '$1«가림»@');
946
+
947
+ /** `이름=값&이름=값` 한 토막의 값만 가린다. 물음표 뒤와 `#` 뒤가 같은 규칙을 쓴다. */
948
+ const 칸가리기 = (것) => 것.split('&').map((쌍) => {
745
949
  const j = 쌍.indexOf('=');
746
950
  if (j < 0) return 쌍;
747
951
  const 이름 = 쌍.slice(0, j);
@@ -749,7 +953,27 @@ export function 주소가리기(주소) {
749
953
  if (/^api-version$/i.test(이름)) return 쌍;
750
954
  return `${이름}=«가림»`;
751
955
  }).join('&');
752
- return `${앞}?${가린}`;
956
+
957
+ /*
958
+ * 프래그먼트(`#…`)를 먼저 떼어 낸다.
959
+ *
960
+ * 여기가 물음표 뒤를 통째로 한 덩이로 봤다. 그래서 `#` 뒤가 **마지막 칸의 값**
961
+ * 으로 같이 먹혀서 `?code=X#section` 이 `?code=«가림»` 이 됐다 — 프래그먼트가
962
+ * 소리 없이 사라진 것이다. 이 함수의 답은 화면·진단 보고서·대화 기록의 base 로
963
+ * 그대로 적히므로, 사람이 그걸 복사해 붙이면 원래와 다른 자리로 간다.
964
+ *
965
+ * 되살리되 그냥 붙여 놓지는 않는다. OAuth 암묵 흐름은 토큰을 `#access_token=`
966
+ * 으로 준다 — 물음표 뒤와 **같은 규칙**을 먹인다. 아니면 이번에는 물음표 없는
967
+ * 주소의 프래그먼트가 평문으로 남는다(그게 전에 있던 또 하나의 구멍이다).
968
+ * `#` 뒤는 전부 프래그먼트다 — 그 안의 `?` 는 물음표가 아니다(WHATWG · curl).
969
+ */
970
+ const 샾 = s.indexOf('#');
971
+ const 몸 = 샾 < 0 ? s : s.slice(0, 샾);
972
+ const 조각 = 샾 < 0 ? '' : `#${칸가리기(s.slice(샾 + 1))}`;
973
+
974
+ const i = 몸.indexOf('?');
975
+ if (i < 0) return `${몸}${조각}`;
976
+ return `${몸.slice(0, i)}?${칸가리기(몸.slice(i + 1))}${조각}`;
753
977
  }
754
978
 
755
979
  /**
@@ -780,7 +1004,9 @@ export const 가릴도구 = new Set(['Bash', 'Verify', 'Jobs', 'WebFetch']);
780
1004
  * @param {{바깥?: boolean}} o 바깥 = 이 연결이 이 컴퓨터 밖으로 나가나
781
1005
  */
782
1006
  export function 가릴까(도구이름, { 바깥 = false } = {}) {
783
- return 가릴도구.has(도구이름) || 바깥 === true;
1007
+ // MCP 도구의 결과는 **남의 프로그램이 낸 출력**이다 — 명령 출력과 같은 자리라 같이 가린다.
1008
+ // 이름 목록(가릴도구)에는 붙은 서버마다 달라지는 이 이름들을 적어 둘 수가 없다.
1009
+ return 가릴도구.has(도구이름) || String(도구이름 ?? '').startsWith('mcp__') || 바깥 === true;
784
1010
  }
785
1011
 
786
1012
  /*
@@ -36,7 +36,8 @@
36
36
  //
37
37
  // PWD 유닉스에서 지금 폴더다. 빼면 거의 모든 셸 스크립트가 부서진다.
38
38
  // AUTH SSH_AUTH_SOCK 이 걸린다. 그건 열쇠가 아니라 통로 이름이고,
39
- // 빼면 `git push` 가 안 된다.
39
+ // 빼면 `git push` 가 안 된다. 그래서 마디표에는 없고, **마지막 마디**일
40
+ // 때만 건다(아래 끝에만마디 — NPM_CONFIG__AUTH 는 비밀이다).
40
41
  //
41
42
  // ── 그리고 뺀 것은 말한다 ──────────────────────────────────────────────
42
43
  //
@@ -65,6 +66,60 @@ export const 비밀마디 = Object.freeze([
65
66
 
66
67
  const 마디표 = new Set(비밀마디);
67
68
 
69
+ /*
70
+ * ── `_` 로만 가르면 새는 이름들 (사냥5 M6) ─────────────────────────────
71
+ *
72
+ * 위 마디표는 `_` 로 나눈 조각이 **통째로** 같을 때만 걸렸다. 실제 이름은 그렇게
73
+ * 얌전하지 않아서, 이것들이 `env` 한 줄에 값째 나왔다 —
74
+ *
75
+ * PGPASSWORD AWS_SECRETACCESSKEY PRIVATEKEY 붙여 씀
76
+ * GITHUB-TOKEN 붙임표
77
+ * githubToken npm_config__authToken 낙타 꼴
78
+ * MYSQL_PWD GH_PAT CI_JOB_JWT 짧은 이름
79
+ * DATABASE_URL=postgres://u:비밀번호@h/db 값에 든 자격
80
+ *
81
+ * 그래서 셋을 더 본다.
82
+ *
83
+ * 1) 마디를 `_` · `-` · `.` 과 **낙타 꼴 경계**(소문자·숫자 다음 대문자)로 가른다.
84
+ * 2) 한 마디가 아래 끝말로 **끝나면** 건다. `*KEY*` 같은 통무늬가 아니라 끝말이다 —
85
+ * KEY 는 끝말에 안 넣고 APIKEY·ACCESSKEY 처럼 붙어 쓰이는 꼴만 넣는다. KEY 로
86
+ * 끝나기만 보면 MONKEY_PATCH · TURKEY_TZ 가 같이 걸린다(위 머리말의 그 오작동).
87
+ * 3) PWD · PAT · JWT · AUTH 는 **다른 마디 뒤 마지막 마디**일 때만 건다. `PWD` 혼자는 지금
88
+ * 폴더다(빼면 셸 스크립트가 부서진다 — 위 머리말). `MYSQL_PWD` 는 비밀번호다.
89
+ * JWT_ISSUER · COMPAT_LAYER 같은 설정 이름은 안 건다.
90
+ * AUTH 는 6회차(Gemini 모드6p Q2)에 넣었다 — npm 의 `NPM_CONFIG__AUTH` 는 base64 사용자:비밀번호인데
91
+ * AUTH 를 통째로 뺀 탓에 `env` 한 줄에 실렸다. 끝 마디만 보므로 `SSH_AUTH_SOCK`(끝이 SOCK)은 그대로 산다.
92
+ */
93
+ const 비밀끝말 = Object.freeze([
94
+ 'PASSWORD', 'PASSWD', 'PASSPHRASE',
95
+ 'TOKEN', 'TOKENS', 'SECRET', 'SECRETS', 'CREDENTIAL', 'CREDENTIALS',
96
+ 'APIKEY', 'ACCESSKEY', 'PRIVATEKEY', 'SECRETKEY', 'AUTHKEY', 'MASTERKEY', 'SIGNINGKEY', 'ENCRYPTIONKEY',
97
+ ]);
98
+ const 끝에만마디 = new Set(['PWD', 'PAT', 'JWT', 'AUTH']);
99
+
100
+ /** 이름을 마디로 — `_` `-` `.` 과 낙타 꼴 경계. 대문자로 맞춰 돌려준다. */
101
+ function 이름마디들(이름) {
102
+ return String(이름 ?? '')
103
+ .split(/[_\-.]+|(?<=[a-z0-9])(?=[A-Z])/)
104
+ .filter(Boolean)
105
+ .map((x) => x.toUpperCase());
106
+ }
107
+
108
+ /*
109
+ * 값에 `사용자:비밀번호@` 가 든 주소인가.
110
+ *
111
+ * `://` 바로 뒤 자격 자리만 본다 — 경로에 든 `a:b@c` 나 포트(`host:8443`)는 자격이
112
+ * 아니다. 비밀번호 칸이 빈 것(`http://user@host`)은 안 건다. 사내 프록시에 자격을
113
+ * 넣어 둔 것(`http://id:pw@proxy:8080`)도 빠진다 — 그 비밀번호는 흔히 사내 계정
114
+ * 비밀번호라 대화에 실려 나가면 안 되고, 필요하면 남길것 에 적어 되살린다(아래 뺀것 안내).
115
+ */
116
+ const 자격든주소 = /^[a-z][a-z0-9+.-]*:\/\/[^\s/?#@:]*:[^\s/?#@]+@/i;
117
+
118
+ /** 이 값이 자격이 든 주소인가. 이름으로는 못 거르는 DATABASE_URL 같은 것을 거른다. */
119
+ export function 비밀값인가(값) {
120
+ return typeof 값 === 'string' && 자격든주소.test(값.trim());
121
+ }
122
+
68
123
  /**
69
124
  * 이 환경변수 이름이 열쇠처럼 생겼나.
70
125
  *
@@ -77,7 +132,11 @@ export function 비밀환경인가(이름, { 남길것 = [] } = {}) {
77
132
  // 사람이 되살리라고 적은 것은 무늬에 걸려도 그대로 넘긴다. 사람이 제
78
133
  // 환경을 우리보다 잘 안다 — 우리는 기본값만 정한다.
79
134
  if (남길것.some((x) => String(x ?? '').toUpperCase() === n)) return false;
80
- return n.split('_').some((조각) => 마디표.has(조각));
135
+ if (n.split('_').some((조각) => 마디표.has(조각))) return true;
136
+ // 붙여 쓴 이름·낙타 꼴·짧은 끝 마디 (위 「`_` 로만 가르면 새는 이름들」)
137
+ const 마디들 = 이름마디들(이름);
138
+ if (마디들.some((조각) => 마디표.has(조각) || 비밀끝말.some((끝) => 조각.endsWith(끝)))) return true;
139
+ return 마디들.length > 1 && 끝에만마디.has(마디들[마디들.length - 1]);
81
140
  }
82
141
 
83
142
  /**
@@ -107,9 +166,11 @@ export function 남길것읽기(cfg) {
107
166
  export function 셸환경(env = process.env, { 남길것 = [] } = {}) {
108
167
  const out = { ...env };
109
168
  const 뺀것 = [];
169
+ const 남기라함 = (k) => 남길것.some((x) => String(x ?? '').toUpperCase() === String(k).toUpperCase());
110
170
  for (const k of Object.keys(out)) {
111
171
  if (열쇠환경인가(k)) { delete out[k]; continue; } // 우리 것은 말없이 뺀다
112
- if (비밀환경인가(k, { 남길것 })) { delete out[k]; 뺀것.push(k); }
172
+ // 이름이 멀쩡해도 값이 자격 든 주소면 뺀다 (비밀값인가 머리말). 되살리는 길은 같다.
173
+ if (비밀환경인가(k, { 남길것 }) || (비밀값인가(out[k]) && !남기라함(k))) { delete out[k]; 뺀것.push(k); }
113
174
  }
114
175
  뺀것.sort();
115
176
  return { env: out, 뺀것 };