deel-local-cli 1.4.1 → 1.4.3
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/README.en.md +95 -5
- package/README.md +87 -5
- package/package.json +1 -1
- package/src/pack/selfpack.js +9 -3
- package/src/preview/serve.js +18 -4
- package/src/safety/guard.js +4 -1
- package/src/tools/webfetch.js +20 -6
- package/src/ui/status.js +1 -1
package/README.en.md
CHANGED
|
@@ -64,6 +64,7 @@ Zero dependencies · Node 20+ · Exactly one place your source can go
|
|
|
64
64
|
## Contents
|
|
65
65
|
|
|
66
66
|
- [Why this exists](#why-this-exists)
|
|
67
|
+
- [What's different](#whats-different)
|
|
67
68
|
- [Quick start](#quick-start)
|
|
68
69
|
- [Where your data can go](#where-your-data-can-go)
|
|
69
70
|
- [Multiple local runtimes](#multiple-local-runtimes)
|
|
@@ -99,7 +100,7 @@ This page is the **summary**. Each section links to the detail behind it.
|
|
|
99
100
|
| [Speed and spend](docs/en/tuning.md) | Per-stage effort · the prefix cache · context length |
|
|
100
101
|
| [Safety and corporate review](docs/en/safety.md) | Undo · working scope · audit log · the review package |
|
|
101
102
|
| [Configuration](docs/en/config.md) · [Development](docs/en/develop.md) | Env vars · run flags · running the tests · folder layout |
|
|
102
|
-
| [Release notes](docs/en/releases.md) | [1.4.0](docs/en/releases.md#140) · [1.3.0](docs/en/releases.md#130) · [1.2.0](docs/en/releases.md#120) |
|
|
103
|
+
| [Release notes](docs/en/releases.md) | [1.4.3](docs/en/releases.md#143) · [1.4.2](docs/en/releases.md#142) · [1.4.1](docs/en/releases.md#141) · [1.4.0](docs/en/releases.md#140) · [1.3.0](docs/en/releases.md#130) · [1.2.0](docs/en/releases.md#120) |
|
|
103
104
|
|
|
104
105
|
---
|
|
105
106
|
|
|
@@ -128,6 +129,82 @@ deel audit # full review sheet
|
|
|
128
129
|
|
|
129
130
|
---
|
|
130
131
|
|
|
132
|
+
## What's different
|
|
133
|
+
|
|
134
|
+
A handful of coding agents can talk to a local model. Far fewer were
|
|
135
|
+
**redesigned inside for running locally.**
|
|
136
|
+
|
|
137
|
+
| | Other tools | deel |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `/undo` | rolls back files only — the conversation still believes it happened | rewinds **the conversation too** |
|
|
140
|
+
| Long conversations | pay a cost cloud tools never feel, unchanged, locally | ordering designed so the prefix cache **survives** |
|
|
141
|
+
| Edits on small models | fail on a single whitespace mismatch | 20%→**100%** success, 0 wrong-location edits |
|
|
142
|
+
| Korean models | unknown until you've run them | known **in advance** from public docs |
|
|
143
|
+
| "Done" | says so even for what wasn't checked | `/evidence` / `/export` — **unproven items included** |
|
|
144
|
+
| MCP · ACP | need an SDK | `child_process` + `JSON`, nothing else |
|
|
145
|
+
| Compliance paperwork | hand-written, drifts from reality | **generated by scanning the source** |
|
|
146
|
+
|
|
147
|
+
<br>
|
|
148
|
+
|
|
149
|
+
#### `/undo` rewinds the conversation along with the files
|
|
150
|
+
|
|
151
|
+
Roll back only the files and the model still believes it just made that edit
|
|
152
|
+
— it builds the next step on a premise that no longer holds, and nothing on
|
|
153
|
+
screen says otherwise. deel folds the messages back in lockstep with the
|
|
154
|
+
files. Folding can orphan a tool call, which the server answers with a 400,
|
|
155
|
+
so the same pass repairs the pairing (`repairToolPairs`).
|
|
156
|
+
|
|
157
|
+
#### Fixed the hidden reason local models get slower as a conversation grows
|
|
158
|
+
|
|
159
|
+
Ollama and llama.cpp only reuse computation when a request's prefix exactly
|
|
160
|
+
matches the last one — change one character near the front and everything
|
|
161
|
+
after it, the whole conversation, gets recomputed. A cloud API never pays
|
|
162
|
+
this cost, so cloud-first tools have no reason to care; someone running
|
|
163
|
+
locally feels it compound every turn. deel pushes what can change per turn
|
|
164
|
+
(mode, pins) to the **end** of the prompt and sends Ollama `keep_alive: 60m`
|
|
165
|
+
so the front stays cached. The ordering is enforced by a test
|
|
166
|
+
(`test/cache.test.js`).
|
|
167
|
+
|
|
168
|
+
#### Edits actually succeed on small models
|
|
169
|
+
|
|
170
|
+
Small local models often can't reproduce the exact whitespace of the string
|
|
171
|
+
they're trying to edit. The internal benchmark (`npm run bench`) measured
|
|
172
|
+
20% success for the old exact-match-only approach. The current approach
|
|
173
|
+
(stepped whitespace/indent tolerance) measures **100%** — and both approaches
|
|
174
|
+
land at **0** wrong-location edits. When it's ambiguous, it says so instead
|
|
175
|
+
of guessing.
|
|
176
|
+
|
|
177
|
+
#### Korean models are known before you've ever run them
|
|
178
|
+
|
|
179
|
+
EXAONE, HyperCLOVA X, Kanana, Midm, and Solar get whatever's verifiable from
|
|
180
|
+
public documentation (e.g., whether a model is a reasoning model) applied
|
|
181
|
+
before the first prompt. Other tools meet these models cold, and it takes a
|
|
182
|
+
dozen-plus turns of trial and error before anyone learns their quirks.
|
|
183
|
+
|
|
184
|
+
#### "Done" comes with a receipt, not just a claim
|
|
185
|
+
|
|
186
|
+
`/evidence` and `/export` record what wasn't verified alongside what was —
|
|
187
|
+
because the moment an AI coding tool is most likely to mislead someone is
|
|
188
|
+
exactly the moment it confidently says "done." `/export` is a self-contained
|
|
189
|
+
HTML file with zero outbound links, so it opens anywhere, including an
|
|
190
|
+
air-gapped network.
|
|
191
|
+
|
|
192
|
+
#### MCP and ACP, with no SDK
|
|
193
|
+
|
|
194
|
+
Both the Model Context Protocol and the Agent Client Protocol are just
|
|
195
|
+
newline-delimited JSON-RPC 2.0 over stdio. deel implements both with nothing
|
|
196
|
+
but `child_process` and `JSON` — proof that zero dependencies isn't a
|
|
197
|
+
capability given up, it's a capability that was never needed.
|
|
198
|
+
|
|
199
|
+
#### Compliance paperwork it doesn't hand-write
|
|
200
|
+
|
|
201
|
+
The import-review report, SBOM, and audit spec that `deel pack` produces are
|
|
202
|
+
generated **by scanning the actual source**, not typed by a person.
|
|
203
|
+
Hand-written paperwork eventually drifts from reality, and the moment a
|
|
204
|
+
reviewer catches one drifted claim, they stop trusting the rest of it.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
131
208
|
## Quick start
|
|
132
209
|
|
|
133
210
|
### The screen speaks English too
|
|
@@ -209,9 +286,18 @@ and never builds one for an address that is not on the allow-list.
|
|
|
209
286
|
GET only, zero-byte body. Private/loopback addresses refused. Every visit logged.
|
|
210
287
|
|
|
211
288
|
[C] Plugin fetch ─────── open only while /plugin install runs
|
|
289
|
+
|
|
290
|
+
[D] MCP servers ──────── a separate child process, someone else's program
|
|
291
|
+
Only starts if a human writes it into .deel/mcp.json. Off by default.
|
|
212
292
|
```
|
|
213
293
|
|
|
214
|
-
|
|
294
|
+
A, B, and C are requests deel makes itself, so each one can be filtered.
|
|
295
|
+
**D is different** — an MCP server is its own process; there is no way to see
|
|
296
|
+
what sockets it opens from the outside. So under `--offline`, instead of
|
|
297
|
+
filtering its requests, deel **never starts the server at all** — it doesn't
|
|
298
|
+
claim to have blocked what it can't actually see.
|
|
299
|
+
|
|
300
|
+
Pass `--offline` and **B, C, and D are all closed** — traffic stays on this machine.
|
|
215
301
|
|
|
216
302
|
```bash
|
|
217
303
|
deel --offline
|
|
@@ -226,8 +312,9 @@ The destination is printed at the top of every session:
|
|
|
226
312
|
Nothing is collected or transmitted. No telemetry, no usage stats, no crash reporting.
|
|
227
313
|
Conversation history, undo snapshots and config live only in `.deel/` inside your working folder.
|
|
228
314
|
|
|
229
|
-
> Verified by
|
|
230
|
-
> confirming that **not a single request reaches it** when it is not allow-listed
|
|
315
|
+
> Verified by 123 checks in `npm test` (network + web + mcp), including bringing up a real
|
|
316
|
+
> server and confirming that **not a single request reaches it** when it is not allow-listed,
|
|
317
|
+
> and that an MCP server **never starts** under `--offline`.
|
|
231
318
|
|
|
232
319
|
---
|
|
233
320
|
|
|
@@ -893,7 +980,10 @@ so one run tells you everything.
|
|
|
893
980
|
|
|
894
981
|
| Version | What changed |
|
|
895
982
|
|---|---|
|
|
896
|
-
| **[1.4.
|
|
983
|
+
| **[1.4.3](docs/en/releases.md#143)** | The README explains what's different, and the review report gets its missing line |
|
|
984
|
+
| [1.4.2](docs/en/releases.md#142) | 1.4.1 shipped before its own security fixes — this corrects that |
|
|
985
|
+
| [1.4.1](docs/en/releases.md#141) | No new features, only what was actually found and fixed — Windows abort, ReDoS, XSS |
|
|
986
|
+
| [1.4.0](docs/en/releases.md#140) | deel gets a face, speaks English, and sees meaning — eleven places |
|
|
897
987
|
| [1.3.0](docs/en/releases.md#130) | Evidence instead of claims, the editor instead of a terminal — six places |
|
|
898
988
|
| [1.2.0](docs/en/releases.md#120) | So the conversation doesn't break — six places |
|
|
899
989
|
|
package/README.md
CHANGED
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
## 목차
|
|
66
66
|
|
|
67
67
|
- [왜 만들었나](#왜-만들었나)
|
|
68
|
+
- [무엇이 다른가](#무엇이-다른가)
|
|
68
69
|
- [빠른 시작](#빠른-시작)
|
|
69
70
|
- [데이터가 나가는 길](#데이터가-나가는-길)
|
|
70
71
|
- [로컬 모델 여러 개 쓰기](#로컬-모델-여러-개-쓰기)
|
|
@@ -100,7 +101,7 @@
|
|
|
100
101
|
| [속도와 씀씀이](docs/ko/tuning.md) | 단계별 추론 강도 · 프리픽스 캐시 · 컨텍스트 길이 |
|
|
101
102
|
| [안전망과 사내 반입](docs/ko/safety.md) | 되돌리기 · 작업 범위 · 감사기록 · 심사 서류 |
|
|
102
103
|
| [설정](docs/ko/config.md) · [개발](docs/ko/develop.md) | 환경변수 · 실행 옵션 · 검사 돌리기 · 폴더 구조 |
|
|
103
|
-
| [릴리스 노트](docs/ko/releases.md) | [1.4.0](docs/ko/releases.md#140) · [1.3.0](docs/ko/releases.md#130) · [1.2.0](docs/ko/releases.md#120) |
|
|
104
|
+
| [릴리스 노트](docs/ko/releases.md) | [1.4.3](docs/ko/releases.md#143) · [1.4.2](docs/ko/releases.md#142) · [1.4.1](docs/ko/releases.md#141) · [1.4.0](docs/ko/releases.md#140) · [1.3.0](docs/ko/releases.md#130) · [1.2.0](docs/ko/releases.md#120) |
|
|
104
105
|
|
|
105
106
|
---
|
|
106
107
|
|
|
@@ -128,6 +129,75 @@ deel audit # 심사서 전문 출력
|
|
|
128
129
|
|
|
129
130
|
---
|
|
130
131
|
|
|
132
|
+
## 무엇이 다른가
|
|
133
|
+
|
|
134
|
+
로컬 모델을 상대하는 코딩 에이전트는 몇 개 있습니다. 그런데 **로컬로 돌리는
|
|
135
|
+
걸 전제로 속을 다시 설계한** 것은 드뭅니다.
|
|
136
|
+
|
|
137
|
+
| | 다른 도구 | deel |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `/undo` | 파일만 되돌린다 — 대화는 그 일이 있었다고 믿는 채로 | **대화까지** 같이 되감는다 |
|
|
140
|
+
| 긴 대화 | 클라우드가 안 느끼는 비용을 로컬에서도 그대로 짊어진다 | 프리픽스 캐시가 **살아남게** 순서를 설계 |
|
|
141
|
+
| 작은 모델의 편집 | 공백 하나 틀려도 실패 | 성공률 20%→**100%**, 엉뚱한 곳 0건 |
|
|
142
|
+
| 국산 모델 | 겪어 보기 전엔 모른다 | 공개 문서로 **미리** 안다 |
|
|
143
|
+
| "다 됐습니다" | 확인 안 된 것도 됐다고 한다 | `/evidence`·`/export` — **안 된 것까지** 적는다 |
|
|
144
|
+
| MCP·ACP | SDK 가 필요하다 | `child_process` + `JSON` 뿐 |
|
|
145
|
+
| 심사 서류 | 사람이 손으로 써서 실제와 어긋난다 | **코드가 소스를 훑어서** 만든다 |
|
|
146
|
+
|
|
147
|
+
<br>
|
|
148
|
+
|
|
149
|
+
#### `/undo` 는 파일과 대화를 같이 되감습니다
|
|
150
|
+
|
|
151
|
+
파일만 되돌리면 모델은 방금 그 편집을 했다고 계속 믿고 다음 턴을 그 위에
|
|
152
|
+
쌓습니다 — 화면에는 아무 일도 없었던 것처럼 보입니다. deel 은 되돌린 만큼
|
|
153
|
+
오간 메시지도 같이 접습니다. 접다가 도구 호출의 짝이 어긋나면 API 가 400 을
|
|
154
|
+
내므로, 짝을 다시 맞추는 로직(`repairToolPairs`)까지 같이 돕니다.
|
|
155
|
+
|
|
156
|
+
#### 긴 대화에서 로컬 모델이 갈수록 느려지는 이유를 직접 잡았습니다
|
|
157
|
+
|
|
158
|
+
Ollama·llama.cpp 는 프롬프트 앞부분이 한 글자라도 바뀌면 그 뒤 전부를 다시
|
|
159
|
+
계산합니다. 클라우드 API 는 이 비용을 안 느끼니 신경 쓸 이유가 없지만,
|
|
160
|
+
로컬로 돌리는 사람에게는 대화가 길어질수록 턴마다 지연이 쌓입니다. deel 은
|
|
161
|
+
매 턴 바뀔 수 있는 것(모드·핀)을 프롬프트 **맨 끝**으로 보내고, Ollama 에는
|
|
162
|
+
`keep_alive: 60m` 을 같이 보내 앞부분이 캐시에 그대로 남게 합니다. 이 순서는
|
|
163
|
+
검사(`test/cache.test.js`)가 지킵니다.
|
|
164
|
+
|
|
165
|
+
#### 작은 모델도 편집이 실제로 성공합니다
|
|
166
|
+
|
|
167
|
+
작은 로컬 모델은 고치려는 문자열을 공백 하나까지 완벽히 재현하지 못하는
|
|
168
|
+
경우가 흔합니다. `npm run bench` 로 잰 내부 벤치마크에서, 정확히 일치만
|
|
169
|
+
요구하던 옛 방식은 성공률 20% 였습니다. 지금 방식(공백·들여쓰기를 단계적으로
|
|
170
|
+
완화)은 **100%** — 그러면서도 엉뚱한 곳을 고친 건수는 두 방식 다 **0건**
|
|
171
|
+
입니다. 모호하면 찾지 못했다고 말할지언정 짐작해서 고치지 않습니다.
|
|
172
|
+
|
|
173
|
+
#### 국산 모델은 겪어 보기 전에 압니다
|
|
174
|
+
|
|
175
|
+
EXAONE·HyperCLOVA X·Kanana·Midm·Solar 는 공개 문서로 확인 가능한 버릇(예:
|
|
176
|
+
추론형 여부)을 deel 이 미리 알고 시작합니다. 다른 도구는 이 모델들을 한
|
|
177
|
+
번도 못 만나 본 채로 취급하고, 사람이 열 몇 턴을 겪어야 요령이 붙습니다.
|
|
178
|
+
|
|
179
|
+
#### "다 됐습니다" 대신 증명서를 냅니다
|
|
180
|
+
|
|
181
|
+
`/evidence` 와 `/export` 는 무엇을 바꿨는지뿐 아니라 **무엇을 확인하지
|
|
182
|
+
못했는지**까지 적습니다. AI 코딩 도구가 사람을 제일 잘 속이는 순간이 자신
|
|
183
|
+
있게 "됐습니다" 라고 말할 때라는 걸 알기 때문입니다. `/export` 는 바깥
|
|
184
|
+
주소가 하나도 없는 자기완결 HTML 이라 폐쇄망 어디서든 열립니다.
|
|
185
|
+
|
|
186
|
+
#### MCP·ACP 를 SDK 없이 붙였습니다
|
|
187
|
+
|
|
188
|
+
Model Context Protocol 과 Agent Client Protocol 은 둘 다 표준입출력으로 줄
|
|
189
|
+
단위 JSON-RPC 2.0 을 주고받는 게 전부입니다. deel 은 `child_process` 와
|
|
190
|
+
`JSON` 만으로 둘 다 구현했습니다 — 의존성 0개가 "기능을 뺀 결과" 가 아니라
|
|
191
|
+
"안 필요해서" 라는 걸 이 두 가지가 보여 줍니다.
|
|
192
|
+
|
|
193
|
+
#### 심사 서류를 손으로 안 씁니다
|
|
194
|
+
|
|
195
|
+
`deel pack` 이 만드는 반입심사서·SBOM·감사 사양은 **코드가 소스를 훑어서**
|
|
196
|
+
만든 겁니다. 손으로 적은 서류는 언젠가 실제와 어긋나고, 담당자가 그걸 한 번
|
|
197
|
+
발견하면 나머지 서류도 안 믿습니다.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
131
201
|
## 빠른 시작
|
|
132
202
|
|
|
133
203
|
### 설치
|
|
@@ -185,9 +255,17 @@ deel
|
|
|
185
255
|
GET 만. 본문 0바이트. 사내망·로컬 주소는 거절. 다녀온 곳은 전부 기록.
|
|
186
256
|
|
|
187
257
|
[C] 플러그인 받기 ────── /plugin install 을 칠 때만 잠깐
|
|
258
|
+
|
|
259
|
+
[D] MCP 서버 ─────────── 딴 자식 프로세스, 남의 프로그램
|
|
260
|
+
.deel/mcp.json 에 사람이 직접 적어야만 뜹니다. 기본은 꺼져 있습니다.
|
|
188
261
|
```
|
|
189
262
|
|
|
190
|
-
|
|
263
|
+
A·B·C 는 deel 이 직접 만드는 요청이라 하나씩 걸러낼 수 있습니다. **[D] 는
|
|
264
|
+
다릅니다** — MCP 서버는 별도 프로세스라 그 안에서 무슨 소켓을 여는지 코드로
|
|
265
|
+
볼 수 없습니다. 그래서 `--offline` 일 때는 요청을 거르는 대신 **그 서버
|
|
266
|
+
자체를 아예 띄우지 않습니다** — 막을 수 없는 것을 막았다고 말하지 않습니다.
|
|
267
|
+
|
|
268
|
+
`--offline` 을 주면 **B·C·D 가 모두 막히고 이 컴퓨터 안으로만** 다닙니다.
|
|
191
269
|
|
|
192
270
|
```bash
|
|
193
271
|
deel --offline
|
|
@@ -202,8 +280,9 @@ deel --offline
|
|
|
202
280
|
수집·전송하는 것이 없습니다. 텔레메트리, 사용 통계, 오류 보고 전부 없습니다.
|
|
203
281
|
대화 기록·되돌리기 이력·설정은 작업 폴더의 `.deel/` 안에만 남습니다.
|
|
204
282
|
|
|
205
|
-
> 검증: `npm test` 의 network·web 검사
|
|
206
|
-
> **한 건도 닿지
|
|
283
|
+
> 검증: `npm test` 의 network·web·mcp 검사 123항목. 허용되지 않은 서버에 실제로
|
|
284
|
+
> 요청이 **한 건도 닿지 않는지**, `--offline` 일 때 MCP 서버가 **한 번도 안
|
|
285
|
+
> 뜨는지**까지 진짜 서버를 띄워서 확인합니다.
|
|
207
286
|
|
|
208
287
|
---
|
|
209
288
|
|
|
@@ -892,7 +971,10 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
892
971
|
|
|
893
972
|
| 판 | 무엇이 바뀌었나 |
|
|
894
973
|
|---|---|
|
|
895
|
-
| **[1.4.
|
|
974
|
+
| **[1.4.3](docs/ko/releases.md#143)** | README 가 왜 다른지 말하고, 심사서의 빠진 한 줄을 채운다 |
|
|
975
|
+
| [1.4.2](docs/ko/releases.md#142) | 1.4.1 이 보안 수정 전에 나갔다 — 그걸 바로잡는 판 |
|
|
976
|
+
| [1.4.1](docs/ko/releases.md#141) | 새 기능 없이, 실제로 찾은 것만 고친다 — 윈도우 abort·ReDoS·XSS |
|
|
977
|
+
| [1.4.0](docs/ko/releases.md#140) | deel 이 제 얼굴을 갖고, 영어로도 말하고, 뜻까지 본다 — 열한 자리 |
|
|
896
978
|
| [1.3.0](docs/ko/releases.md#130) | 말 대신 증거, 터미널 대신 에디터 — 여섯 자리 |
|
|
897
979
|
| [1.2.0](docs/ko/releases.md#120) | 대화가 끊기지 않게 — 여섯 자리 |
|
|
898
980
|
|
package/package.json
CHANGED
package/src/pack/selfpack.js
CHANGED
|
@@ -164,7 +164,7 @@ export function reviewSheet(a, at) {
|
|
|
164
164
|
L.push(' 설정 파일: ~/.deel/config.json (또는 환경변수 DEEL_API_KEY)');
|
|
165
165
|
L.push('');
|
|
166
166
|
|
|
167
|
-
L.push('3-1. 나가는 길은
|
|
167
|
+
L.push('3-1. 나가는 길은 네 갈래이고 서로 섞이지 않습니다');
|
|
168
168
|
L.push(줄());
|
|
169
169
|
L.push(' [A] 모델 게이트웨이 — 소스 코드가 실려 나가는 유일한 길');
|
|
170
170
|
L.push(' · 주소: deel setup 에서 정한 곳 딱 한 자리');
|
|
@@ -181,8 +181,14 @@ export function reviewSheet(a, at) {
|
|
|
181
181
|
L.push(' [C] 플러그인 받기 (github) — 사용자가 /plugin install 을 칠 때만');
|
|
182
182
|
L.push(' · 그 명령이 도는 동안만 열리고 끝나면 닫힙니다.');
|
|
183
183
|
L.push('');
|
|
184
|
-
L.push('
|
|
185
|
-
L.push('
|
|
184
|
+
L.push(' [D] MCP 서버 — 딴 자식 프로세스, 남의 프로그램');
|
|
185
|
+
L.push(' · .deel/mcp.json 에 사람이 직접 적어야만 뜹니다. 기본은 꺼져 있습니다.');
|
|
186
|
+
L.push(' · A·B·C 와 달리 이 서버가 안에서 무슨 소켓을 여는지는 코드로 볼 수');
|
|
187
|
+
L.push(' 없습니다. 그래서 요청을 거르는 대신, --offline 이면 서버 자체를');
|
|
188
|
+
L.push(' 아예 띄우지 않습니다.');
|
|
189
|
+
L.push('');
|
|
190
|
+
L.push(' --offline 으로 켜면 [B]·[C]·[D] 가 모두 막히고, 이 컴퓨터 안으로만 다닙니다.');
|
|
191
|
+
L.push(' (검증: npm test 안의 network / web / mcp 검사 123항목이 이를 확인합니다)');
|
|
186
192
|
L.push('');
|
|
187
193
|
|
|
188
194
|
L.push('4. 스킬·플러그인');
|
package/src/preview/serve.js
CHANGED
|
@@ -60,12 +60,21 @@ const 되살림 = `
|
|
|
60
60
|
|
|
61
61
|
const 안전한경로 = (u) => {
|
|
62
62
|
// %00 이나 널바이트가 섞이면 아래 fs 가 경로를 잘라 읽는다. 통째로 거절한다.
|
|
63
|
+
// `\` 도 거절한다 — 앞의 `/` 는 지우지만 `\evil.com` 처럼 역슬래시로 시작하면
|
|
64
|
+
// 남아 있다가, 밑에서 `/${길}/` 로 리다이렉트 주소를 만들 때 일부 브라우저가
|
|
65
|
+
// `\` 를 `/` 로 봐서 `//evil.com/` 처럼 다른 도메인으로 새는 통로가 된다.
|
|
63
66
|
let p;
|
|
64
67
|
try { p = decodeURIComponent(String(u).split('?')[0].split('#')[0]); } catch { return null; }
|
|
65
|
-
if (p.includes('\0')) return null;
|
|
68
|
+
if (p.includes('\0') || p.includes('\\')) return null;
|
|
66
69
|
return p.replace(/^\/+/, '');
|
|
67
70
|
};
|
|
68
71
|
|
|
72
|
+
/** 이용자 파일 이름을 HTML 안에 그대로 꽂기 전에 씌운다 — 파일 이름에
|
|
73
|
+
* `<script>` 가 들어 있어도 태그로 안 읽히게. */
|
|
74
|
+
const html씌우기 = (s) => String(s).replace(/[&<>"']/g, (c) => ({
|
|
75
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
76
|
+
}[c]));
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* 폴더 하나를 띄운다.
|
|
71
80
|
*
|
|
@@ -294,15 +303,20 @@ function 목록주기(abs, 뿌리, res) {
|
|
|
294
303
|
let 것들 = [];
|
|
295
304
|
try { 것들 = readdirSync(abs, { withFileTypes: true }); } catch { /* 못 읽으면 빈 채로 */ }
|
|
296
305
|
const 여기 = relative(뿌리, abs).split(sep).join('/');
|
|
306
|
+
// 파일 이름은 디스크에서 온 것이라 `<script>` 같은 것이 그대로 들어 있을 수
|
|
307
|
+
// 있다 — 씌우지 않고 꽂으면 그 파일이 있는 폴더를 미리보기로 여는 사람 화면에서
|
|
308
|
+
// 그대로 실행된다(Stored XSS). href 는 이미 encodeURIComponent 로 씌웠지만
|
|
309
|
+
// 화면에 보이는 글자는 안 씌웠던 것이 문제였다.
|
|
297
310
|
const 줄 = (e) => {
|
|
298
|
-
const 이름 = e.name + (e.isDirectory() ? '/' : '');
|
|
311
|
+
const 이름 = html씌우기(e.name) + (e.isDirectory() ? '/' : '');
|
|
299
312
|
return `<li><a href="${encodeURIComponent(e.name)}${e.isDirectory() ? '/' : ''}">${이름}</a></li>`;
|
|
300
313
|
};
|
|
301
|
-
const
|
|
314
|
+
const 여기씌움 = html씌우기(여기);
|
|
315
|
+
const 몸 = `<!doctype html><meta charset="utf-8"><title>${여기씌움 || '/'}</title>`
|
|
302
316
|
+ '<style>body{font:14px/1.7 ui-monospace,monospace;max-width:44rem;margin:3rem auto;padding:0 1rem}'
|
|
303
317
|
+ 'h1{font-size:1rem;color:#888;font-weight:400}a{color:#0a7}li{list-style:none}'
|
|
304
318
|
+ 'p{color:#999}@media(prefers-color-scheme:dark){body{background:#111;color:#ddd}a{color:#4d9}}</style>'
|
|
305
|
-
+ `<h1>/${
|
|
319
|
+
+ `<h1>/${여기씌움}</h1>`
|
|
306
320
|
+ (여기 ? '<li><a href="../">../</a></li>' : '')
|
|
307
321
|
+ 것들.map(줄).join('')
|
|
308
322
|
+ '<p>index.html 이 없어서 목록을 보여 줍니다.</p>';
|
package/src/safety/guard.js
CHANGED
|
@@ -81,8 +81,11 @@ const BLOCKED = [
|
|
|
81
81
|
{ re: /\bdel\b[^|;&]*\s\/[sq]\b[^|;&]*(\\\*|\s[a-z]:\\?\s*$)/i, why: '하위 폴더까지 전부 지웁니다' },
|
|
82
82
|
// 파워셸도 같은 일을 한다. 드라이브 뿌리나 집 폴더를 통째로 미는 것만 본다 —
|
|
83
83
|
// 폴더 하나 지우는 평범한 Remove-Item 까지 막으면 도구를 못 쓴다.
|
|
84
|
+
// 가운데 `(-?[a-z]*\s*)*` 는 양쪽 다 빈 문자열을 허용해서, 안 맞는 긴 입력이
|
|
85
|
+
// 들어오면 되돌아가는 경우의 수가 기하급수로 늘어난다 — guard 검사 자체가
|
|
86
|
+
// 멈춰 버린다(ReDoS). 각 겹이 최소 한 글자는 먹게 고쳐서 그 길을 막는다.
|
|
84
87
|
{
|
|
85
|
-
re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(
|
|
88
|
+
re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(?:-?[a-z]+\s+)*([a-z]:\\?|~|\$HOME|\$env:USERPROFILE)\s*$/i,
|
|
86
89
|
why: '드라이브나 집 폴더를 통째로 지웁니다',
|
|
87
90
|
},
|
|
88
91
|
{ re: /git\s+push\b[^|;&]*--force(?!-with-lease)/i, why: '원격 이력을 덮어씁니다 (--force-with-lease 를 쓰세요)' },
|
package/src/tools/webfetch.js
CHANGED
|
@@ -91,15 +91,29 @@ function 얼마나쉬라나(머리, 회차) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
function 태그벗기기(html) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
let 글 = String(html);
|
|
95
|
+
// script·style·주석을 겹쳐 쓰거나 안 닫은 모양으로 흘려 보내는 페이지가
|
|
96
|
+
// 있다 — 정규식 한 번으로는 다 못 걷어 낸다(파서가 아니라서). 더 지울 것이
|
|
97
|
+
// 없어질 때까지 반복한다. 그래도 이 글은 모델에게 주는 참고용 글일 뿐
|
|
98
|
+
// 화면에 그리지 않으니, 여기서 다 못 걸러도 실행되는 것은 아니다.
|
|
99
|
+
for (let i = 0; i < 5; i += 1) {
|
|
100
|
+
const 전 = 글;
|
|
101
|
+
글 = 글
|
|
102
|
+
.replace(/<script[\s\S]*?<\/script\s*>/gi, ' ')
|
|
103
|
+
.replace(/<style[\s\S]*?<\/style\s*>/gi, ' ')
|
|
104
|
+
.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
105
|
+
if (글 === 전) break;
|
|
106
|
+
}
|
|
107
|
+
return 글
|
|
98
108
|
.replace(/<\/(p|div|section|article|li|tr|h[1-6]|br)>/gi, '\n')
|
|
99
109
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
100
110
|
.replace(/<[^>]+>/g, ' ')
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
// 한 번에 찾아서 한 번에 바꾼다 — 차례로 바꾸면(`&` 를 먼저 `&` 로
|
|
112
|
+
// 풀고 나서 `<` 를 다시 찾는 식) `&lt;` 처럼 두 겹 씌운 것이
|
|
113
|
+
// 두 번 풀려서 `<` 로 튀어나온다(글자로 남아야 하는데 태그처럼 보이게 됨).
|
|
114
|
+
.replace(/&(nbsp|amp|lt|gt|quot|#39);/g, (_, 이름) => ({
|
|
115
|
+
nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'",
|
|
116
|
+
}[이름]))
|
|
103
117
|
.replace(/[ \t]+/g, ' ')
|
|
104
118
|
.replace(/\n\s*\n\s*\n+/g, '\n\n')
|
|
105
119
|
.trim();
|
package/src/ui/status.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 남의 패키지를 붙이지 않는다. 필요한 숫자는 전부 session 이 이미 갖고 있고,
|
|
4
4
|
// 화면 그리기는 ansi.js 만 쓴다. 반입 심사에 새로 설명할 것이 늘지 않게 하려는 뜻이다.
|
|
5
|
-
import { c,
|
|
5
|
+
import { c, 눈금게이지, width, clip, cols, mark } from './ansi.js';
|
|
6
6
|
import { PROFILES } from '../agent/effort.js';
|
|
7
7
|
import { get as workMode, canWrite, 보일이름 } from '../agent/modes.js';
|
|
8
8
|
import { isLocalHost, isOffline } from '../safety/network.js';
|