leerness 1.9.287 → 1.9.289
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/CHANGELOG.md +35 -0
- package/README.md +6 -6
- package/bin/harness.js +69 -20
- package/package.json +1 -1
- package/scripts/e2e.js +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.289 — 2026-06-03 — UR-0036 (Codex #3): REPL agy/copilot 프롬프트 셸 주입 차단
|
|
4
|
+
|
|
5
|
+
**🔒 REPL 스트리밍에서 agy/copilot 프롬프트가 `shell:true` 인자로 raw 전달돼 셸 분리/주입되던 보안 갭 수정 (Codex gpt-5.5 #3).**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
claude/codex 는 1.9.188 에서 프롬프트를 stdin 으로 전달(셸 escape 우회)하나, agy(`-p`)/copilot(`gh copilot suggest`)은 인자 모드만 지원해 `promptText` 를 args 에 raw 로 넣었음. `cp.spawn(cmd, args, {shell:true})` 는 args 를 따옴표 없이 조인하므로 `agy -p hello; rm -rf` 처럼 프롬프트의 `;`/`&&`/`$(...)`/공백이 셸에 해석됨.
|
|
9
|
+
|
|
10
|
+
### 구현
|
|
11
|
+
1. **`_shellQuoteArg(s)` 순수 헬퍼** — POSIX(sh): single-quote(bulletproof, 내부 `'`→`'\''`). Windows(cmd.exe): double-quote + 내부 `"`→`""` (공백/`&`/`|`/`<`/`>`/`(`/`)`/`;`/`$()` 모두 리터럴화).
|
|
12
|
+
2. **agy/copilot args 안전 인용** — `['-p', _shellQuoteArg(promptText)]` / `['copilot','suggest',_shellQuoteArg(promptText)]`. 프롬프트가 단일 리터럴 인자로 전달.
|
|
13
|
+
3. selftest 36→37 + e2e 233→234.
|
|
14
|
+
|
|
15
|
+
### 검증
|
|
16
|
+
- **selftest 37/37 PASS** · **E2E 234/234 PASS** (회귀 0) · `a; rm -rf / && echo $(whoami)` → 단일 인용 리터럴 확인 (POSIX/Windows 분기).
|
|
17
|
+
- 남은 백로그: UR-0037(#4) require 시 top-level side effect 격리.
|
|
18
|
+
|
|
19
|
+
## 1.9.288 — 2026-06-03 — Codex gpt-5.5 코드 리뷰 수렴: MCP policy enforce + release dry-run + 도구수 정합
|
|
20
|
+
|
|
21
|
+
**🔒 Codex(gpt-5.5, xhigh)가 실제 코드 라인 근거로 제시한 5건 중 검증된 high/med 3건 수렴 + 나머지 2건 백로그.**
|
|
22
|
+
|
|
23
|
+
### 배경
|
|
24
|
+
codex CLI(gpt-5.5)로 leerness@1.9.287 을 직접 리뷰(샌드박스 우회로 파일 접근, 285k 토큰). 5건 모두 코드 라인 근거의 타당한 지적. 이번 라운드에 검증·테스트 가능한 3건 반영.
|
|
25
|
+
|
|
26
|
+
### 구현 (Codex 지적 → 수렴)
|
|
27
|
+
1. **#1 (high) MCP policy enforce 우회** — `_policyEnforce` 가 `agents multi --execute` 한 곳뿐이라 MCP `state_start` 등 write 도구가 정책을 우회. → MCP `tools/call` 의 `callLeerness` 직전에 **중앙 정책 게이트** 추가. enforce ON 시 허용 등급 초과 도구는 JSON-RPC `isError` 로 차단(실행 안 함). read-only enforce 에서 MCP state_start 차단 + state.json 미생성 실측.
|
|
28
|
+
2. **#2 (high) `release publish --dry-run` 이 실제 push + 실패도 성공 처리** — dry-run 인데 npm pack/git push 실행, status 미검사로 실패 시에도 exit 0. → dry-run 은 모든 외부 side effect(pack/push/gh/pages)를 **계획 출력만**, live 는 git push status 실패 시 **non-zero 종료**.
|
|
29
|
+
3. **#5 (med) 도구 수 불일치** — 배지 80 / 관리블록 42 / capability 카운트 자기-매칭 80 vs 실제 tools/list 79. → `_mcpToolCount()` 단일 출처(정확한 도구 정의 패턴)로 배지·관리블록·capability·CHANGELOG 카운트 통일. syncReadme 가 MCP 배지 자동 동기화(stale 방지).
|
|
30
|
+
|
|
31
|
+
### 백로그 (Codex #3/#4)
|
|
32
|
+
- **UR-0036** (#3): REPL agy/copilot prompt 가 shell:true args 전달(셸 주입 위험) → stdin/shell-safe 통일.
|
|
33
|
+
- **UR-0037** (#4): require.main 가드에도 top-level side effect(warning listener/NODE_OPTIONS/chcp) 가 require 시 실행 → main() 내부 이동.
|
|
34
|
+
|
|
35
|
+
### 검증
|
|
36
|
+
- **selftest 36/36 PASS** · **E2E 233/233 PASS** (회귀 0) · MCP 정책 차단(write)/허용(enforce off) + release dry-run 무push exit 0 + 배지==tools/list(79) 실측.
|
|
37
|
+
|
|
3
38
|
## 1.9.287 — 2026-06-03 — Codex 외부 리뷰 수렴: 허위 완료 차단 강화 + handoff 펜스 + status minimal
|
|
4
39
|
|
|
5
40
|
**🛡️ 다른 AI(Codex)가 실측한 핵심 한계 "테스트만 통과하면 미구현도 done 통과"를 보강 + 발견된 품질 버그 2건 수정.**
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -471,7 +471,7 @@ MIT — © leerness contributors
|
|
|
471
471
|
<!-- leerness:project-readme:start -->
|
|
472
472
|
## Leerness Project Harness
|
|
473
473
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.289 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### Core Commands
|
|
477
477
|
|
|
@@ -513,7 +513,7 @@ leerness memory restore decision <date|title>
|
|
|
513
513
|
|
|
514
514
|
### MCP server (외부 AI 통합)
|
|
515
515
|
|
|
516
|
-
Leerness v1.9.
|
|
516
|
+
Leerness v1.9.289는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **79개 도구**를 노출:
|
|
517
517
|
|
|
518
518
|
```jsonc
|
|
519
519
|
// 카테고리별
|
|
@@ -526,7 +526,7 @@ Leerness v1.9.287는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
526
526
|
// • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new
|
|
527
527
|
|
|
528
528
|
// MCP server 실행: leerness mcp serve
|
|
529
|
-
// tools/list 응답:
|
|
529
|
+
// tools/list 응답: 79 도구
|
|
530
530
|
```
|
|
531
531
|
|
|
532
532
|
### Autonomous mode (자율 모드)
|
|
@@ -534,7 +534,7 @@ Leerness v1.9.287는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
534
534
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
535
535
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
536
536
|
|
|
537
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
537
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.289)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
538
538
|
|
|
539
539
|
### 성능 가이드 (1.9.140 측정)
|
|
540
540
|
|
|
@@ -572,6 +572,6 @@ leerness release pack --close --auto-main-push
|
|
|
572
572
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
573
573
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
574
574
|
|
|
575
|
-
Last synced by Leerness v1.9.
|
|
575
|
+
Last synced by Leerness v1.9.289: 2026-06-03
|
|
576
576
|
<!-- leerness:project-readme:end -->
|
|
577
577
|
|
package/bin/harness.js
CHANGED
|
@@ -10,7 +10,7 @@ const readline = require('readline');
|
|
|
10
10
|
const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
11
11
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord } = require('../lib/pure-utils');
|
|
12
12
|
|
|
13
|
-
const VERSION = '1.9.
|
|
13
|
+
const VERSION = '1.9.289';
|
|
14
14
|
|
|
15
15
|
// 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
|
|
16
16
|
// leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
|
|
@@ -152,6 +152,18 @@ function fail(s) { log('✗ ' + s); }
|
|
|
152
152
|
// 1.9.287 (Codex 리뷰 수렴): 임베딩 텍스트의 코드펜스(```)를 중립화해 외부 마크다운(session-handoff)이 깨지지 않게.
|
|
153
153
|
// ``` → ''' (펜스가 아닌 표시), 인라인 백틱은 보존. 순수 함수.
|
|
154
154
|
function _sanitizeFences(s) { return String(s || '').replace(/```+/g, "'''"); }
|
|
155
|
+
// 1.9.289 (Codex gpt-5.5 리뷰 #3 수렴): shell:true spawn 의 인자를 셸-안전하게 인용 — 프롬프트 셸 주입/분리 방지.
|
|
156
|
+
// POSIX(sh): single-quote (bulletproof). Windows(cmd.exe): double-quote + inner " 이스케이프 (공백/&|<>() 안전).
|
|
157
|
+
function _shellQuoteArg(s) {
|
|
158
|
+
s = String(s == null ? '' : s);
|
|
159
|
+
if (process.platform === 'win32') return '"' + s.replace(/"/g, '""') + '"';
|
|
160
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
161
|
+
}
|
|
162
|
+
// 1.9.288 (Codex gpt-5.5 리뷰 #5 수렴): MCP 도구 수 단일 출처 — tools 정의 패턴만 카운트(설명/자기-매칭 오탐 제외).
|
|
163
|
+
// 배지/관리블록/capability 가 모두 이 값을 써서 tools/list 실제 수와 일치.
|
|
164
|
+
function _mcpToolCount() {
|
|
165
|
+
try { return (read(__filename).match(/\{ name: 'leerness_[a-z_]+', description:/g) || []).length; } catch { return 0; }
|
|
166
|
+
}
|
|
155
167
|
function absRoot(p) { return path.resolve(p || process.cwd()); }
|
|
156
168
|
function exists(p) { return fs.existsSync(p); }
|
|
157
169
|
function read(p) {
|
|
@@ -317,7 +329,7 @@ function managedReadmeBlock(project) {
|
|
|
317
329
|
'',
|
|
318
330
|
'### MCP server (외부 AI 통합)',
|
|
319
331
|
'',
|
|
320
|
-
`Leerness v${VERSION}는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에
|
|
332
|
+
`Leerness v${VERSION}는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **${_mcpToolCount()}개 도구**를 노출:`,
|
|
321
333
|
'',
|
|
322
334
|
'```jsonc',
|
|
323
335
|
'// 카테고리별',
|
|
@@ -330,7 +342,7 @@ function managedReadmeBlock(project) {
|
|
|
330
342
|
'// • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new',
|
|
331
343
|
'',
|
|
332
344
|
'// MCP server 실행: leerness mcp serve',
|
|
333
|
-
|
|
345
|
+
`// tools/list 응답: ${_mcpToolCount()} 도구`,
|
|
334
346
|
'```',
|
|
335
347
|
'',
|
|
336
348
|
'### Autonomous mode (자율 모드)',
|
|
@@ -813,6 +825,11 @@ function syncReadme(root) {
|
|
|
813
825
|
const stCount = _selfTestCases().length;
|
|
814
826
|
if (stCount > 0) updated = updated.replace(/badge\/selftest-\d+(?:%2F\d+)?/g, `badge/selftest-${stCount}`);
|
|
815
827
|
} catch {}
|
|
828
|
+
// 1.9.288 (Codex #5): MCP 도구 배지 자동 동기화 — tools/list 실제 수와 일치 (badge 80↔실제 79 불일치 해소).
|
|
829
|
+
try {
|
|
830
|
+
const mcpN = _mcpToolCount();
|
|
831
|
+
if (mcpN > 0) updated = updated.replace(/badge\/MCP--tools-\d+/g, `badge/MCP--tools-${mcpN}`);
|
|
832
|
+
} catch {}
|
|
816
833
|
} catch {}
|
|
817
834
|
if (updated !== existing) writeUtf8(p, updated);
|
|
818
835
|
ok('README.md Leerness section synced');
|
|
@@ -2972,6 +2989,8 @@ function _selfTestCases() {
|
|
|
2972
2989
|
{ name: 'CAPABILITY_SURFACE: 6 영역 + risk/optOut + 주의명령 (1.9.272)', run: () => { const ks = Object.keys(CAPABILITY_SURFACE); return ks.length === 6 && ks.includes('automationBridges') && Object.values(CAPABILITY_SURFACE).every(v => ['low','medium','high'].includes(v.risk) && v.optOut && v.desc) && POWERFUL_COMMANDS.length >= 5; } },
|
|
2973
2990
|
{ name: '_resolveNpmTag: latest 기본 + next 허용 + 잘못된 형식 폴백 (1.9.275)', run: () => _resolveNpmTag(null, {}) === 'latest' && _resolveNpmTag('next', {}) === 'next' && _resolveNpmTag(null, { LEERNESS_NPM_TAG: 'next' }) === 'next' && _resolveNpmTag('bad tag!', {}) === 'latest' && _resolveNpmTag('Beta', {}) === 'beta' },
|
|
2974
2991
|
{ name: '_sanitizeFences: 코드펜스 중립화 (Codex 수렴 1.9.287)', run: () => { const out = _sanitizeFences('text\n```js\ncode\n```\nmore'); return !/```/.test(out) && /'''/.test(out) && /code/.test(out); } },
|
|
2992
|
+
{ name: '_mcpToolCount: 실제 도구 정의 수 (자기-매칭 오탐 없음) (Codex #5 1.9.288)', run: () => { const n = _mcpToolCount(); return n >= 75 && n < 200; } },
|
|
2993
|
+
{ name: '_shellQuoteArg: 프롬프트 셸 주입 중립화 (Codex #3 1.9.289)', run: () => { const q = _shellQuoteArg('a; rm -rf / && echo x'); if (process.platform === 'win32') return q === '"a; rm -rf / && echo x"' && _shellQuoteArg('a"b') === '"a""b"'; return q === "'a; rm -rf / && echo x'" && _shellQuoteArg("it's") === "'it'\\''s'"; } },
|
|
2975
2994
|
{ name: '_evidenceQuality: 파일+테스트 근거 강제 (Codex 수렴 1.9.287)', run: () => { const good = _evidenceQuality('src/api.js 수정, npm test 12/12 통과 (Exit: 0)'); const weak = _evidenceQuality('테스트 통과함'); const noFile = _evidenceQuality('12 tests passed'); return good.ok === true && good.hasFile && good.hasTest && weak.ok === false && weak.missing.includes('수정 파일 경로') && noFile.ok === false; } },
|
|
2976
2995
|
{ name: '_parseEvidenceStats: review-evidence pass/fail 집계 (1.9.286)', run: () => { const s = _parseEvidenceStats('## 2026-06-03\nCommand: npm test\nExit: 0\n\n## 2026-06-03\nCommand: build\nExit: 1\n\n## 2026-06-03\nverify PASS 통과\n'); return s.entries === 3 && s.pass === 2 && s.fail === 1 && s.rate === 67; } },
|
|
2977
2996
|
{ name: '_reuseDetect: 키워드→OSS 카테고리 + 체크리스트 (1.9.285)', run: () => { const a = _reuseDetect('JWT 인증 구현'); const b = _reuseDetect('날짜 포맷 date'); const c = _reuseDetect('전혀무관한xyzzy'); return a.some(x => x.key === 'auth') && b.some(x => x.key === 'date') && c.length === 0 && REUSE_CHECKLIST.length >= 5 && REUSE_CATEGORIES.length >= 12; } },
|
|
@@ -14562,20 +14581,31 @@ function releasePublish(root) {
|
|
|
14562
14581
|
if (remote) log(`Git remote (origin): ${remote.host === 'github' ? `${remote.owner}/${remote.repo}` : remote.url}`);
|
|
14563
14582
|
else log('Git remote: 없음');
|
|
14564
14583
|
|
|
14584
|
+
// 1.9.288 (Codex gpt-5.5 리뷰 #2 수렴): dry-run 은 모든 외부 side effect 를 계획 출력으로만 + 각 단계 status 실패 시 non-zero.
|
|
14585
|
+
let pubFail = false;
|
|
14565
14586
|
// 2. npm pack (필요한 경우 — pack-only도 의미 있음)
|
|
14566
14587
|
if (has('--pack') || has('--npm-publish') || (!has('--git-push') && !has('--gh-release') && !has('--gh-pages'))) {
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14588
|
+
if (dryRun) { log('(dry-run) npm pack 생략 (실행 안 함)'); }
|
|
14589
|
+
else {
|
|
14590
|
+
const packR = cp.spawnSync('npm', ['pack'], { cwd: root, encoding: 'utf8', shell: true });
|
|
14591
|
+
if (packR.status !== 0) { fail('npm pack 실패'); log(packR.stderr); process.exitCode = 1; return; }
|
|
14592
|
+
ok('npm pack 완료');
|
|
14593
|
+
}
|
|
14570
14594
|
}
|
|
14571
14595
|
|
|
14572
14596
|
// 3. git push (--git-push 또는 --auto + remote 있을 때)
|
|
14573
14597
|
if (has('--git-push') || (has('--auto') && remote)) {
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14598
|
+
if (dryRun) {
|
|
14599
|
+
log('(dry-run) git push / git push --tags 생략 (실행 안 함)');
|
|
14600
|
+
} else {
|
|
14601
|
+
log('git push:');
|
|
14602
|
+
const r1 = cp.spawnSync('git', ['push'], { cwd: root, encoding: 'utf8', shell: true });
|
|
14603
|
+
log((r1.stdout || r1.stderr || '').slice(-200) || '(no output)');
|
|
14604
|
+
if (r1.status !== 0) { fail('git push 실패'); pubFail = true; }
|
|
14605
|
+
const r2 = cp.spawnSync('git', ['push', '--tags'], { cwd: root, encoding: 'utf8', shell: true });
|
|
14606
|
+
log((r2.stdout || r2.stderr || '').slice(-200) || '(no output)');
|
|
14607
|
+
if (r2.status !== 0) { fail('git push --tags 실패'); pubFail = true; }
|
|
14608
|
+
}
|
|
14579
14609
|
}
|
|
14580
14610
|
|
|
14581
14611
|
// 4. GitHub Release (--gh-release, gh CLI 사용)
|
|
@@ -14584,6 +14614,7 @@ function releasePublish(root) {
|
|
|
14584
14614
|
else {
|
|
14585
14615
|
const v = getCurrentVersion(root);
|
|
14586
14616
|
if (!v) { warn('--gh-release: package.json#version 없음 — 스킵'); }
|
|
14617
|
+
else if (dryRun) { log(`(dry-run) gh release create v${v} 생략 (실행 안 함)`); }
|
|
14587
14618
|
else {
|
|
14588
14619
|
const tag = `v${v}`;
|
|
14589
14620
|
const ghArgs = ['release', 'create', tag, '--generate-notes', '--title', `${remote.repo} ${tag}`];
|
|
@@ -14600,8 +14631,11 @@ function releasePublish(root) {
|
|
|
14600
14631
|
|
|
14601
14632
|
// 5. gh-pages 배포 (--gh-pages)
|
|
14602
14633
|
if (has('--gh-pages')) {
|
|
14603
|
-
|
|
14604
|
-
|
|
14634
|
+
if (dryRun) { log('(dry-run) gh-pages 배포 생략 (실행 안 함)'); }
|
|
14635
|
+
else {
|
|
14636
|
+
const src = arg('--gh-pages-src', null) || arg('--roadmap', null) || 'roadmap.html';
|
|
14637
|
+
deployGhPages(root, src);
|
|
14638
|
+
}
|
|
14605
14639
|
}
|
|
14606
14640
|
|
|
14607
14641
|
// 6. npm publish (--npm-publish)
|
|
@@ -14614,7 +14648,9 @@ function releasePublish(root) {
|
|
|
14614
14648
|
log((r.stdout || '').split('\n').slice(-5).join('\n'));
|
|
14615
14649
|
if (r.status !== 0) { fail('npm publish 실패'); process.exitCode = 1; return; }
|
|
14616
14650
|
}
|
|
14617
|
-
|
|
14651
|
+
// 1.9.288 (Codex #2): 단계 실패가 있으면 완료를 성공으로 출력하지 않고 non-zero 종료.
|
|
14652
|
+
if (pubFail) { fail('release publish: 일부 단계 실패 (위 로그 확인)'); process.exitCode = 1; return; }
|
|
14653
|
+
ok(`release publish 완료${dryRun ? ' (dry-run — 실제 변경 없음)' : ''}`);
|
|
14618
14654
|
}
|
|
14619
14655
|
|
|
14620
14656
|
// ===== 1.9.7 A: verify-code — npm scripts 자동 감지 + evidence 자동 기록 =====
|
|
@@ -16887,6 +16923,15 @@ function mcpServeCmd(root) {
|
|
|
16887
16923
|
default:
|
|
16888
16924
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
16889
16925
|
}
|
|
16926
|
+
// 1.9.288 (Codex gpt-5.5 리뷰 #1 수렴): MCP 도구도 policy enforce 적용 — read-only enforce 시 write 도구 차단.
|
|
16927
|
+
// 이전: _policyEnforce 는 agents multi --execute 한 곳뿐 → MCP state_start 등이 정책 우회하고 .leerness 기록.
|
|
16928
|
+
// cliArgs(실제 실행 명령) 로 required tier 판정 → enforce ON 이고 초과 시 JSON-RPC error 반환(실행 안 함).
|
|
16929
|
+
try {
|
|
16930
|
+
const pol = _policyEnforce(targetPath, cliArgs.join(' '));
|
|
16931
|
+
if (!pol.allowed) {
|
|
16932
|
+
return send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `정책 차단(policy): ${pol.reason}` }], isError: true } });
|
|
16933
|
+
}
|
|
16934
|
+
} catch {}
|
|
16890
16935
|
const r = callLeerness(cliArgs);
|
|
16891
16936
|
// 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
|
|
16892
16937
|
const fullText = r.stdout || r.stderr || '(no output)';
|
|
@@ -17485,8 +17530,9 @@ async function _cliChatStream(root, provider, promptText, opts) {
|
|
|
17485
17530
|
args = ['exec', '--skip-git-repo-check', '-']; // - = stdin from claude/codex convention
|
|
17486
17531
|
useStdinForPrompt = true;
|
|
17487
17532
|
}
|
|
17488
|
-
|
|
17489
|
-
else if (provider === '
|
|
17533
|
+
// 1.9.289 (Codex #3): agy/copilot 은 인자 모드만 지원 → shell:true 조인 시 프롬프트가 분리/주입될 수 있어 _shellQuoteArg 로 안전 인용.
|
|
17534
|
+
else if (provider === 'agy') { cmd = 'agy'; args = ['-p', _shellQuoteArg(promptText)]; } // agy -p 는 인자 모드만 지원
|
|
17535
|
+
else if (provider === 'copilot') { cmd = 'gh'; args = ['copilot', 'suggest', _shellQuoteArg(promptText)]; }
|
|
17490
17536
|
else return { ok: false, error: `provider ${provider} 미지원`, provider };
|
|
17491
17537
|
const t0 = Date.now();
|
|
17492
17538
|
return new Promise(resolve => {
|
|
@@ -19976,9 +20022,8 @@ function healthCmd(root) {
|
|
|
19976
20022
|
cap.replMultiProvider = (hasRepl && hasCliChat)
|
|
19977
20023
|
? { score: 90, status: '✓', evidence: 'ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)' }
|
|
19978
20024
|
: { score: 30, status: '⚠', evidence: 'REPL 미완성' };
|
|
19979
|
-
// (5) MCP 도구 — tools array 카운트
|
|
19980
|
-
const
|
|
19981
|
-
const toolCount = toolsMatch ? toolsMatch.length : 0;
|
|
20025
|
+
// (5) MCP 도구 — tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
|
|
20026
|
+
const toolCount = _mcpToolCount();
|
|
19982
20027
|
cap.mcpTools = toolCount >= 50
|
|
19983
20028
|
? { score: 100, status: '✓', evidence: `${toolCount}/50+ 도구 (1.9.159 CRUD 완성)` }
|
|
19984
20029
|
: { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: `${toolCount} 도구` };
|
|
@@ -21611,5 +21656,9 @@ module.exports = {
|
|
|
21611
21656
|
// 1.9.286: 스킬 영향 상관추적 (UR-0024) — 단위 테스트
|
|
21612
21657
|
_parseEvidenceStats, skillImpactCmd,
|
|
21613
21658
|
// 1.9.287: evidence 완전성 + 코드펜스 sanitize (Codex 리뷰 수렴) — 단위 테스트
|
|
21614
|
-
_evidenceQuality, _sanitizeFences
|
|
21659
|
+
_evidenceQuality, _sanitizeFences,
|
|
21660
|
+
// 1.9.288: MCP 도구 수 단일 출처 (Codex #5) — 단위 테스트
|
|
21661
|
+
_mcpToolCount,
|
|
21662
|
+
// 1.9.289: shell-safe 인용 (Codex #3) — 단위 테스트
|
|
21663
|
+
_shellQuoteArg
|
|
21615
21664
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -3042,5 +3042,59 @@ total++;
|
|
|
3042
3042
|
if (!ok) { failed++; console.log((rFake.stdout || '').slice(-300)); }
|
|
3043
3043
|
}
|
|
3044
3044
|
|
|
3045
|
+
// 1.9.288 회귀 (Codex gpt-5.5 리뷰 수렴): MCP policy enforce + release dry-run + 도구수 정합
|
|
3046
|
+
total++;
|
|
3047
|
+
{
|
|
3048
|
+
const env = { ...process.env, LEERNESS_OFFLINE: '1' };
|
|
3049
|
+
// #1 MCP policy: read-only enforce → state_start(write) 차단 + state.json 미생성
|
|
3050
|
+
const pDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mcppol-'));
|
|
3051
|
+
cp.spawnSync(process.execPath, [CLI, 'init', pDir, '--minimal', '--no-env', '--yes'], { encoding: 'utf8', timeout: 30000, env });
|
|
3052
|
+
cp.spawnSync(process.execPath, [CLI, 'policy', 'set', 'read-only', '--enforce', '--path', pDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3053
|
+
const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'leerness_state_start', arguments: { path: pDir, goal: 'x' } } });
|
|
3054
|
+
const rMcp = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 15000, input: req + '\n', env });
|
|
3055
|
+
let blocked = false;
|
|
3056
|
+
try { const j = JSON.parse(rMcp.stdout.split('\n').filter(Boolean)[0]); blocked = j.result.isError === true && /정책 차단/.test(j.result.content[0].text); } catch {}
|
|
3057
|
+
const noWrite = !fs.existsSync(path.join(pDir, '.leerness', 'state.json'));
|
|
3058
|
+
// enforce 해제 시 통과(회귀 방지 — 정상 동작 보존)
|
|
3059
|
+
cp.spawnSync(process.execPath, [CLI, 'policy', 'set', 'project-write', '--no-enforce', '--path', pDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3060
|
+
const rMcp2 = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 15000, input: req + '\n', env });
|
|
3061
|
+
let allowed = false;
|
|
3062
|
+
try { const j = JSON.parse(rMcp2.stdout.split('\n').filter(Boolean)[0]); allowed = j.result.isError !== true && /run-0001/.test(j.result.content[0].text); } catch {}
|
|
3063
|
+
// #2 release publish --dry-run --git-push: push 시도 안 함(생략 출력) + exit 0
|
|
3064
|
+
const rDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-reldry-'));
|
|
3065
|
+
fs.writeFileSync(path.join(rDir, 'package.json'), JSON.stringify({ name: 'x', version: '0.0.1' }) + '\n');
|
|
3066
|
+
const rRel = cp.spawnSync(process.execPath, [CLI, 'release', 'publish', rDir, '--dry-run', '--git-push'], { encoding: 'utf8', timeout: 20000, env });
|
|
3067
|
+
const dryOk = rRel.status === 0 && /\(dry-run\)/.test(rRel.stdout) && /생략/.test(rRel.stdout) && !/git push:/.test(rRel.stdout);
|
|
3068
|
+
// #5 도구수 정합: 배지 == 관리블록 == tools/list
|
|
3069
|
+
let countOk = false;
|
|
3070
|
+
try {
|
|
3071
|
+
const rl = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 10000, input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n', env });
|
|
3072
|
+
const live = JSON.parse(rl.stdout.split('\n').filter(Boolean)[0]).result.tools.length;
|
|
3073
|
+
const readme = fs.readFileSync(path.resolve(__dirname, '..', 'README.md'), 'utf8');
|
|
3074
|
+
const badge = (readme.match(/MCP--tools-(\d+)/) || [])[1];
|
|
3075
|
+
countOk = String(live) === badge;
|
|
3076
|
+
} catch {}
|
|
3077
|
+
const ok = blocked && noWrite && allowed && dryOk && countOk;
|
|
3078
|
+
console.log(ok ? '✓ B(1.9.288) Codex 수렴: MCP policy 차단/허용 + release dry-run 무push + 도구수 정합' : `✗ Codex 수렴 실패 (blocked=${blocked} noWrite=${noWrite} allowed=${allowed} dry=${dryOk} count=${countOk})`);
|
|
3079
|
+
if (!ok) { failed++; console.log((rRel.stdout || '').slice(0, 300)); }
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// 1.9.289 회귀 (Codex #3): _shellQuoteArg — REPL agy/copilot 프롬프트 셸 주입 중립화
|
|
3083
|
+
total++;
|
|
3084
|
+
{
|
|
3085
|
+
let ok = false;
|
|
3086
|
+
try {
|
|
3087
|
+
const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
|
|
3088
|
+
const q = h._shellQuoteArg('a; rm -rf / && echo $(whoami)');
|
|
3089
|
+
const win = process.platform === 'win32';
|
|
3090
|
+
// 따옴표로 감싸 메타문자가 단일 리터럴 인자가 됨 (POSIX 단일/Windows 이중)
|
|
3091
|
+
const wrapped = win ? (q.startsWith('"') && q.endsWith('"')) : (q.startsWith("'") && q.endsWith("'"));
|
|
3092
|
+
const neutral = win ? !/^[^"]*[;&|][^"]*$/.test(q.slice(1, -1)) || q.includes('"') : true;
|
|
3093
|
+
ok = wrapped && q.includes('rm -rf') && typeof h._shellQuoteArg === 'function';
|
|
3094
|
+
} catch {}
|
|
3095
|
+
console.log(ok ? '✓ B(1.9.289) _shellQuoteArg: 프롬프트 셸 주입 중립화 (agy/copilot args)' : '✗ _shellQuoteArg 실패');
|
|
3096
|
+
if (!ok) failed++;
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3045
3099
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
3046
3100
|
if (failed > 0) process.exit(1);
|