leerness 1.9.288 → 1.9.290
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 +33 -0
- package/README.md +5 -5
- package/bin/harness.js +37 -28
- package/package.json +1 -1
- package/scripts/e2e.js +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.290 — 2026-06-03 — UR-0037 (Codex #4): require 시 top-level side effect 격리 — Codex 5건 완전 수렴 🎉
|
|
4
|
+
|
|
5
|
+
**🧩 `require('leerness/bin/harness.js')` 가 호스트 프로세스를 오염시키던 마지막 갭 수정 (Codex gpt-5.5 #4). 이로써 Codex 코드 리뷰 5건 전부 수렴 완료.**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
1.9.184/249 에서 (1) `process.removeAllListeners('warning')` + warning 핸들러 재등록, (2) `process.env.NODE_OPTIONS += ' --no-deprecation'`, (3) chcp 65001 IIFE 가 **파일 최상단에서 즉시 실행**됐음. CLI 로는 정상이나, leerness 를 라이브러리로 `require()` 하거나 단위 테스트(`require.main` 가드로 노출한 14종 순수 함수)에서 불러오면 호스트 프로세스의 warning 리스너가 통째로 제거되고 NODE_OPTIONS 가 변형됨 → side-effect-free import 원칙 위반.
|
|
9
|
+
|
|
10
|
+
### 구현 (Codex #4 → 수렴)
|
|
11
|
+
1. **`_cliBootstrap()` 로 CLI 전용 부작용 격리** — warning listener 제거/재등록 + NODE_OPTIONS 변형 + `_ensureStdoutEncoding()` 호출을 한 함수로 묶고, 파일 끝의 기존 `if (require.main === module)` 가드(1.9.255)와 동일하게 **상단에서도 `if (require.main === module) _cliBootstrap();`** 로만 호출.
|
|
12
|
+
2. **`_ensureStdoutEncoding()` IIFE → named 함수** — 즉시실행 제거, `_cliBootstrap` 내부에서만 호출 (chcp/encoding 부작용도 CLI 한정).
|
|
13
|
+
3. selftest 37→38 (소스 가드 정합 + named 함수 존재 검증) · e2e 234→235 (깨끗한 자식에서 require 후 warning listener 보존 + NODE_OPTIONS 미오염 실측).
|
|
14
|
+
|
|
15
|
+
### 검증
|
|
16
|
+
- **selftest 38/38 PASS** · **E2E 235/235 PASS** (회귀 0).
|
|
17
|
+
- 실측: `node -e "process.on('warning',L); require('harness'); ..."` → listener 보존 `true` · NODE_OPTIONS 오염 `false`. CLI(`selftest`/`--version`)는 부트스트랩 정상 실행.
|
|
18
|
+
- **🎉 Codex gpt-5.5 코드 리뷰 5건(#1 MCP policy / #2 release dry-run / #3 셸 주입 / #4 side effect / #5 도구수) 전부 수렴 완료.**
|
|
19
|
+
|
|
20
|
+
## 1.9.289 — 2026-06-03 — UR-0036 (Codex #3): REPL agy/copilot 프롬프트 셸 주입 차단
|
|
21
|
+
|
|
22
|
+
**🔒 REPL 스트리밍에서 agy/copilot 프롬프트가 `shell:true` 인자로 raw 전달돼 셸 분리/주입되던 보안 갭 수정 (Codex gpt-5.5 #3).**
|
|
23
|
+
|
|
24
|
+
### 배경
|
|
25
|
+
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` 처럼 프롬프트의 `;`/`&&`/`$(...)`/공백이 셸에 해석됨.
|
|
26
|
+
|
|
27
|
+
### 구현
|
|
28
|
+
1. **`_shellQuoteArg(s)` 순수 헬퍼** — POSIX(sh): single-quote(bulletproof, 내부 `'`→`'\''`). Windows(cmd.exe): double-quote + 내부 `"`→`""` (공백/`&`/`|`/`<`/`>`/`(`/`)`/`;`/`$()` 모두 리터럴화).
|
|
29
|
+
2. **agy/copilot args 안전 인용** — `['-p', _shellQuoteArg(promptText)]` / `['copilot','suggest',_shellQuoteArg(promptText)]`. 프롬프트가 단일 리터럴 인자로 전달.
|
|
30
|
+
3. selftest 36→37 + e2e 233→234.
|
|
31
|
+
|
|
32
|
+
### 검증
|
|
33
|
+
- **selftest 37/37 PASS** · **E2E 234/234 PASS** (회귀 0) · `a; rm -rf / && echo $(whoami)` → 단일 인용 리터럴 확인 (POSIX/Windows 분기).
|
|
34
|
+
- 남은 백로그: UR-0037(#4) require 시 top-level side effect 격리.
|
|
35
|
+
|
|
3
36
|
## 1.9.288 — 2026-06-03 — Codex gpt-5.5 코드 리뷰 수렴: MCP policy enforce + release dry-run + 도구수 정합
|
|
4
37
|
|
|
5
38
|
**🔒 Codex(gpt-5.5, xhigh)가 실제 코드 라인 근거로 제시한 5건 중 검증된 high/med 3건 수렴 + 나머지 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.290 하네스를 사용합니다. 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.290는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **79개 도구**를 노출:
|
|
517
517
|
|
|
518
518
|
```jsonc
|
|
519
519
|
// 카테고리별
|
|
@@ -534,7 +534,7 @@ Leerness v1.9.288는 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.290)** · 매 라운드 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.290: 2026-06-03
|
|
576
576
|
<!-- leerness:project-readme:end -->
|
|
577
577
|
|
package/bin/harness.js
CHANGED
|
@@ -10,33 +10,17 @@ 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.
|
|
14
|
-
|
|
15
|
-
// 1.9.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (w && (w.code === 'DEP0190' || /DEP0190/.test(String(w.message || '')))) return;
|
|
21
|
-
process.stderr.write(`(node:${process.pid}) ${w.name || 'Warning'}: ${w.message || w}\n`);
|
|
22
|
-
});
|
|
23
|
-
// 1.9.185 (사용자 명시): REPL 진입 후 user input 시점에도 DEP0190 발생 (claude/ollama CLI 가 Node 자식 spawn 시 자식의 노이즈).
|
|
24
|
-
// 부모 핸들러는 자식에 상속 X → NODE_OPTIONS=--no-deprecation 으로 모든 Node child 까지 전파.
|
|
25
|
-
// process.env 변경은 자식 spawn 시 상속 (이미 시작된 부모에는 영향 X, 부모는 위의 'warning' 핸들러로 처리).
|
|
26
|
-
if (!/--no-deprecation/.test(process.env.NODE_OPTIONS || '')) {
|
|
27
|
-
process.env.NODE_OPTIONS = ((process.env.NODE_OPTIONS || '') + ' --no-deprecation').trim();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// 1.9.249 (사용자 명시 UR-0018): 터미널 출력 한국어 깨짐 사전 방지
|
|
31
|
-
// 사용자 보고: protected-files.md 같은 한국어 텍스트가 ? / 한자 조각으로 깨짐 (UTF-8 → CP949 오인식)
|
|
32
|
-
// 조치: (1) stdout/stderr UTF-8 명시 (2) Windows + 한국어 OS + 비-65001 코드페이지 시 chcp 65001 자동 시도
|
|
33
|
-
// 안전: 1회만 시도 (LEERNESS_NO_AUTOCHCP=1 로 opt-out), 실패 시 무시
|
|
34
|
-
(function _ensureStdoutEncoding() {
|
|
13
|
+
const VERSION = '1.9.290';
|
|
14
|
+
|
|
15
|
+
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
16
|
+
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
17
|
+
// 해결: 부작용을 _cliBootstrap() 으로 묶고 파일 끝 require.main 가드에서만 호출. import/단위테스트는 부작용 0.
|
|
18
|
+
// 1.9.249 (UR-0018): 터미널 출력 한국어 깨짐 사전 방지 (stdout UTF-8 + Windows 비-65001 시 chcp 65001 자동, opt-out LEERNESS_NO_AUTOCHCP=1).
|
|
19
|
+
function _ensureStdoutEncoding() {
|
|
35
20
|
try {
|
|
36
21
|
if (process.stdout && process.stdout.setEncoding) process.stdout.setEncoding('utf8');
|
|
37
22
|
if (process.stderr && process.stderr.setEncoding) process.stderr.setEncoding('utf8');
|
|
38
23
|
} catch {}
|
|
39
|
-
// Windows + 한국어 사용자 환경 (chcp 949 = CP949) → 한국어 콘솔 자동 65001 (UTF-8)
|
|
40
24
|
if (process.platform !== 'win32') return;
|
|
41
25
|
if (process.env.LEERNESS_NO_AUTOCHCP === '1') return;
|
|
42
26
|
if (process.env._LEERNESS_CHCP_DONE === '1') return; // 자식 spawn 무한 재호출 방지
|
|
@@ -47,7 +31,6 @@ if (!/--no-deprecation/.test(process.env.NODE_OPTIONS || '')) {
|
|
|
47
31
|
const code = m ? parseInt(m[1], 10) : null;
|
|
48
32
|
// 65001 = UTF-8, 949 = CP949 (한국), 932 = CP932 (일본), 936 = CP936 (중국)
|
|
49
33
|
if (code && code !== 65001) {
|
|
50
|
-
// 자동 chcp 65001 시도 (실패해도 무시 — 출력 방향 X 인 비-TTY 등)
|
|
51
34
|
try {
|
|
52
35
|
cp.spawnSync('chcp.com', ['65001'], { encoding: 'utf8', timeout: 2000, shell: true, stdio: 'ignore' });
|
|
53
36
|
process.env._LEERNESS_AUTOCHCP_APPLIED = String(code); // 진단용 — handoff body 에서 노출
|
|
@@ -55,7 +38,21 @@ if (!/--no-deprecation/.test(process.env.NODE_OPTIONS || '')) {
|
|
|
55
38
|
}
|
|
56
39
|
process.env._LEERNESS_CHCP_DONE = '1';
|
|
57
40
|
} catch {}
|
|
58
|
-
}
|
|
41
|
+
}
|
|
42
|
+
// 1.9.184/185: DEP0190 (shell:true) 경고 억제 — cross-platform .cmd resolution 위해 shell:true 의도 사용.
|
|
43
|
+
// warning listener 제거 + NODE_OPTIONS=--no-deprecation (자식 spawn 까지 전파). CLI 전용 → _cliBootstrap 내부.
|
|
44
|
+
function _cliBootstrap() {
|
|
45
|
+
process.removeAllListeners('warning');
|
|
46
|
+
process.on('warning', (w) => {
|
|
47
|
+
if (w && (w.code === 'DEP0190' || /DEP0190/.test(String(w.message || '')))) return;
|
|
48
|
+
process.stderr.write(`(node:${process.pid}) ${w.name || 'Warning'}: ${w.message || w}\n`);
|
|
49
|
+
});
|
|
50
|
+
if (!/--no-deprecation/.test(process.env.NODE_OPTIONS || '')) {
|
|
51
|
+
process.env.NODE_OPTIONS = ((process.env.NODE_OPTIONS || '') + ' --no-deprecation').trim();
|
|
52
|
+
}
|
|
53
|
+
_ensureStdoutEncoding();
|
|
54
|
+
}
|
|
55
|
+
if (require.main === module) _cliBootstrap();
|
|
59
56
|
|
|
60
57
|
const MARK = '<!-- leerness:managed -->';
|
|
61
58
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
@@ -152,6 +149,13 @@ function fail(s) { log('✗ ' + s); }
|
|
|
152
149
|
// 1.9.287 (Codex 리뷰 수렴): 임베딩 텍스트의 코드펜스(```)를 중립화해 외부 마크다운(session-handoff)이 깨지지 않게.
|
|
153
150
|
// ``` → ''' (펜스가 아닌 표시), 인라인 백틱은 보존. 순수 함수.
|
|
154
151
|
function _sanitizeFences(s) { return String(s || '').replace(/```+/g, "'''"); }
|
|
152
|
+
// 1.9.289 (Codex gpt-5.5 리뷰 #3 수렴): shell:true spawn 의 인자를 셸-안전하게 인용 — 프롬프트 셸 주입/분리 방지.
|
|
153
|
+
// POSIX(sh): single-quote (bulletproof). Windows(cmd.exe): double-quote + inner " 이스케이프 (공백/&|<>() 안전).
|
|
154
|
+
function _shellQuoteArg(s) {
|
|
155
|
+
s = String(s == null ? '' : s);
|
|
156
|
+
if (process.platform === 'win32') return '"' + s.replace(/"/g, '""') + '"';
|
|
157
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
158
|
+
}
|
|
155
159
|
// 1.9.288 (Codex gpt-5.5 리뷰 #5 수렴): MCP 도구 수 단일 출처 — tools 정의 패턴만 카운트(설명/자기-매칭 오탐 제외).
|
|
156
160
|
// 배지/관리블록/capability 가 모두 이 값을 써서 tools/list 실제 수와 일치.
|
|
157
161
|
function _mcpToolCount() {
|
|
@@ -2983,6 +2987,7 @@ function _selfTestCases() {
|
|
|
2983
2987
|
{ 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' },
|
|
2984
2988
|
{ name: '_sanitizeFences: 코드펜스 중립화 (Codex 수렴 1.9.287)', run: () => { const out = _sanitizeFences('text\n```js\ncode\n```\nmore'); return !/```/.test(out) && /'''/.test(out) && /code/.test(out); } },
|
|
2985
2989
|
{ name: '_mcpToolCount: 실제 도구 정의 수 (자기-매칭 오탐 없음) (Codex #5 1.9.288)', run: () => { const n = _mcpToolCount(); return n >= 75 && n < 200; } },
|
|
2990
|
+
{ 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'"; } },
|
|
2986
2991
|
{ 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; } },
|
|
2987
2992
|
{ 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; } },
|
|
2988
2993
|
{ 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; } },
|
|
@@ -2991,6 +2996,7 @@ function _selfTestCases() {
|
|
|
2991
2996
|
{ name: 'ADAPTERS + _mcpJsonContent: 도구 매핑 + .mcp.json 등록 (1.9.280)', run: () => { const ids = Object.keys(ADAPTERS); const okMap = ['claude','cursor','codex','goose','opencode'].every(id => ADAPTERS[id] && Array.isArray(ADAPTERS[id].keys) && ADAPTERS[id].keys.length); const m = JSON.parse(_mcpJsonContent()); return ids.length >= 9 && okMap && ADAPTERS.claude.mcp === true && ADAPTERS.copilot.mcp === false && m.mcpServers.leerness.command === 'npx' && m.mcpServers.leerness.args.join(' ') === 'leerness mcp serve'; } },
|
|
2992
2997
|
{ name: '_newRunRecord: GPT-5.5 권고 스키마 14필드 + 기본값 (1.9.278)', run: () => { const r = _newRunRecord({ run_id: 'run-0001', goal: 'g', started_at: '2026-06-03T00:00:00Z' }); return r.schemaVersion === 1 && r.run_id === 'run-0001' && r.started_at === '2026-06-03T00:00:00Z' && Array.isArray(r.files_changed) && Array.isArray(r.commands_run) && Array.isArray(r.tests_run) && Array.isArray(r.decisions) && r.verification_result === null && r.status === 'in-progress' && 'handoff_summary' in r && 'model_name' in r && 'task_id' in r; } },
|
|
2993
2998
|
{ name: 'coreFiles --minimal: 핵심 유지 + 비핵심 제외 + verify 필수 보존 (1.9.276)', run: () => { const full = coreFiles('.', 'ko', []); const min = coreFiles('.', 'ko', [], { minimal: true }); const keep = ['.harness/plan.md','.harness/progress-tracker.md','.harness/session-handoff.md','AGENTS.md','CLAUDE.md','.harness/consistency-policy.md','.harness/reuse-map.md','.harness/encoding-policy.md','.harness/secret-policy.md']; const drop = ['.cursor/rules/leerness.mdc','.harness/skill-index.md','.harness/architecture.md']; const verifyReq = ['.harness/design-system.md','.harness/protected-files.md','.harness/current-state.md']; if (!verifyReq.every(k => min[k])) return false; return Object.keys(min).length < Object.keys(full).length && keep.every(k => min[k]) && drop.every(k => !min[k]); } },
|
|
2999
|
+
{ name: '_cliBootstrap: CLI 부작용 require.main 가드 격리 (Codex #4 UR-0037 1.9.290)', run: () => { if (typeof _cliBootstrap !== 'function' || typeof _ensureStdoutEncoding !== 'function') return false; const src = read(__filename); const guarded = /if \(require\.main === module\) _cliBootstrap\(\);/.test(src); const inFn = /function _cliBootstrap\(\)\s*\{[\s\S]*?removeAllListeners\('warning'\)[\s\S]*?NODE_OPTIONS[\s\S]*?_ensureStdoutEncoding\(\);[\s\S]*?\n\}/.test(src); const noTopIife = !/\}\)\(\);\s*\n\n\/\/ 1\.9\.184/.test(src); return guarded && inFn && noTopIife; } },
|
|
2994
3000
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
2995
3001
|
];
|
|
2996
3002
|
}
|
|
@@ -17522,8 +17528,9 @@ async function _cliChatStream(root, provider, promptText, opts) {
|
|
|
17522
17528
|
args = ['exec', '--skip-git-repo-check', '-']; // - = stdin from claude/codex convention
|
|
17523
17529
|
useStdinForPrompt = true;
|
|
17524
17530
|
}
|
|
17525
|
-
|
|
17526
|
-
else if (provider === '
|
|
17531
|
+
// 1.9.289 (Codex #3): agy/copilot 은 인자 모드만 지원 → shell:true 조인 시 프롬프트가 분리/주입될 수 있어 _shellQuoteArg 로 안전 인용.
|
|
17532
|
+
else if (provider === 'agy') { cmd = 'agy'; args = ['-p', _shellQuoteArg(promptText)]; } // agy -p 는 인자 모드만 지원
|
|
17533
|
+
else if (provider === 'copilot') { cmd = 'gh'; args = ['copilot', 'suggest', _shellQuoteArg(promptText)]; }
|
|
17527
17534
|
else return { ok: false, error: `provider ${provider} 미지원`, provider };
|
|
17528
17535
|
const t0 = Date.now();
|
|
17529
17536
|
return new Promise(resolve => {
|
|
@@ -21649,5 +21656,7 @@ module.exports = {
|
|
|
21649
21656
|
// 1.9.287: evidence 완전성 + 코드펜스 sanitize (Codex 리뷰 수렴) — 단위 테스트
|
|
21650
21657
|
_evidenceQuality, _sanitizeFences,
|
|
21651
21658
|
// 1.9.288: MCP 도구 수 단일 출처 (Codex #5) — 단위 테스트
|
|
21652
|
-
_mcpToolCount
|
|
21659
|
+
_mcpToolCount,
|
|
21660
|
+
// 1.9.289: shell-safe 인용 (Codex #3) — 단위 테스트
|
|
21661
|
+
_shellQuoteArg
|
|
21653
21662
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -3079,5 +3079,41 @@ total++;
|
|
|
3079
3079
|
if (!ok) { failed++; console.log((rRel.stdout || '').slice(0, 300)); }
|
|
3080
3080
|
}
|
|
3081
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
|
+
|
|
3099
|
+
// 1.9.290 회귀 (Codex #4 UR-0037): require('harness') 가 호스트 프로세스 오염 X (top-level side effect 격리)
|
|
3100
|
+
total++;
|
|
3101
|
+
{
|
|
3102
|
+
let ok = false;
|
|
3103
|
+
try {
|
|
3104
|
+
// 깨끗한 자식에서: 내 warning listener 등록 → require → 보존 확인 + NODE_OPTIONS 미변경
|
|
3105
|
+
const probe = "const L=()=>{};process.on('warning',L);const o=process.env.NODE_OPTIONS||'';" +
|
|
3106
|
+
"require(process.argv[1]);" +
|
|
3107
|
+
"const survived=process.listeners('warning').includes(L);" +
|
|
3108
|
+
"const polluted=(process.env.NODE_OPTIONS||'')!==o;" +
|
|
3109
|
+
"process.exit(survived&&!polluted?0:1);";
|
|
3110
|
+
const harnessPath = path.resolve(__dirname, '..', 'bin', 'harness.js');
|
|
3111
|
+
const r = cp.spawnSync(process.execPath, ['-e', probe, harnessPath], { encoding: 'utf8', timeout: 20000 });
|
|
3112
|
+
ok = r.status === 0;
|
|
3113
|
+
} catch {}
|
|
3114
|
+
console.log(ok ? '✓ B(1.9.290) require 부작용 격리: warning listener 보존 + NODE_OPTIONS 미오염 (Codex #4)' : '✗ require 부작용 격리 실패');
|
|
3115
|
+
if (!ok) failed++;
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3082
3118
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
3083
3119
|
if (failed > 0) process.exit(1);
|