leerness 1.9.288 → 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 CHANGED
@@ -1,5 +1,21 @@
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
+
3
19
  ## 1.9.288 — 2026-06-03 — Codex gpt-5.5 코드 리뷰 수렴: MCP policy enforce + release dry-run + 도구수 정합
4
20
 
5
21
  **🔒 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
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.288-green)]() [![tests](https://img.shields.io/badge/e2e-233%2F233-success)]() [![selftest](https://img.shields.io/badge/selftest-36-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-79-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.289-green)]() [![tests](https://img.shields.io/badge/e2e-234%2F234-success)]() [![selftest](https://img.shields.io/badge/selftest-37-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-79-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
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.288 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
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.288는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **79개 도구**를 노출:
516
+ Leerness v1.9.289는 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.288)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
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.288: 2026-06-03
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.288';
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,13 @@ 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
+ }
155
162
  // 1.9.288 (Codex gpt-5.5 리뷰 #5 수렴): MCP 도구 수 단일 출처 — tools 정의 패턴만 카운트(설명/자기-매칭 오탐 제외).
156
163
  // 배지/관리블록/capability 가 모두 이 값을 써서 tools/list 실제 수와 일치.
157
164
  function _mcpToolCount() {
@@ -2983,6 +2990,7 @@ function _selfTestCases() {
2983
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' },
2984
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); } },
2985
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'"; } },
2986
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; } },
2987
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; } },
2988
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; } },
@@ -17522,8 +17530,9 @@ async function _cliChatStream(root, provider, promptText, opts) {
17522
17530
  args = ['exec', '--skip-git-repo-check', '-']; // - = stdin from claude/codex convention
17523
17531
  useStdinForPrompt = true;
17524
17532
  }
17525
- else if (provider === 'agy') { cmd = 'agy'; args = ['-p', promptText]; } // agy -p 인자 모드만 지원
17526
- else if (provider === 'copilot') { cmd = 'gh'; args = ['copilot', 'suggest', promptText]; }
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)]; }
17527
17536
  else return { ok: false, error: `provider ${provider} 미지원`, provider };
17528
17537
  const t0 = Date.now();
17529
17538
  return new Promise(resolve => {
@@ -21649,5 +21658,7 @@ module.exports = {
21649
21658
  // 1.9.287: evidence 완전성 + 코드펜스 sanitize (Codex 리뷰 수렴) — 단위 테스트
21650
21659
  _evidenceQuality, _sanitizeFences,
21651
21660
  // 1.9.288: MCP 도구 수 단일 출처 (Codex #5) — 단위 테스트
21652
- _mcpToolCount
21661
+ _mcpToolCount,
21662
+ // 1.9.289: shell-safe 인용 (Codex #3) — 단위 테스트
21663
+ _shellQuoteArg
21653
21664
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.288",
3
+ "version": "1.9.289",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -3079,5 +3079,22 @@ 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
+
3082
3099
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
3083
3100
  if (failed > 0) process.exit(1);