deel-local-cli 1.7.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +1166 -0
- package/README.md +1223 -1107
- package/bin/deel.js +78 -12
- package/package.json +4 -4
- package/src/acp/map.js +1 -1
- package/src/acp/serve.js +40 -3
- package/src/agent/compact.js +314 -296
- package/src/agent/effort.js +27 -7
- package/src/agent/evidence.js +2 -0
- package/src/agent/filemem.js +141 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +218 -40
- package/src/agent/session.js +136 -14
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +272 -23
- package/src/backend/learn.js +24 -0
- package/src/backend/mcp.js +96 -5
- package/src/backend/probe.js +131 -61
- package/src/backend/quota.js +29 -5
- package/src/backend/retry.js +3 -0
- package/src/backend/scanui.js +1 -1
- package/src/backend/toolfit.js +325 -0
- package/src/commands.js +170 -61
- package/src/completion.js +20 -2
- package/src/config.js +47 -2
- package/src/i18n/en.js +210 -3
- package/src/i18n/index.js +40 -0
- package/src/i18n/ja.js +204 -3
- package/src/i18n/ko.js +270 -3
- package/src/i18n/zh.js +204 -3
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +92 -5
- package/src/pack/sbom.js +30 -4
- package/src/pack/selfpack.js +26 -10
- package/src/pack/sheet.en.js +288 -0
- package/src/pack/tar.js +65 -2
- package/src/plugins/manage.js +46 -9
- package/src/repl.js +344 -61
- package/src/reset.js +397 -0
- package/src/safety/audit.js +95 -8
- package/src/safety/authcmd.js +316 -0
- package/src/safety/guard.js +143 -0
- package/src/safety/keystore.js +114 -38
- package/src/safety/undo.js +25 -6
- package/src/tools/desc.en.js +19 -14
- package/src/tools/index.js +349 -75
- package/src/tools/jobs.js +175 -42
- package/src/tools/lsp.js +5 -4
- package/src/tools/outline.js +7 -3
- package/src/tools/task.js +10 -6
- package/src/tools/todo.js +9 -2
- package/src/tools/verify.js +42 -7
- package/src/tools/webfetch.js +94 -10
- package/src/ui/export.js +1 -1
- package/src/ui/md.js +201 -5
- package/src/ui/motion.js +0 -1
- package/src/ui/pastechip.js +50 -3
- package/src/ui/pick.js +115 -0
- package/src/ui/screen.js +23 -3
- package/README.en.md +0 -1117
package/src/oneshot.js
CHANGED
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
// 그래서 '묻는 자리' 를 전부 없앤 길을 따로 낸다. 에이전트 루프는 그대로 쓴다 —
|
|
12
12
|
// 여기서 루프를 다시 짜면 두 벌이 되고, 언젠가 한쪽만 고쳐진다.
|
|
13
13
|
import { c, mark, clip } from './ui/ansi.js';
|
|
14
|
-
import {
|
|
14
|
+
import { 규칙모으기, 정책읽기 } from './safety/policy.js';
|
|
15
|
+
import { 받기설정 } from './safety/authcmd.js';
|
|
15
16
|
import { run } from './agent/loop.js';
|
|
16
17
|
import { Session } from './agent/session.js';
|
|
17
18
|
import { makeScope } from './safety/guard.js';
|
|
18
19
|
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
19
20
|
import { History } from './safety/undo.js';
|
|
20
|
-
import { Audit } from './safety/audit.js';
|
|
21
|
-
import { activeProfile, load, resolveKey,
|
|
21
|
+
import { Audit, 열쇠묻기 } from './safety/audit.js';
|
|
22
|
+
import { activeProfile, load, resolveKey, 잠금소식, 열쇠탈소식 } from './config.js';
|
|
22
23
|
import { 말 as 옮긴말 } from './i18n/index.js';
|
|
23
24
|
import { 알림채움 } from './backend/retry.js';
|
|
24
25
|
import { discover } from './skills/discover.js';
|
|
@@ -43,6 +44,15 @@ export const EXIT = {
|
|
|
43
44
|
limit: 2, // 도구 호출 걸음 수 상한에 닿았다
|
|
44
45
|
stuck: 3, // 같은 자리에서 헛돌아 스스로 멈췄다
|
|
45
46
|
aborted: 4, // 도중에 끊겼다 (Ctrl+C)
|
|
47
|
+
/*
|
|
48
|
+
* 서버가 끝났다는 말을 한 번도 안 주고 답을 멈췄다.
|
|
49
|
+
*
|
|
50
|
+
* 여기에 자기 코드가 있어야 한다. 전에는 이걸 아예 안 보고 `done`(0) 으로
|
|
51
|
+
* 끝냈다. `deel -p` 는 잡·CI 에서 돌고 그 뒤에 스크립트가 붙는데, 반쪽짜리
|
|
52
|
+
* 답이 0 으로 넘어가면 그 스크립트는 온전한 답으로 알고 그대로 쓴다.
|
|
53
|
+
* 사람이 나중에 결과를 보고 왜 반쪽인지 되짚을 방법이 없다.
|
|
54
|
+
*/
|
|
55
|
+
cutoff: 5,
|
|
46
56
|
};
|
|
47
57
|
|
|
48
58
|
/**
|
|
@@ -63,7 +73,7 @@ async function 표준입력읽기() {
|
|
|
63
73
|
// 도구 한 줄 요약. 화면 그림이 아니라 로그에 남을 글이라 색을 아낀다.
|
|
64
74
|
function 도구줄(name, args) {
|
|
65
75
|
const a = args ?? {};
|
|
66
|
-
const 첫 = a.file_path ?? a.pattern ?? a.path ?? a.url ?? a.name ?? a.목적 ??
|
|
76
|
+
const 첫 = a.file_path ?? a.pattern ?? a.path ?? a.url ?? a.name ?? a.purpose ?? a.목적 ??
|
|
67
77
|
(a.command ? String(a.command).replace(/\s+/g, ' ') : null) ??
|
|
68
78
|
// 뒤에서 도는 명령. 번호가 곧 그 일감의 이름이라, 이게 없으면 기록에
|
|
69
79
|
// `Jobs()` 만 여러 줄 남아 나중에 무엇을 본 것인지 알 수 없다.
|
|
@@ -154,6 +164,14 @@ export async function runOnce(opts = {}) {
|
|
|
154
164
|
const conn = {
|
|
155
165
|
kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
|
|
156
166
|
key: resolveKey(prof), model: prof.model,
|
|
167
|
+
/*
|
|
168
|
+
* 열쇠를 갖고 있는 대신 **받아 오는** 설정 (safety/authcmd.js).
|
|
169
|
+
*
|
|
170
|
+
* 여기서는 설정만 싣는다. 명령은 요청 직전에 부른다 — 켤 때 한 번
|
|
171
|
+
* 받아 두면 한 시간짜리 토큰이 세 시간짜리 대화 한복판에서 죽고,
|
|
172
|
+
* 그 401 은 화면에서 「열쇠가 틀렸다」 와 구별이 안 된다.
|
|
173
|
+
*/
|
|
174
|
+
열쇠받기: 받기설정(prof, { 정책값: 정책읽기().값 }),
|
|
157
175
|
ctx: opts.ctx ?? prof.ctx ?? CTX_DEFAULT,
|
|
158
176
|
// 답 길이 상한 — deel --max-tokens 32k 로 높일 수 있다(대화 화면의 /out 과 같은 값).
|
|
159
177
|
maxTokens: opts.maxTokens ?? prof.maxTokens ?? null,
|
|
@@ -162,6 +180,16 @@ export async function runOnce(opts = {}) {
|
|
|
162
180
|
vision: prof.vision ?? false,
|
|
163
181
|
};
|
|
164
182
|
|
|
183
|
+
/*
|
|
184
|
+
* 잠근 열쇠를 못 풀었으면 그 까닭을 표준오류로 낸다.
|
|
185
|
+
*
|
|
186
|
+
* 배치로 부르는 쪽은 표준출력을 JSON 으로 읽으므로 거기에 섞으면 안 된다.
|
|
187
|
+
* 그렇다고 조용히 넘기면 여기가 더 나쁘다 — 아무도 안 보고 있는 자리라
|
|
188
|
+
* 401 하나 남기고 끝난다. 위 잠금소식 과 같은 자리, 같은 규칙이다.
|
|
189
|
+
*/
|
|
190
|
+
const 열쇠탈 = 열쇠탈소식();
|
|
191
|
+
if (열쇠탈) { try { process.stderr.write(` ${열쇠탈}\n`); } catch { /* 못 써도 그만 */ } }
|
|
192
|
+
|
|
165
193
|
/*
|
|
166
194
|
* 자물쇠는 대화 화면과 똑같이 건다. 비대화라고 느슨해질 이유가 없다 —
|
|
167
195
|
* 오히려 배치는 아무도 안 보는 자리라 더 단단해야 한다.
|
|
@@ -224,7 +252,7 @@ export async function runOnce(opts = {}) {
|
|
|
224
252
|
// 적어 둔 허락·금지 규칙 (safety/policy.js). 승인 모드보다 먼저 본다.
|
|
225
253
|
규칙들: 규칙모으기(cfg),
|
|
226
254
|
history: new History(root),
|
|
227
|
-
audit: new Audit(root),
|
|
255
|
+
audit: new Audit(root, { 열쇠들: 열쇠묻기(conn) }),
|
|
228
256
|
seen: new Set(),
|
|
229
257
|
skills: found.skills,
|
|
230
258
|
loadedSkills: new Set(),
|
|
@@ -299,6 +327,8 @@ export async function runOnce(opts = {}) {
|
|
|
299
327
|
let steps = 0;
|
|
300
328
|
let 이번단계글 = ''; // 지금 단계에서 모델이 흘린 글. 단계가 바뀌면 비운다
|
|
301
329
|
let 답 = null;
|
|
330
|
+
// 서버가 끝났다는 말 없이 멈춘 적이 있나. 뒤에 오는 done 이 이걸 못 지운다.
|
|
331
|
+
let 말없이끊겼나 = false;
|
|
302
332
|
|
|
303
333
|
try {
|
|
304
334
|
// 하위 작업 안쪽에서 온 이벤트는 그 겹만큼 들여 쓴다.
|
|
@@ -397,6 +427,50 @@ export async function runOnce(opts = {}) {
|
|
|
397
427
|
곁(` ${c.gray(`(접지 못했습니다: ${ev.why})`)}`);
|
|
398
428
|
break;
|
|
399
429
|
|
|
430
|
+
/*
|
|
431
|
+
* ── 여기 넷은 여태 아예 안 보고 있었다 ────────────────────────
|
|
432
|
+
*
|
|
433
|
+
* 대화창은 넷 다 화면에 적어 준다. 한 방 실행은 안 적었다. 그래서
|
|
434
|
+
* 잘린 답이 **온전한 답인 척** 파이프 뒤로 넘어갔다. `deel -p` 는
|
|
435
|
+
* 잡·CI 에서 돌고 그 기록이 나중에 근거가 되는 물건이라, 여기서
|
|
436
|
+
* 빠지면 왜 반쪽인지 되짚을 자리가 아무 데도 없다.
|
|
437
|
+
*/
|
|
438
|
+
case 'capped':
|
|
439
|
+
곁(` ${c.yellow('⚠')} ${c.gray(옮긴말('ev.capped', { 한계: ev.cap.toLocaleString() }))}`);
|
|
440
|
+
break;
|
|
441
|
+
|
|
442
|
+
/*
|
|
443
|
+
* 말없이 끊긴 것은 **종료코드까지** 바꾼다.
|
|
444
|
+
*
|
|
445
|
+
* 뒤에 done 이 따라오므로 그대로 두면 0 으로 끝난다. 잘렸다는 사실을
|
|
446
|
+
* 화면에만 적고 코드로는 성공이라고 하면, 스크립트는 화면을 안 읽으니
|
|
447
|
+
* 아무 일도 없던 것이 된다. 다만 여기서 곧바로 reason 을 바꾸지는
|
|
448
|
+
* 않는다 — 뒤에 오는 done 이 덮어쓰기 때문에 표시만 해 두고
|
|
449
|
+
* 루프가 끝난 뒤에 판정한다.
|
|
450
|
+
*/
|
|
451
|
+
case 'cutoff':
|
|
452
|
+
말없이끊겼나 = true;
|
|
453
|
+
곁(` ${c.yellow('⚠')} ${c.gray(옮긴말('ev.cutoff'))}`);
|
|
454
|
+
break;
|
|
455
|
+
|
|
456
|
+
case 'nudge':
|
|
457
|
+
곁(` ${c.gray(`↺ ${ev.why === '요청누락'
|
|
458
|
+
? 옮긴말('ev.nudgeMissed', { n: ev.빠진?.length ?? 0 })
|
|
459
|
+
: 옮긴말('ev.nudgeRead')}`)}`);
|
|
460
|
+
break;
|
|
461
|
+
|
|
462
|
+
// 자리가 다 차서 비웠다. 오류가 아니라 이어 가는 중이라, 종료코드는 안 건드린다.
|
|
463
|
+
case 'reset':
|
|
464
|
+
곁(` ${c.cyan('◱')} ${c.gray((ev.kept?.할일 ?? 0)
|
|
465
|
+
? 옮긴말('ev.reset', { 버린수: ev.dropped, 할일: ev.kept.할일 })
|
|
466
|
+
: 옮긴말('ev.resetBare', { 버린수: ev.dropped }))}`);
|
|
467
|
+
break;
|
|
468
|
+
case 'learned':
|
|
469
|
+
곁(` ${c.cyan('◎')} ${c.gray(ev.what === 'ctx'
|
|
470
|
+
? 옮긴말('ev.learnedCtx', { 한계: ev.limit.toLocaleString() })
|
|
471
|
+
: 옮긴말('ev.learnedOut', { 한계: ev.limit.toLocaleString() }))}`);
|
|
472
|
+
break;
|
|
473
|
+
|
|
400
474
|
case 'limit':
|
|
401
475
|
reason = 'limit';
|
|
402
476
|
why = `도구 호출 ${ev.steps}회에서 멈췄습니다. 한 번에 하기엔 큰 일입니다 — 나눠서 시키세요.`;
|
|
@@ -434,6 +508,19 @@ export async function runOnce(opts = {}) {
|
|
|
434
508
|
// 왜 안 됐는지 짐작할 거리조차 없다.
|
|
435
509
|
if (답 == null) 답 = 이번단계글;
|
|
436
510
|
|
|
511
|
+
/*
|
|
512
|
+
* 말없이 끊긴 답은 '다 됐다' 로 안 넘긴다.
|
|
513
|
+
*
|
|
514
|
+
* done 이 뒤따라오므로 그냥 두면 reason 이 done 으로 덮인다. 그런데 그
|
|
515
|
+
* 답은 중간에서 끊긴 물건이다. 여기서 갈라 주지 않으면 파이프 뒤 스크립트가
|
|
516
|
+
* 반쪽을 온전한 것으로 받아 쓴다 — 그러고도 아무 표시가 안 남는다.
|
|
517
|
+
* 진짜 탈(error·limit·stuck)이 이미 잡혔으면 그쪽이 먼저다.
|
|
518
|
+
*/
|
|
519
|
+
if (말없이끊겼나 && reason === 'done') {
|
|
520
|
+
reason = 'cutoff';
|
|
521
|
+
why = 옮긴말('ev.cutoff');
|
|
522
|
+
}
|
|
523
|
+
|
|
437
524
|
const code = EXIT[reason] ?? EXIT.error;
|
|
438
525
|
if (why) 삐끗(` ${reason === 'done' ? c.gray('·') : c.red('✗')} ${why}`);
|
|
439
526
|
if (!json && !quiet) {
|
package/src/pack/sbom.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
// 적어 둔 칸이 다 있는지 본다.
|
|
26
26
|
import { randomUUID } from 'node:crypto';
|
|
27
27
|
import { 보관방식 } from '../safety/keystore.js';
|
|
28
|
+
import { 언어 } from '../i18n/index.js';
|
|
29
|
+
import { specEn, egressEn, keyStorageEn, auditLogEn, specSummaryEn } from './sheet.en.js';
|
|
28
30
|
|
|
29
31
|
/** 우리가 내놓는 SBOM 규격. 스캐너가 이 숫자를 보고 읽는 법을 정한다. */
|
|
30
32
|
export const CDX판 = '1.5';
|
|
@@ -41,7 +43,7 @@ export const CDX판 = '1.5';
|
|
|
41
43
|
* @param {Date} o.at 만든 때 (검사가 고정할 수 있게)
|
|
42
44
|
* @param {string} o.serial 문서 일련번호 (검사가 고정할 수 있게)
|
|
43
45
|
*/
|
|
44
|
-
export function sbom(a, { at = new Date(), serial = null } = {}) {
|
|
46
|
+
export function sbom(a, { at = new Date(), serial = null, lang = 언어() } = {}) {
|
|
45
47
|
const purl = `pkg:npm/${a.name}@${a.version}`;
|
|
46
48
|
const 본체 = {
|
|
47
49
|
type: 'application',
|
|
@@ -50,7 +52,11 @@ export function sbom(a, { at = new Date(), serial = null } = {}) {
|
|
|
50
52
|
version: a.version,
|
|
51
53
|
purl,
|
|
52
54
|
licenses: a.license ? [{ license: { id: a.license } }] : [],
|
|
53
|
-
|
|
55
|
+
// SBOM 은 남에게 내는 서류다. 화면을 영어로 켜 둔 사람이 한국어 설명이
|
|
56
|
+
// 박힌 문서를 심사에 낼 수는 없다.
|
|
57
|
+
description: lang === 'ko'
|
|
58
|
+
? '로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI'
|
|
59
|
+
: 'A coding agent CLI for local models and in-house gateways',
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
return {
|
|
@@ -208,7 +214,20 @@ export function 감사명세() {
|
|
|
208
214
|
/**
|
|
209
215
|
* 세 가지를 한 덩이로. zip 에 이대로 들어간다.
|
|
210
216
|
*/
|
|
211
|
-
export function 심사명세(a, { at = new Date() } = {}) {
|
|
217
|
+
export function 심사명세(a, { at = new Date(), lang = 언어() } = {}) {
|
|
218
|
+
/*
|
|
219
|
+
* 한국어가 아니면 영어 서류를 낸다 (pack/sheet.en.js).
|
|
220
|
+
*
|
|
221
|
+
* 열쇠 이름까지 영어라야 뜻이 있다. 값만 옮기고 열쇠를 `통신`·`감사기록`
|
|
222
|
+
* 으로 두면, 스캐너에 넣는 사람이 무엇이 무엇인지 못 읽는다 — 반쯤 옮긴
|
|
223
|
+
* 서류는 안 옮긴 것보다 나쁘다.
|
|
224
|
+
*
|
|
225
|
+
* 일본어·중국어도 영어로 간다. i18n 의 물러날곳(ja→en→ko)과 같은 규칙이다.
|
|
226
|
+
*/
|
|
227
|
+
if (lang !== 'ko') {
|
|
228
|
+
const 자리 = (id) => (a.calls?.[id] ?? []).map((x) => `${x.file}:${x.line}`);
|
|
229
|
+
return specEn(a, at, egressEn(자리, a), keyStorageEn(보관방식(null, { lang: 'en' })), auditLogEn());
|
|
230
|
+
}
|
|
212
231
|
return {
|
|
213
232
|
만든것: a.name,
|
|
214
233
|
판: a.version,
|
|
@@ -231,9 +250,16 @@ export function 심사명세(a, { at = new Date() } = {}) {
|
|
|
231
250
|
};
|
|
232
251
|
}
|
|
233
252
|
|
|
234
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* 사람이 읽을 짧은 요약. 화면에 찍는 자리에서 쓴다.
|
|
255
|
+
*
|
|
256
|
+
* 어느 말로 쓸지를 **명세 자체를 보고** 정한다. 언어() 를 여기서 또 물으면,
|
|
257
|
+
* 명세를 만든 뒤에 말이 바뀐 자리에서 요약만 딴 말이 된다 — 그러면 읽을 수
|
|
258
|
+
* 없는 열쇠에서 값을 꺼내려다 전부 0 으로 찍힌다.
|
|
259
|
+
*/
|
|
235
260
|
export function 명세요약(명세) {
|
|
236
261
|
const m = 명세 ?? {};
|
|
262
|
+
if (!m.파일 && m.files) return specSummaryEn(m);
|
|
237
263
|
return [
|
|
238
264
|
`SBOM 부품 ${(m.파일?.length ?? 0).toLocaleString()}개 (전부 SHA-256 붙임)`,
|
|
239
265
|
`의존성 ${(m.의존성?.dependencies?.length ?? 0)}개 · 설치 스크립트 ${(m.의존성?.설치스크립트?.length ?? 0)}개`,
|
package/src/pack/selfpack.js
CHANGED
|
@@ -12,12 +12,14 @@ import { join, dirname, relative } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { makeZip } from './zip.js';
|
|
14
14
|
import { sbom, 심사명세 } from './sbom.js';
|
|
15
|
+
import { 언어 } from '../i18n/index.js';
|
|
16
|
+
import { reviewSheetEn, readMeEn } from './sheet.en.js';
|
|
15
17
|
|
|
16
18
|
export const repoRoot = () => join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
19
|
|
|
18
20
|
const SKIP_DIRS = new Set(['node_modules', '.git', '.github', 'test', '.deel']);
|
|
19
21
|
const SHIP = ['bin', 'src'];
|
|
20
|
-
const SHIP_FILES = ['package.json', 'README.md', 'README.
|
|
22
|
+
const SHIP_FILES = ['package.json', 'README.md', 'README.ko.md', 'LICENSE'];
|
|
21
23
|
|
|
22
24
|
function walk(dir, base, out) {
|
|
23
25
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -127,7 +129,10 @@ export function audit(root = repoRoot()) {
|
|
|
127
129
|
|
|
128
130
|
const 줄 = (n = 74) => '-'.repeat(n);
|
|
129
131
|
|
|
130
|
-
export function reviewSheet(a, at) {
|
|
132
|
+
export function reviewSheet(a, at, { lang = 언어() } = {}) {
|
|
133
|
+
// 한국어가 아니면 영어 서류 (pack/sheet.en.js). 같은 audit() 을 쓴다 —
|
|
134
|
+
// 말이 달라도 숫자가 달라지면 그건 서류가 거짓말을 하는 것이다.
|
|
135
|
+
if (lang !== 'ko') return reviewSheetEn(a, at, PROBES);
|
|
131
136
|
const L = [];
|
|
132
137
|
L.push('deel 사내 반입 심사 자료');
|
|
133
138
|
L.push(줄());
|
|
@@ -221,10 +226,11 @@ export function reviewSheet(a, at) {
|
|
|
221
226
|
/**
|
|
222
227
|
* 심사서 + 소스를 zip 하나로 묶는다.
|
|
223
228
|
*/
|
|
224
|
-
export function packSelf(outFile, { root = repoRoot(), at = new Date() } = {}) {
|
|
229
|
+
export function packSelf(outFile, { root = repoRoot(), at = new Date(), lang = 언어() } = {}) {
|
|
225
230
|
const a = audit(root);
|
|
226
231
|
const stamp = at.toISOString().replace('T', ' ').slice(0, 19);
|
|
227
|
-
const sheet = reviewSheet(a, stamp);
|
|
232
|
+
const sheet = reviewSheet(a, stamp, { lang });
|
|
233
|
+
const 한국어 = lang === 'ko';
|
|
228
234
|
|
|
229
235
|
/*
|
|
230
236
|
* 사람이 읽는 것 하나, 기계가 읽는 것 둘.
|
|
@@ -233,10 +239,20 @@ export function packSelf(outFile, { root = repoRoot(), at = new Date() } = {}) {
|
|
|
233
239
|
* 취약점 목록을 뽑고, 운영팀은 감사기록 사양을 보고 SIEM 수집 규칙을 짠다.
|
|
234
240
|
* 사람이 읽는 글은 그 어느 쪽에도 못 들어간다 — 그래서 셋을 다 넣는다.
|
|
235
241
|
*/
|
|
242
|
+
/*
|
|
243
|
+
* 파일 이름도 같이 옮긴다.
|
|
244
|
+
*
|
|
245
|
+
* 안이 영어인데 이름이 `반입심사서.txt` 면, 받은 사람은 열기 전에 무엇인지
|
|
246
|
+
* 모르고 메일에 첨부하면 이름이 깨져서 온다. 서류의 이름은 서류의 일부다.
|
|
247
|
+
*/
|
|
248
|
+
const 이름 = 한국어
|
|
249
|
+
? { 심사서: '반입심사서.txt', 명세: '심사명세.json', 안내: '읽어주세요.txt' }
|
|
250
|
+
: { 심사서: 'import-review.txt', 명세: 'audit-spec.json', 안내: 'READ-ME-FIRST.txt' };
|
|
251
|
+
|
|
236
252
|
const entries = [
|
|
237
|
-
{ name:
|
|
238
|
-
{ name: 'sbom.cdx.json', data: Buffer.from(JSON.stringify(sbom(a, { at }), null, 2), 'utf8'), mtime: at },
|
|
239
|
-
{ name:
|
|
253
|
+
{ name: 이름.심사서, data: Buffer.from(sheet, 'utf8'), mtime: at },
|
|
254
|
+
{ name: 'sbom.cdx.json', data: Buffer.from(JSON.stringify(sbom(a, { at, lang }), null, 2), 'utf8'), mtime: at },
|
|
255
|
+
{ name: 이름.명세, data: Buffer.from(JSON.stringify(심사명세(a, { at, lang }), null, 2), 'utf8'), mtime: at },
|
|
240
256
|
];
|
|
241
257
|
for (const f of a.files) {
|
|
242
258
|
entries.push({
|
|
@@ -247,8 +263,8 @@ export function packSelf(outFile, { root = repoRoot(), at = new Date() } = {}) {
|
|
|
247
263
|
});
|
|
248
264
|
}
|
|
249
265
|
entries.push({
|
|
250
|
-
name:
|
|
251
|
-
data: Buffer.from([
|
|
266
|
+
name: 이름.안내,
|
|
267
|
+
data: 한국어 ? Buffer.from([
|
|
252
268
|
'deel — 로컬 모델·사내 게이트웨이 코딩 에이전트',
|
|
253
269
|
'',
|
|
254
270
|
'쓰는 법 (설치 절차 없음)',
|
|
@@ -265,7 +281,7 @@ export function packSelf(outFile, { root = repoRoot(), at = new Date() } = {}) {
|
|
|
265
281
|
'',
|
|
266
282
|
' 세 파일 모두 소스를 훑어 자동으로 만든 것입니다. 손으로 적은 값이 아닙니다.',
|
|
267
283
|
'',
|
|
268
|
-
].join('\n'), 'utf8'),
|
|
284
|
+
].join('\n'), 'utf8') : Buffer.from(readMeEn(), 'utf8'),
|
|
269
285
|
mtime: at,
|
|
270
286
|
});
|
|
271
287
|
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// 반입 심사 서류의 영어판.
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 번역표가 아니라 딴 파일인가 ──────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 화면 말은 열쇠 하나에 한 문장이라 i18n 표가 맞다(src/i18n). 이건 다르다.
|
|
6
|
+
// **서류**다. 한 장이 통째로 하나의 글이고, 절 이름·차례·줄맞춤이 같이
|
|
7
|
+
// 뜻을 만든다. 그걸 열쇠 이백 개로 쪼개 두면, 고칠 때마다 두 파일을 오가며
|
|
8
|
+
// 짜맞춰야 하고 결국 한쪽만 고친 서류가 나간다.
|
|
9
|
+
//
|
|
10
|
+
// 이 저장소는 이미 같은 판단을 한 번 했다 — README.md 와 README.ko.md 를
|
|
11
|
+
// 갈라 뒀다. 서류는 서류대로 통째로 쓰는 편이 읽기도 고치기도 낫다.
|
|
12
|
+
//
|
|
13
|
+
// ── 대신 어긋나지 않게 검사한다 ─────────────────────────────────────────
|
|
14
|
+
//
|
|
15
|
+
// 갈라 두면 한쪽만 고치는 날이 온다. 그래서 test/packlang.test.js 가 두 판의
|
|
16
|
+
// **모양**을 견준다 — 절 수, 실린 파일 수, 통신 갈래 수. 글은 달라도 되지만
|
|
17
|
+
// 사실이 달라지면 그건 서류가 거짓말을 하는 것이다.
|
|
18
|
+
//
|
|
19
|
+
// ── 지어내지 않는다 ────────────────────────────────────────────────────
|
|
20
|
+
//
|
|
21
|
+
// 여기 적히는 값은 전부 selfpack.js 의 audit() 이 소스를 훑어 얻은 것이다.
|
|
22
|
+
// 한국어판과 같은 자료를 쓴다. 영어판이라고 다른 숫자가 나오면 안 된다.
|
|
23
|
+
|
|
24
|
+
const rule = (n = 74) => '-'.repeat(n);
|
|
25
|
+
|
|
26
|
+
/** 소스를 훑는 자리들. 한국어판 PROBES 와 같은 차례여야 한다. */
|
|
27
|
+
const PROBE_LABELS = {
|
|
28
|
+
net: { label: 'Network requests', note: 'Only to the address you entered in setup (through HTTPS_PROXY when one is set)' },
|
|
29
|
+
exec: { label: 'External commands', note: 'Commands you or the model asked for, plus git when installing a plugin' },
|
|
30
|
+
listen: { label: 'Listening ports', note: 'Should be none' },
|
|
31
|
+
eval: { label: 'String evaluation', note: 'Should be none' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 사람이 읽는 심사서 — 영어판.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} a selfpack.js 의 audit()
|
|
38
|
+
* @param {string} at 만든 시각
|
|
39
|
+
* @param {Array} probes selfpack.js 의 PROBES (차례를 그대로 따른다)
|
|
40
|
+
*/
|
|
41
|
+
export function reviewSheetEn(a, at, probes) {
|
|
42
|
+
const L = [];
|
|
43
|
+
L.push('deel — security review package');
|
|
44
|
+
L.push(rule());
|
|
45
|
+
L.push(`Name ${a.name}`);
|
|
46
|
+
L.push(`Version ${a.version}`);
|
|
47
|
+
L.push(`License ${a.license}`);
|
|
48
|
+
L.push(`Runtime Node ${a.node} (standard library only)`);
|
|
49
|
+
L.push(`Generated ${at}`);
|
|
50
|
+
L.push('');
|
|
51
|
+
|
|
52
|
+
L.push('1. Third-party dependencies');
|
|
53
|
+
L.push(rule());
|
|
54
|
+
L.push(` dependencies ${a.deps.length}${a.deps.length ? ' ' + a.deps.join(', ') : ' <- no third-party code is shipped'}`);
|
|
55
|
+
L.push(` devDependencies ${a.devDeps.length}`);
|
|
56
|
+
L.push(` external imports ${a.외부모듈.length}${a.외부모듈.length ? '' : ' <- only node: builtins and its own files'}`);
|
|
57
|
+
for (const x of a.외부모듈) L.push(` ${x}`);
|
|
58
|
+
L.push('');
|
|
59
|
+
|
|
60
|
+
L.push('2. Code that runs on install');
|
|
61
|
+
L.push(rule());
|
|
62
|
+
L.push(a.lifecycle.length
|
|
63
|
+
? ` Present: ${a.lifecycle.join(', ')} <- needs review`
|
|
64
|
+
: ' None <- no preinstall / install / postinstall / prepare');
|
|
65
|
+
L.push(' Unzip and run `node bin/deel.js`. There is no install step.');
|
|
66
|
+
L.push('');
|
|
67
|
+
|
|
68
|
+
L.push('3. Every outbound call site (found by scanning the source)');
|
|
69
|
+
L.push(rule());
|
|
70
|
+
for (const p of probes) {
|
|
71
|
+
const hits = a.calls[p.id];
|
|
72
|
+
const w = PROBE_LABELS[p.id] ?? { label: p.id, note: '' };
|
|
73
|
+
L.push(` [${w.label}] ${hits.length} ${w.note}`);
|
|
74
|
+
for (const h of hits) L.push(` ${h.file}:${h.line} ${h.text}`);
|
|
75
|
+
if (!hits.length) L.push(' (none)');
|
|
76
|
+
}
|
|
77
|
+
L.push('');
|
|
78
|
+
L.push(' Note: no endpoint is hard-coded in the source. deel only uses the address you');
|
|
79
|
+
L.push(' entered in setup. Config file: ~/.deel/config.json (or DEEL_API_KEY)');
|
|
80
|
+
L.push('');
|
|
81
|
+
|
|
82
|
+
L.push('3-1. Four separate outbound lanes that never mix');
|
|
83
|
+
L.push(rule());
|
|
84
|
+
L.push(' [A] Model gateway - the only lane that carries your source code');
|
|
85
|
+
L.push(' - Address: the single endpoint chosen in deel setup');
|
|
86
|
+
L.push(' - That one address is the entire allow-list. Switch models and the old one closes.');
|
|
87
|
+
L.push(' - src/safety/network.js checks every request. If the address is not on the');
|
|
88
|
+
L.push(' list, the request is never built.');
|
|
89
|
+
L.push(' - With HTTPS_PROXY (or proxy in the config) the call goes **through** that');
|
|
90
|
+
L.push(' proxy (CONNECT tunnel). The destination stays the same single address, and');
|
|
91
|
+
L.push(' the proxy in use is printed at startup and in /status. Requests to this');
|
|
92
|
+
L.push(' machine (127.*, localhost) never go through the proxy.');
|
|
93
|
+
L.push('');
|
|
94
|
+
L.push(' [B] Web reading (WebFetch tool) - fetch only');
|
|
95
|
+
L.push(' - GET only. No request body, so source and conversation cannot leave this way.');
|
|
96
|
+
L.push(' - This machine and private ranges (127.*, 10.*, 192.168.*, 172.16-31.*) are refused.');
|
|
97
|
+
L.push(' - The lane opens only while the tool runs and closes immediately after.');
|
|
98
|
+
L.push(' - Every address visited is written to the audit log.');
|
|
99
|
+
L.push('');
|
|
100
|
+
L.push(' [C] Plugin install (github) - only when you type /plugin install');
|
|
101
|
+
L.push(' - Open only while that command runs.');
|
|
102
|
+
L.push('');
|
|
103
|
+
L.push(' [D] MCP servers - separate child processes, someone else\'s program');
|
|
104
|
+
L.push(' - Off by default. A server runs only if you list it in .deel/mcp.json.');
|
|
105
|
+
L.push(' - Unlike A/B/C we cannot see which sockets that server opens. So instead of');
|
|
106
|
+
L.push(' filtering its requests, --offline does not start the server at all.');
|
|
107
|
+
L.push('');
|
|
108
|
+
L.push(' Starting with --offline blocks [B], [C] and [D]; traffic stays on this machine.');
|
|
109
|
+
L.push(' (Verified by the network / web / mcp checks in npm test - 123 assertions.)');
|
|
110
|
+
L.push('');
|
|
111
|
+
|
|
112
|
+
L.push('4. Skills and plugins');
|
|
113
|
+
L.push(rule());
|
|
114
|
+
L.push(' This package contains no skills and no plugins.');
|
|
115
|
+
L.push(' It only reads ~/.claude, ~/.deel and the project folder on the machine it runs on.');
|
|
116
|
+
L.push(' (Verified by the no-bundle check in npm test.)');
|
|
117
|
+
L.push('');
|
|
118
|
+
|
|
119
|
+
L.push(`5. ${a.files.length} files shipped - SHA-256`);
|
|
120
|
+
L.push(rule());
|
|
121
|
+
const w = Math.max(...a.files.map((f) => f.path.length));
|
|
122
|
+
for (const f of a.files) {
|
|
123
|
+
L.push(` ${f.path.padEnd(w)} ${String(f.bytes).padStart(7)}B ${f.sha}`);
|
|
124
|
+
}
|
|
125
|
+
L.push('');
|
|
126
|
+
L.push(' Verify these yourself with:');
|
|
127
|
+
L.push(' sha256sum <file> (Linux, macOS)');
|
|
128
|
+
L.push(' certutil -hashfile <file> SHA256 (Windows)');
|
|
129
|
+
L.push('');
|
|
130
|
+
return L.join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** zip 맨 앞에 넣는 안내 — 영어판. */
|
|
134
|
+
export function readMeEn() {
|
|
135
|
+
return [
|
|
136
|
+
'deel - a coding agent CLI for local models and in-house gateways',
|
|
137
|
+
'',
|
|
138
|
+
'How to run it (there is no install step)',
|
|
139
|
+
' 1. Unzip this archive anywhere.',
|
|
140
|
+
' 2. Check that Node 20 or newer is present: node -v',
|
|
141
|
+
' 3. Choose a connection: node deel/bin/deel.js setup',
|
|
142
|
+
' 4. Start talking: node deel/bin/deel.js',
|
|
143
|
+
'',
|
|
144
|
+
'For the security reviewer',
|
|
145
|
+
' import-review.txt The review package in prose: dependencies, install scripts,',
|
|
146
|
+
' every network call site, and a SHA-256 for each file.',
|
|
147
|
+
' sbom.cdx.json SBOM (CycloneDX 1.5). Feed it straight to your scanner.',
|
|
148
|
+
' audit-spec.json Egress list, audit-log specification, file hashes - machine readable.',
|
|
149
|
+
'',
|
|
150
|
+
' All three are generated by scanning the source. None of it is written by hand.',
|
|
151
|
+
'',
|
|
152
|
+
].join('\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 심사명세 — 영어판.
|
|
157
|
+
*
|
|
158
|
+
* 한국어판(sbom.js 의 심사명세)과 **같은 자료, 같은 차례**다. 열쇠 이름까지
|
|
159
|
+
* 영어라야 뜻이 있다 — 스캐너에 넣는 사람이 `통신` 을 읽을 수는 없다.
|
|
160
|
+
*/
|
|
161
|
+
export function specEn(a, at, 통신, 열쇠, 감사) {
|
|
162
|
+
return {
|
|
163
|
+
product: a.name,
|
|
164
|
+
version: a.version,
|
|
165
|
+
license: a.license,
|
|
166
|
+
runtime: `Node ${a.node} (standard library only)`,
|
|
167
|
+
generated: at.toISOString(),
|
|
168
|
+
dependencies: {
|
|
169
|
+
dependencies: a.deps,
|
|
170
|
+
devDependencies: a.devDeps,
|
|
171
|
+
installScripts: a.lifecycle,
|
|
172
|
+
externalImportsInSource: a.외부모듈,
|
|
173
|
+
summary: a.deps.length === 0 && a.외부모듈.length === 0
|
|
174
|
+
? 'No third-party code is bundled. Only node: builtins and its own files are imported.'
|
|
175
|
+
: 'External modules are present - review the list above.',
|
|
176
|
+
},
|
|
177
|
+
egress: 통신,
|
|
178
|
+
keyStorage: 열쇠,
|
|
179
|
+
auditLog: 감사,
|
|
180
|
+
files: a.files.map((f) => ({ path: f.path, bytes: f.bytes, sha256: f.sha })),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 나가는 길 — 영어판. 자리(소스 줄 번호)는 부르는 쪽이 넣어 준다. */
|
|
185
|
+
export function egressEn(자리, a) {
|
|
186
|
+
return {
|
|
187
|
+
lanes: [
|
|
188
|
+
{
|
|
189
|
+
lane: 'Model gateway',
|
|
190
|
+
when: 'On every turn you type',
|
|
191
|
+
where: 'The single address chosen in deel setup',
|
|
192
|
+
what: 'The conversation, and the file contents the model read',
|
|
193
|
+
control: 'src/safety/network.js checks the allow-list on every request. If the address is not listed, the request is never built.',
|
|
194
|
+
proxy: 'With HTTPS_PROXY (or proxy in the config) the call goes through that proxy (CONNECT tunnel). The destination allow-list is unchanged, and addresses on this machine bypass the proxy.',
|
|
195
|
+
source: 자리('net'),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
lane: 'Web reading (WebFetch)',
|
|
199
|
+
when: 'Only when the model calls that tool',
|
|
200
|
+
where: 'An address the model chose. This machine and private ranges are refused',
|
|
201
|
+
what: 'Nothing - GET only, so no request body is sent',
|
|
202
|
+
control: 'Blocked entirely under --offline.',
|
|
203
|
+
source: 자리('net'),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
lane: 'Plugin install',
|
|
207
|
+
when: 'Only when you type /plugin install',
|
|
208
|
+
where: 'github.com',
|
|
209
|
+
what: 'Nothing',
|
|
210
|
+
control: 'Blocked entirely under --offline.',
|
|
211
|
+
source: 자리('net'),
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
listeningPorts: {
|
|
215
|
+
count: (a.calls?.listen ?? []).length,
|
|
216
|
+
use: 'Only /preview, which serves the page you just built. It binds to 127.0.0.1 and closes when the command ends.',
|
|
217
|
+
source: 자리('listen'),
|
|
218
|
+
},
|
|
219
|
+
externalCommands: {
|
|
220
|
+
count: (a.calls?.exec ?? []).length,
|
|
221
|
+
use: 'Commands you or the model asked for, starting MCP servers, and git when installing a plugin.',
|
|
222
|
+
source: 자리('exec'),
|
|
223
|
+
},
|
|
224
|
+
stringEvaluation: {
|
|
225
|
+
count: (a.calls?.eval ?? []).length,
|
|
226
|
+
use: 'Should be none.',
|
|
227
|
+
source: 자리('eval'),
|
|
228
|
+
},
|
|
229
|
+
whenOffline: [
|
|
230
|
+
'Web reading (WebFetch) is blocked.',
|
|
231
|
+
'Plugin install is blocked.',
|
|
232
|
+
'MCP servers are not started - we cannot control where a child process connects.',
|
|
233
|
+
'The model gateway still works. If that address is on this machine, nothing leaves it at all.',
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 열쇠 보관 — 영어판. 이 PC 에서 실제로 쓰는 방식은 부르는 쪽이 넣어 준다. */
|
|
239
|
+
export function keyStorageEn(이PC에서) {
|
|
240
|
+
return {
|
|
241
|
+
where: 'profiles[].apiKey in ~/.deel/config.json (or DEEL_HOME)',
|
|
242
|
+
onThisMachine: 이PC에서,
|
|
243
|
+
methods: [
|
|
244
|
+
{ os: 'Windows', how: 'DPAPI ProtectedData (CurrentUser) - only this account on this machine can unseal it. Copying the file elsewhere does not help.' },
|
|
245
|
+
{ os: 'macOS', how: 'Login keychain (security add-generic-password)' },
|
|
246
|
+
{ os: 'Other', how: 'File mode 0600. There is no OS keystore, so we say so plainly.' },
|
|
247
|
+
],
|
|
248
|
+
howItIsPassed: 'The key never appears on a command line - it is always passed over stdin, so another user on the same machine cannot read it from the process list.',
|
|
249
|
+
avoidTheFile: 'Set DEEL_API_KEY (or DEEL_KEY_<profile>) and nothing is written to the file at all. The environment variable wins over the file.',
|
|
250
|
+
disable: 'DEEL_KEYSTORE=off - no OS keystore, file permissions only.',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 감사기록 사양 — 영어판. */
|
|
255
|
+
export function auditLogEn() {
|
|
256
|
+
return {
|
|
257
|
+
location: '<workdir>/.deel/audit.jsonl',
|
|
258
|
+
format: 'JSON Lines (UTF-8, one record per line)',
|
|
259
|
+
growth: 'Append only. Records are never edited or removed.',
|
|
260
|
+
retention: 'deel never deletes it. Set retention according to your own policy.',
|
|
261
|
+
everyLine: [
|
|
262
|
+
{ field: 'at', meaning: 'When (ISO 8601, UTC)' },
|
|
263
|
+
{ field: 'session', meaning: 'Which conversation - an id derived from the start time' },
|
|
264
|
+
{ field: 'kind', meaning: 'Record type - see below' },
|
|
265
|
+
],
|
|
266
|
+
kinds: [
|
|
267
|
+
{ kind: 'turn', meaning: 'Something the user asked for', fields: ['text'], note: 'First 500 characters only' },
|
|
268
|
+
{ kind: 'tool', meaning: 'A tool call', fields: ['tool', 'target', 'ok', 'note'], note: 'target is a file path, a command, or a search pattern' },
|
|
269
|
+
{ kind: 'blocked', meaning: 'Something a safety check refused', fields: ['why', 'what'], note: 'The refused command is kept verbatim' },
|
|
270
|
+
{ kind: 'undo', meaning: 'A rollback', fields: ['files', 'turns'] },
|
|
271
|
+
],
|
|
272
|
+
neverRecorded: [
|
|
273
|
+
'Keys and passwords. Neither apiKey from the config nor a spreadsheet password reaches the log.',
|
|
274
|
+
'Whole file contents. What was touched is recorded; what was written is not.',
|
|
275
|
+
'Model replies. Those live in the session transcript (.deel/sessions) instead.',
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 사람이 읽을 짧은 요약 — 영어판. */
|
|
281
|
+
export function specSummaryEn(m = {}) {
|
|
282
|
+
return [
|
|
283
|
+
`SBOM components ${(m.files?.length ?? 0).toLocaleString()} (each with SHA-256)`,
|
|
284
|
+
`Dependencies ${(m.dependencies?.dependencies?.length ?? 0)} - install scripts ${(m.dependencies?.installScripts?.length ?? 0)}`,
|
|
285
|
+
`Egress lanes ${(m.egress?.lanes?.length ?? 0)} - listening ports ${(m.egress?.listeningPorts?.count ?? 0)}`,
|
|
286
|
+
`Audit records ${(m.auditLog?.kinds?.length ?? 0)} kinds - ${m.auditLog?.location ?? ''}`,
|
|
287
|
+
].join('\n');
|
|
288
|
+
}
|