deel-local-cli 1.20.12 → 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 -77
  2. package/README.md +119 -80
  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 +62 -13
  7. package/src/acp/serve.js +338 -30
  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 +506 -32
  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 +40 -9
  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
@@ -32,7 +32,14 @@ const LOCAL = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0', '::']);
32
32
  * 169.254.* (링크 로컬 — 클라우드 메타데이터가 여기 산다)와 IPv6 의 fe80:: · fc00::/7 도
33
33
  * 이 컴퓨터·사내망으로 친다.
34
34
  */
35
- export const isLocalHost = (h) => {
35
+ /*
36
+ * 철자를 편다 — 끝 점을 떼고, 대괄호를 벗기고, ::ffff: 로 싼 IPv4 는 점 네 개 꼴로.
37
+ *
38
+ * 아래 isLocalHost · 루프백인가 · 안쪽주소인가 가 **같은 펴기**를 쓴다. 표마다 따로
39
+ * 펴면 한쪽만 `[::ffff:127.0.0.1]` 을 알아보는 모양이 다시 생긴다 — backend/proxy.js 가
40
+ * 제 잣대(`/^127\./`)를 따로 들고 있다가 딱 그렇게 됐다(사냥4 W15).
41
+ */
42
+ function 철자펴기(h) {
36
43
  let s = String(h ?? '').trim().toLowerCase().replace(/\.$/, '').replace(/^\[|\]$/g, '');
37
44
  if (s.startsWith('::ffff:')) {
38
45
  const 뒤 = s.slice(7);
@@ -41,12 +48,158 @@ export const isLocalHost = (h) => {
41
48
  ? [hex[1], hex[2]].flatMap((x) => { const n = parseInt(x, 16); return [n >> 8, n & 255]; }).join('.')
42
49
  : 뒤;
43
50
  }
51
+ return s;
52
+ }
53
+
54
+ export const isLocalHost = (h) => {
55
+ const s = 철자펴기(h);
44
56
  if (LOCAL.has(s)) return true;
45
- if (/^(127|10|0)\./.test(s) || /^192\.168\./.test(s) || /^172\.(1[6-9]|2\d|3[01])\./.test(s) || /^169\.254\./.test(s)) return true;
57
+ const 넷 = 숫자주소펴기(s);
58
+ if (넷) {
59
+ const [a, b] = 넷.split('.').map(Number);
60
+ if (a === 127 || a === 10 || a === 0) return true;
61
+ if (a === 192 && b === 168) return true;
62
+ if (a === 172 && b >= 16 && b <= 31) return true;
63
+ if (a === 169 && b === 254) return true;
64
+ return false; // 그 밖의 숫자 주소는 바깥이다
65
+ }
46
66
  if (/^fe[89ab][0-9a-f]:/.test(s) || /^f[cd][0-9a-f]{2}:/.test(s)) return true;
47
67
  return false;
48
68
  };
49
69
 
70
+ /*
71
+ * 이 컴퓨터 **자신**인가 — 사내망은 빼고.
72
+ *
73
+ * 프록시 고르기(backend/proxy.js)가 「루프백은 언제나 직접」 을 가를 때 쓴다. 거기는
74
+ * `/^127\./` 로 글자 앞머리를 봐서 두 쪽으로 틀렸다(사냥4 W15). `127.example.com` 은
75
+ * **이름인데** 루프백으로 읽혀 프록시를 건너뛰었고 — 바깥으로 직접 못 나가는 사내에서는
76
+ * 그대로 「연결 실패」 다 — 진짜 루프백인 `[::ffff:127.0.0.1]` 은 프록시로 돌아 나갔다.
77
+ * 아래 숫자주소펴기 머리말이 여기서 먼저 고친 바로 그 탈이라, 잣대를 옮기지 않고 내준다.
78
+ */
79
+ export const 루프백인가 = (h) => {
80
+ const s = 철자펴기(h);
81
+ if (s === 'localhost' || s === '::1' || s === '0.0.0.0') return true;
82
+ const 넷 = 숫자주소펴기(s);
83
+ return !!넷 && 넷.startsWith('127.');
84
+ };
85
+
86
+ /*
87
+ * ── 웹이 닿으면 안 되는 안쪽 — isLocalHost 보다 **넓다** ───────────────────
88
+ *
89
+ * isLocalHost 는 두 일을 한다. WebFetch 가 안 읽을 곳이고, 동시에 **봉인(offline)이
90
+ * 그래도 열어 주는 곳**이다(아래 checkUrl · safety/runmode.js 의 바깥인가). 그래서 막을
91
+ * 곳을 거기에 더하면 막는 쪽은 조여지지만 봉인 쪽은 **풀린다** — 통신사 CGNAT(100.64/10)
92
+ * 너머가 「이 안」 이 되어 봉인 중에도 나간다. 그건 고치는 것이 아니라 다른 구멍이다.
93
+ *
94
+ * 그런데 막는 쪽에는 빈 데가 있었다(사냥4 W9). 되돌림 검사가 그리로 따라갔다:
95
+ *
96
+ * 100.100.100.200 알리바바 클라우드 메타데이터. 100.64/10(공유 주소) 안에 산다
97
+ * fec0::/10 옛 사이트로컬. 사내 IPv6 로 아직 쓰는 곳이 있다
98
+ * 64:ff9b::a9fe:a9fe NAT64 에 싼 169.254.169.254 — 벗기면 메타데이터다
99
+ * ::7f00:1 IPv4 호환 꼴에 싼 127.0.0.1
100
+ *
101
+ * 메타데이터에 닿으면 임시 자격증명이 모델에게 실린다. 그래서 **막는 쪽만** 넓힌 표를
102
+ * 따로 둔다 — WebFetch(tools/webfetch.js)와 사설로풀리나 가 이걸 쓰고, 봉인은 계속
103
+ * isLocalHost 를 쓴다. 벤치마크(198.18/15)·멀티캐스트·예약(224/4 · 240/4)도 웹 문서가
104
+ * 사는 자리가 아니라 같이 막는다.
105
+ */
106
+ export const 안쪽주소인가 = (h) => {
107
+ if (isLocalHost(h)) return true;
108
+ const s = 철자펴기(h).replace(/%.*$/, ''); // fe80::1%eth0 같은 영역 꼬리는 뗀다
109
+ const 넷 = 숫자주소펴기(s);
110
+ if (넷) {
111
+ const [a, b] = 넷.split('.').map(Number);
112
+ if (a === 100 && b >= 64 && b <= 127) return true; // 공유 주소(CGNAT) — 알리바바 메타데이터
113
+ if (a === 198 && (b === 18 || b === 19)) return true; // 벤치마크
114
+ if (a >= 224) return true; // 멀티캐스트 · 예약 · 브로드캐스트
115
+ return false;
116
+ }
117
+ const 여덟 = 여섯펴기(s);
118
+ if (!여덟) return false;
119
+ if ((여덟[0] & 0xffc0) === 0xfec0) return true; // fec0::/10 옛 사이트로컬
120
+ if ((여덟[0] & 0xff00) === 0xff00) return true; // ff00::/8 멀티캐스트
121
+ // IPv4 를 싼 꼴은 벗겨서 다시 본다 — 싼 채로 보면 「바깥 IPv6」 으로 읽힌다.
122
+ const 끝넷 = () => `${여덟[6] >> 8}.${여덟[6] & 255}.${여덟[7] >> 8}.${여덟[7] & 255}`;
123
+ if (여덟[0] === 0x64 && 여덟[1] === 0xff9b && 여덟.slice(2, 6).every((x) => x === 0)) return 안쪽주소인가(끝넷()); // NAT64
124
+ if (여덟.slice(0, 6).every((x) => x === 0)) return 안쪽주소인가(끝넷()); // IPv4 호환
125
+ if (여덟[0] === 0x2002) return 안쪽주소인가(`${여덟[1] >> 8}.${여덟[1] & 255}.${여덟[2] >> 8}.${여덟[2] & 255}`); // 6to4
126
+ return false;
127
+ };
128
+
129
+ /**
130
+ * IPv6 글을 16비트 마디 여덟 개로. 못 읽으면 null.
131
+ * `::` 줄임과 끝에 붙은 점 넷 IPv4(`::ffff:1.2.3.4` · `64:ff9b::1.2.3.4`)를 받는다.
132
+ * 프록시의 NO_PROXY 대역 견주기(backend/proxy.js)도 이걸 쓴다.
133
+ */
134
+ export function 여섯펴기(글) {
135
+ let s = String(글 ?? '').trim().toLowerCase().replace(/^\[|\]$/g, '').replace(/%.*$/, '');
136
+ if (!s.includes(':')) return null;
137
+ const 점넷 = /(\d+\.\d+\.\d+\.\d+)$/.exec(s);
138
+ if (점넷) {
139
+ const 펴진 = 숫자주소펴기(점넷[1]);
140
+ if (!펴진) return null;
141
+ const [a, b, c, d] = 펴진.split('.').map(Number);
142
+ s = s.slice(0, 점넷.index) + `${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;
143
+ }
144
+ const 갈림 = s.split('::');
145
+ if (갈림.length > 2) return null;
146
+ const 마디로 = (x) => (x ? x.split(':') : []);
147
+ const 앞 = 마디로(갈림[0]);
148
+ const 뒤 = 갈림.length === 2 ? 마디로(갈림[1]) : [];
149
+ const 빈칸 = 8 - 앞.length - 뒤.length;
150
+ if (갈림.length === 2 ? 빈칸 < 1 : 빈칸 !== 0) return null;
151
+ const 모두 = [...앞, ...Array(갈림.length === 2 ? 빈칸 : 0).fill('0'), ...뒤];
152
+ if (모두.some((x) => !/^[0-9a-f]{1,4}$/.test(x))) return null;
153
+ return 모두.map((x) => parseInt(x, 16));
154
+ }
155
+
156
+ /*
157
+ * ── 「숫자로 적힌 주소」 는 점 넷만이 아니다 ─────────────────────────────
158
+ *
159
+ * 여기는 `/^(127|10|0)\./` 처럼 **글자 앞머리**로 봤다. 두 쪽으로 틀렸다.
160
+ *
161
+ * 한쪽은 **덜 막았다.** 운영체제의 주소 푸는 자(getaddrinfo/inet_aton)는
162
+ * 점 넷 말고도 여러 꼴을 받는다. 전부 같은 자리를 가리킨다:
163
+ *
164
+ * 169.254.169.254 ← 클라우드 메타데이터. 임시 자격증명이 나온다
165
+ * 2852039166 ← 같은 주소를 32비트 한 덩이로
166
+ * 0251.0376.0251.0376 ← 8진
167
+ * 0xA9.0xFE.0xA9.0xFE ← 16진
168
+ * 127.1 ← 마디를 줄인 꼴
169
+ *
170
+ * 아래 사설로풀리나() 는 `/^[0-9.]+$/` 면 「위 isLocalHost 가 이미 본다」 며
171
+ * DNS 조회까지 건너뛰었다. 보지 않았다. 그래서 숫자 한 덩이로 적으면 글자
172
+ * 검사도 이름 풀기도 둘 다 지나갔다.
173
+ *
174
+ * 다른 쪽은 **더 막았다.** `/^(127|10|0)\./` 는 **이름**에도 걸린다 —
175
+ * `0.gravatar.com` · `0.pool.ntp.org` · `10.example.com` 이 전부 「사내」 로
176
+ * 분류됐다. WebFetch 에서는 까닭 없는 거절로 보이고(시끄럽다), runmode 의
177
+ * 바깥인가() 에서는 조용하다 — 게이트웨이 주소가 그런 이름이면
178
+ * `바깥 = false` 가 되어 **봉인(offline) 중에도** 나갈 수 있다고 답한다.
179
+ *
180
+ * 그래서 숫자 주소는 **펴서** 보고, 이름은 숫자 규칙을 아예 안 태운다.
181
+ * inet_aton 규칙 그대로다 — 마디 1~4개, 마지막 마디가 남은 자리를 다 먹는다.
182
+ */
183
+ export function 숫자주소펴기(s) {
184
+ const 마디 = String(s).split('.');
185
+ if (마디.length < 1 || 마디.length > 4) return null;
186
+ const 값들 = [];
187
+ for (const m of 마디) {
188
+ if (!/^(?:0[xX][0-9a-fA-F]{1,8}|0[0-7]*|[1-9]\d*)$/.test(m)) return null;
189
+ const n = /^0[xX]/.test(m) ? parseInt(m, 16) : (/^0[0-7]+$/.test(m) ? parseInt(m, 8) : Number(m));
190
+ if (!Number.isFinite(n) || n < 0) return null;
191
+ 값들.push(n);
192
+ }
193
+ const 앞 = 값들.slice(0, -1);
194
+ const 끝 = 값들[값들.length - 1];
195
+ if (앞.some((x) => x > 255)) return null;
196
+ const 남은칸 = 4 - 앞.length;
197
+ if (끝 >= 2 ** (8 * 남은칸)) return null;
198
+ const out = [...앞];
199
+ for (let i = 남은칸 - 1; i >= 0; i--) out.push((끝 / 2 ** (8 * i)) & 255);
200
+ return out.join('.');
201
+ }
202
+
50
203
  /**
51
204
  * 이 이름이 **실제로 어디로 풀리나.** 하나라도 사내·로컬이면 막는다.
52
205
  *
@@ -76,8 +229,9 @@ export const isLocalHost = (h) => {
76
229
  export async function 사설로풀리나(hostname) {
77
230
  const h = String(hostname ?? '').trim();
78
231
  if (!h) return null;
79
- // 숫자로 적힌 주소는 풀 것이 없다 — 위 isLocalHost 가 이미 본다.
80
- if (/^[0-9.]+$/.test(h) || h.includes(':') || h.startsWith('[')) return null;
232
+ // 숫자로 적힌 주소는 풀 것이 없다 — 부르는 쪽(WebFetch)이 안쪽주소인가 로 이미 본다.
233
+ // 「숫자로 적힌」 을 글자 앞머리가 아니라 **펴 보고** 가른다(숫자주소펴기 머리말).
234
+ if (숫자주소펴기(h) || h.includes(':') || h.startsWith('[')) return null;
81
235
  let 주소들;
82
236
  try {
83
237
  const { lookup } = await import('node:dns/promises');
@@ -86,7 +240,9 @@ export async function 사설로풀리나(hostname) {
86
240
  // 못 풀면 어차피 못 붙는다. 여기서 막을 것도 없다.
87
241
  return null;
88
242
  }
89
- const 걸린것 = 주소들.map((a) => a.address).filter((a) => isLocalHost(a));
243
+ // 풀려 나온 주소도 **막는 표**(안쪽주소인가)로 본다. isLocalHost 로 보면 A 레코드 한 줄이
244
+ // 100.100.100.200(알리바바 메타데이터)을 가리킬 때 그대로 지나간다 (사냥4 W9).
245
+ const 걸린것 = 주소들.map((a) => a.address).filter((a) => 안쪽주소인가(a));
90
246
  return 걸린것.length ? { 주소들: 주소들.map((a) => a.address), 걸린것 } : null;
91
247
  }
92
248
 
@@ -95,6 +251,19 @@ function originOf(url) {
95
251
  return `${u.protocol}//${u.host}`;
96
252
  }
97
253
 
254
+ /*
255
+ * 거쳐 갈 프록시 주소에서 **호스트만** 꺼낸다. 못 읽으면 null.
256
+ *
257
+ * 프록시읽기(backend/proxy.js)는 `http://host:port` 꼴로 넘기지만, 여기까지 오는 길에
258
+ * 스킴 없는 `host:port` 가 섞여도 같은 답을 내야 한다 — 봉인을 가르는 자리라 「못 읽었다」 가
259
+ * 「검사 안 함」 이 되면 안 된다. 사람 이름·비밀번호는 여기서 떨어져 나간다.
260
+ */
261
+ function 경유호스트(거쳐) {
262
+ const s = String(거쳐 ?? '').trim();
263
+ if (!s) return null;
264
+ try { return new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(s) ? s : `http://${s}`).hostname; } catch { return null; }
265
+ }
266
+
98
267
  // 하나뿐인 문지기. 모듈 하나에 상태를 두는 것은 일부러다 —
99
268
  // 여기저기서 각자 예외를 두면 자물쇠가 아니게 된다.
100
269
  const gate = {
@@ -121,6 +290,17 @@ export function allowEndpoint(baseUrl) {
121
290
  for (const u of Array.isArray(baseUrl) ? baseUrl : [baseUrl]) {
122
291
  if (u) gate.allow.add(originOf(u));
123
292
  }
293
+ /*
294
+ * 잠깐 연 것(allowTemporarily)은 여기가 닫는 자리가 아니다. 통째로 비우면서 같이 쓸어
295
+ * 갔더니, 플러그인을 받는 동안 /model 로 갈아탄 것만으로 받던 곳이 「허용되지 않은
296
+ * 주소」 로 막혔다(8회차). 닫는 것은 잠깐 연 자가 받아 간 함수뿐이라, 아직 살아 있는
297
+ * 것은 도로 올린다. 「원래 열려 있었나」 는 이제 **새 목록** 기준이다 — 그래야 마지막이
298
+ * 닫을 때 새 연결을 실수로 안 닫는다.
299
+ */
300
+ for (const [o, 칸] of 잠깐연것) {
301
+ 칸.원래 = gate.allow.has(o);
302
+ gate.allow.add(o);
303
+ }
124
304
  return [...gate.allow];
125
305
  }
126
306
 
@@ -183,6 +363,27 @@ export function checkUrl(url, 거쳐 = null) {
183
363
  if (gate.offline && !local) {
184
364
  throw new NetBlocked(url, '오프라인 모드입니다 — 이 컴퓨터 밖으로는 나가지 않습니다.');
185
365
  }
366
+ /*
367
+ * ── 봉인은 **거쳐 가는 기계**도 본다 (8회차) ───────────────────────────
368
+ *
369
+ * 목적지만 봤다. isLocalHost 는 사내망(10.x · 192.168.x · 172.16–31.x)까지 「이 안」 으로
370
+ * 치는데, 프록시를 비켜 가는 잣대(backend/proxy.js 프록시비켜가나)는 **루프백만**이다.
371
+ * 그래서 `--offline` + LAN Ollama(192.168.0.50) + `HTTP_PROXY=proxy.사내.example:8080`
372
+ * 조합에서 목적지만 검사를 받고, 패킷이 실제로 닿는 첫 기계인 사내 프록시는 아무 검사도
373
+ * 안 받았다 — 게이트웨이 열쇠가 실린 요청이 봉인 중에 그대로 컴퓨터 밖으로 나갔다.
374
+ *
375
+ * 봉인이 막는 것은 「이 컴퓨터 밖」 이지 프록시라는 말이 아니다. 그래서 프록시에도 목적지와
376
+ * **같은 잣대**(isLocalHost)를 댄다 — 루프백 프록시(127.0.0.1:3128)를 거쳐 로컬 창구에
377
+ * 붙는 길은 그대로 열려 있고, 바깥으로 나가는 프록시만 막힌다. 주소를 못 읽으면 막는다
378
+ * (모를 때는 좁은 쪽). 탈 글에는 호스트만 싣는다 — 프록시 주소에는 비밀번호가 실려 온다.
379
+ */
380
+ if (gate.offline && 거쳐) {
381
+ const 경유 = 경유호스트(거쳐);
382
+ if (!경유 || !isLocalHost(경유)) {
383
+ throw new NetBlocked(url,
384
+ `오프라인 모드입니다 — 거쳐 갈 프록시(${경유 ?? '읽을 수 없는 주소'})가 이 컴퓨터 밖입니다.`);
385
+ }
386
+ }
186
387
 
187
388
  const origin = `${u.protocol}//${u.host}`;
188
389
  if (!gate.allow.has(origin)) {
@@ -195,6 +396,8 @@ export function checkUrl(url, 거쳐 = null) {
195
396
  const seen = gate.log.find((x) => x.origin === origin);
196
397
  if (seen) { seen.n++; if (거쳐) seen.via = 거쳐; }
197
398
  else gate.log.push({ origin, n: 1, local, via: 거쳐 ?? null });
198
- if (거쳐) 마지막경유 = { url: 거쳐 };
399
+ // 거친 것이 없으면 **비운다.** 놔두면 직접 나간 요청 뒤에도 심사서·화면이 옛 프록시를
400
+ // 「이 프록시를 거친다」 고 적는다 — 어디로 나갔는지 묻는 자리에서 제일 나쁜 거짓말이다(8회차).
401
+ 마지막경유 = 거쳐 ? { url: 거쳐 } : null;
199
402
  return true;
200
403
  }