leerness 1.9.348 → 1.9.350
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 +28 -0
- package/README.md +5 -5
- package/bin/harness.js +13 -6
- package/lib/catalogs.js +7 -0
- package/lib/pure-utils.js +3 -1
- package/package.json +1 -1
- package/scripts/e2e.js +50 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.350 — 2026-06-05 — 외부리뷰 P1 보안 하드닝 3종 (UR-0060/0061/0062)
|
|
4
|
+
|
|
5
|
+
**🔒 외부 멀티모델 리뷰 P1 보안 3건 일괄 수정 — 자율 팀(UR-0073) 전 보안 기반 다지기.**
|
|
6
|
+
|
|
7
|
+
### 구현
|
|
8
|
+
1. **UR-0062 — skill install path traversal 차단** (Codex#2): `SKILL.md name: ..` 가 `.harness/skills/<id>` 밖에 쓰이던 문제. id 정규화에 선행/후행 `./-` 제거 + `..` 거부 + jail(`dir.startsWith(skillsRoot+sep)`).
|
|
9
|
+
2. **UR-0061 — roadmap CSS 인젝션 차단** (Codex#3+Opus P3-1, 2모델): design token 값이 `_roadmapTokenStyles` 의 `:root{}` 에 raw 보간 → `}`/`</style>` breakout. CSS 값 whitelist 살균(`[^#a-zA-Z0-9(),.%\s_-]` 제거)으로 규칙/태그 탈출 차단, 색상/길이 보존.
|
|
10
|
+
3. **UR-0060 — 시크릿 스캐너 보강** (3모델): (a) 파일명 substring(`harness.js`/`secret-policy.md`) skip → **정확 경로** self-exclusion(`path.resolve(file)===__filename` + 생성 secret-policy 템플릿만) — 사용자 파일명 false-negative 제거. (b) SECRET_PATTERNS 13→**19**: GitLab PAT / JWT / DB-URI(임베디드 비밀번호) / SendGrid / AWS Secret Access Key(context) / Bearker 토큰 보강.
|
|
11
|
+
|
|
12
|
+
### 검증
|
|
13
|
+
- **selftest 98/98 PASS** · **E2E 295/295 PASS** (회귀 0).
|
|
14
|
+
- 실측: skill `name: ..` → "path traversal 차단"(`.harness/SKILL.md` 미생성) · 사용자 `harness.js` 의 GitLab PAT **이제 탐지**(false-neg 해소) · CSS `red;}</style><script>` → `<`/`>`/`}` 제거(breakout 차단)·`#2563eb` 보존 · 신규 6패턴 탐지 + clean(john_doe/example.com URL) 오탐 0.
|
|
15
|
+
|
|
16
|
+
## 1.9.349 — 2026-06-05 — 외부리뷰 UR-0063: selftest/doctor 위치독립화 (trust-killer)
|
|
17
|
+
|
|
18
|
+
**🔴 신규 사용자 첫인상 신뢰 직결 버그 수정 — `selftest`/`doctor` 가 비초기화 디렉토리에서 거짓 "설치 손상 → 재설치" 오보고하던 문제 해소.** (Opus 4.8 P1-1 + 웹 GPT-5.5 "가장 치명적" + 하네스 재현 = **3모델 교차검증**)
|
|
19
|
+
|
|
20
|
+
### 배경
|
|
21
|
+
`init 가드` selftest 케이스(UR-0047 1.9.311)가 `_isInitialized('.')===true` 를 검사 → cwd 가 init 프로젝트가 아니면 거짓 실패. `selftest`/`doctor`/`npm run test:fast`/smoke 가 신규 사용자의 임의 디렉토리에서 "1/N 실패 — 설치 손상 의심 / 재설치" 출력 → 첫 진단이 정상 설치를 broken 으로 오인. GPT-5.5 신뢰도 4/10·프로덕션 3/10 의 핵심 원인. **자가진단은 cwd 상태에 의존하면 안 됨.**
|
|
22
|
+
|
|
23
|
+
### 구현
|
|
24
|
+
1. selftest 케이스의 cwd 의존 `liveOk = _isInitialized('.')` → **위치독립 임시 fixture**: `os.tmpdir()` 에 AGENTS.md 마커 생성 후 `_isInitialized(fixture)===true` 검증 + 정리(finally). 비초기화 탐지(`emptyOk`)는 유지.
|
|
25
|
+
2. doctor 는 selftest 를 인라인 실행 → 동일 수정으로 함께 해소.
|
|
26
|
+
|
|
27
|
+
### 검증
|
|
28
|
+
- **비초기화 dir selftest 96/96 통과**(이전 1/96 실패) · **doctor "✓ 설치 정상"**(이전 "✗ 문제 감지 — 재설치") · init dir 회귀 96/96 OK.
|
|
29
|
+
- selftest 96/96 · **E2E 293→294 PASS**(비초기화 cwd selftest/doctor 회귀 가드 추가, 회귀 0).
|
|
30
|
+
|
|
3
31
|
## 1.9.348 — 2026-06-05 — 외부리뷰 UR-0059(P0): --path 라우팅 일관화
|
|
4
32
|
|
|
5
33
|
**🔴 외부 멀티모델 리뷰 P0 수정 — `--path` 플래그가 ~22개 positional-dispatch 명령에서 무시되고 CWD 로 fallback 되던 버그 해소.** (Sonnet 4.8 F-01 발견, 하네스 재현 검증)
|
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.350 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
477
477
|
|
|
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
|
|
|
525
525
|
|
|
526
526
|
### MCP server (외부 AI 통합)
|
|
527
527
|
|
|
528
|
-
Leerness v1.9.
|
|
528
|
+
Leerness v1.9.350는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **83개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.348는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
546
546
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
547
547
|
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) 다음 라운드 예약.
|
|
548
548
|
|
|
549
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
549
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.350)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
550
550
|
|
|
551
551
|
### 성능 가이드 (1.9.140 측정)
|
|
552
552
|
|
|
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
|
|
|
584
584
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
585
585
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
586
586
|
|
|
587
|
-
Last synced by Leerness v1.9.
|
|
587
|
+
Last synced by Leerness v1.9.350: 2026-06-05
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -27,7 +27,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
27
27
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
28
28
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344 (UR-0025): SKILL_CATALOG_PRESETS 분리
|
|
29
29
|
|
|
30
|
-
const VERSION = '1.9.
|
|
30
|
+
const VERSION = '1.9.350';
|
|
31
31
|
|
|
32
32
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
33
33
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -1530,8 +1530,12 @@ async function skillInstallCmd(root, source) {
|
|
|
1530
1530
|
const description = parsed.meta.description || '';
|
|
1531
1531
|
if (!name) { fail('SKILL.md frontmatter에 `name` 필수'); return process.exit(1); }
|
|
1532
1532
|
// .harness/skills/<id>/SKILL.md 저장
|
|
1533
|
-
|
|
1534
|
-
const
|
|
1533
|
+
// 1.9.350 (UR-0062 외부리뷰): skill id 정규화 — 선행/후행 ./- 제거 + .. 거부 + jail (path traversal 차단)
|
|
1534
|
+
const skillId = String(name).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '');
|
|
1535
|
+
if (!skillId || skillId.includes('..')) { fail(`유효하지 않은 skill id (path traversal 차단): ${name}`); return process.exit(1); }
|
|
1536
|
+
const _skillsRoot = path.join(root, '.harness', 'skills');
|
|
1537
|
+
const dir = path.join(_skillsRoot, skillId);
|
|
1538
|
+
if (!path.resolve(dir).startsWith(path.resolve(_skillsRoot) + path.sep)) { fail(`skill id jail 위반: ${name}`); return process.exit(1); }
|
|
1535
1539
|
mkdirp(dir);
|
|
1536
1540
|
writeUtf8(path.join(dir, 'SKILL.md'), body);
|
|
1537
1541
|
// skill.json도 함께 (자체 catalog 호환)
|
|
@@ -3009,7 +3013,7 @@ function _selfTestCases() {
|
|
|
3009
3013
|
{ name: 'brief 2단계: update --direction 이력 + MCP leerness_brief + context 통합 (UR-0055 1.9.308)', run: () => { const src = read(__filename); const b = { project: 'X', intro: '', purpose: '', problem: '', features: [], stack: [], architecture: '', users: [], success: [], nonGoals: [], currentState: '', directionHistory: ['2026-06-04: 확대'] }; const bpOk = /개발 방향 이력/.test(_briefBlueprint(b, VERSION)) && /최근 개발 방향 변경/.test(_briefReadmeBlock(b)); const histWired = /sub === 'update'/.test(src) && /brief\.directionHistory \|\| \[\]\), `\$\{today\(\)\}/.test(src); const mcpOk = require('../lib/mcp-tools').some(t => t.name === 'leerness_brief'); const ctxOk = /brief: \{ intro:/.test(src); return bpOk && histWired && mcpOk && ctxOk; } },
|
|
3010
3014
|
{ name: 'verify-claim: done 주장 evidence 기본강제 + --lenient + MCP/json 도달 (UR-0048 설치리뷰 critical 1.9.309)', run: () => { const src = read(__filename); const def = /const mustHaveEvidence = !has\('--lenient'\) && \(isDoneClaim \|\| has\('--require-evidence'\)\)/.test(src); const threshold = /has\('--require-evidence'\) \? evq\.ok : \(evq\.hasFile \|\| evq\.hasTest \|\| evq\.hasLog\)/.test(src); const jsonWired = /evidenceComplete:/.test(src) && /!evidenceQualityOk\) return process\.exit\(1\)/.test(src); const mcpLenient = !!require('../lib/mcp-tools').find(t => t.name === 'leerness_verify_claim').inputSchema.properties.lenient; return def && threshold && jsonWired && mcpLenient; } },
|
|
3011
3015
|
{ name: '입력 스키마 검증: task status/rule trigger 무효값 거부 + every-round 보존 (UR-0046 설치리뷰 1.9.310)', run: () => { const src = read(__filename); const sets = TASK_STATUSES.has('done') && TASK_STATUSES.has('in-progress') && !TASK_STATUSES.has('nonsense') && RULE_TRIGGERS.has('every-round') && RULE_TRIGGERS.has('every-update') && !RULE_TRIGGERS.has('not-a-trigger'); const helper = typeof _validateChoice === 'function' && _validateChoice('done', TASK_STATUSES, 'x') === true; const wired = /_validateChoice\(arg\('--status', null\), TASK_STATUSES/.test(src) && /_validateChoice\(trigger, RULE_TRIGGERS/.test(src); return sets && helper && wired; } },
|
|
3012
|
-
{ name: 'init 가드: 미초기화 write 차단 + 다중마커 판별 + --force 우회 (UR-0047 설치리뷰 1.9.311)', run: () => { const src = read(__filename); const fnOk = typeof _isInitialized === 'function' && typeof _requireInit === 'function'; const liveOk =
|
|
3016
|
+
{ name: 'init 가드: 미초기화 write 차단 + 다중마커 판별 + --force 우회 (UR-0047 설치리뷰 1.9.311)', run: () => { const src = read(__filename); const fnOk = typeof _isInitialized === 'function' && typeof _requireInit === 'function'; const _fix = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_initfix_')); let liveOk = false; try { fs.writeFileSync(path.join(_fix, 'AGENTS.md'), 'x'); liveOk = _isInitialized(_fix) === true; } finally { try { fs.rmSync(_fix, { recursive: true, force: true }); } catch {} } const emptyOk = _isInitialized(path.join(os.tmpdir(), '__leerness_noinit_marker__')) === false; const wired = ["task add", "task update", "plan add", "decision add", "rule add", "lesson save", "brief set"].every(l => src.includes(`_requireInit(root, '${l}')`)) && !src.includes("_requireInit(root, 'state " + "start')"); const force = /if \(_isInitialized\(root\) \|\| has\('--force'\)\) return true/.test(src); return fnOk && liveOk && emptyOk && wired && force; } },
|
|
3013
3017
|
{ name: 'secret 스캐너 현대 키: OpenAI proj/svcacct·Anthropic api03(_)·GitHub 변종·Stripe·npm 검출 + 오탐 가드 (UR-0050 설치리뷰 1.9.312)', run: () => { const hit = (s) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const named = (s, nm) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s) && p.name === nm; }); const A = 'A'.repeat(40); const projKey = 'sk-' + 'proj-' + A + '_' + A; const svcKey = 'sk-' + 'svcacct-' + A; const antKey = 'sk-' + 'ant-api03-' + A + '_' + A; const ghoKey = 'gho_' + 'a1B2'.repeat(9); const stripeKey = 'sk_' + 'live_' + A; const npmKey = 'npm_' + 'a1B2'.repeat(9); const asiaKey = 'ASIA' + 'ABCD1234EFGH5678'; const legacy = 'sk-' + A; const hits = hit(projKey) && hit(svcKey) && hit(antKey) && hit(ghoKey) && hit(stripeKey) && hit(npmKey) && hit(asiaKey) && hit(legacy); const names = named(projKey, 'OpenAI project/service key') && named(antKey, 'Anthropic API key') && named(stripeKey, 'Stripe secret key') && named(npmKey, 'npm token'); const clean = !hit('const userName = "john' + '_doe_2024";') && !hit('https://example.com/path/to/page'); return hits && names && clean; } },
|
|
3014
3018
|
{ name: 'MCP notification 준수: id없는 요청 무응답 가드 + ping {} (UR-0049 설치리뷰 1.9.313)', run: () => { const src = read(__filename); const guard = src.includes("const isNotification = !('id' in req)") && src.includes("req.method.startsWith('notifications/')") && src.includes('if (isNotification) return;'); const ping = src.includes("req.method === 'ping'") && /ping[\s\S]{0,140}result: \{\} \}/.test(src); return guard && ping; } },
|
|
3015
3019
|
{ name: 'PowerShell 감지: pwsh7(channel/Documents\\PowerShell/install) + ps5.1 영구경로 과경고 안함 (UR-0052 설치리뷰 1.9.314)', run: () => { const f = _detectPwshFromEnv; const pwsh7a = f({ POWERSHELL_DISTRIBUTION_CHANNEL: 'MSI:Windows 10' }).version === '7'; const pwsh7b = f({ PSModulePath: 'C:\\Users\\me\\Documents\\PowerShell\\Modules' }).version === '7'; const pwsh7c = f({ PSModulePath: 'C:\\Program Files\\PowerShell\\7\\Modules' }).version === '7'; const noFalsePs5 = f({ PSModulePath: 'C:\\Users\\me\\Documents\\WindowsPowerShell\\Modules' }).isPowerShell === false; const cmdSys = f({ PSModulePath: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules' }).isPowerShell === false; const empty = f({}).isPowerShell === false; const src = read(__filename); const wired = src.includes('const fromEnv = _detectPwshFromEnv()') && src.includes('const pwshEnv = _detectPwshFromEnv()'); return pwsh7a && pwsh7b && pwsh7c && noFalsePs5 && cmdSys && empty && wired; } },
|
|
@@ -3041,12 +3045,14 @@ function _selfTestCases() {
|
|
|
3041
3045
|
{ name: 'UR-0058: lessons canonical JSON 레이어(_loadLessons/_saveLessons/lessonsJsonPath) + pure 파서/렌더 round-trip', run: () => { const m = require('../lib/pure-utils'); const objs = [{ date: '2026-06-05', text: 'A', tag: 't' }, { date: '2026-06-04', text: 'B', tag: null }]; const rt = m._parseLessonEntries(m._renderLessonsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._parseLessonEntries(m._renderLessonsMd([])).length === 0; const layerOk = typeof _loadLessons === 'function' && typeof _saveLessons === 'function' && typeof lessonsJsonPath === 'function' && _renderLessonsMd === m._renderLessonsMd; return rtOk && tplOk && layerOk; } },
|
|
3042
3046
|
{ name: 'UR-0025 심층: BUILTIN_CATALOG→lib/catalogs + _withBuiltinSource→pure-utils 분리 (1.9.341)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length === 9 && c.BUILTIN_CATALOG.office && c.BUILTIN_CATALOG.office.version === '1.0.0'; const out = m._withBuiltinSource(c.BUILTIN_CATALOG); const work = Object.keys(out).length === 9 && Object.values(out).every(v => v._source === 'builtin') && out.office.version === '1.0.0' && Array.isArray(out.office.capabilities) && Object.keys(m._withBuiltinSource(null)).length === 0; const src = read(__filename); const moved = BUILTIN_CATALOG === c.BUILTIN_CATALOG && _withBuiltinSource === m._withBuiltinSource && !/const BUILTIN_CATALOG = \{/.test(src); return catOk && work && moved; } },
|
|
3043
3047
|
{ name: 'UR-0025 심층: ROADMAP_STATUS_LABEL/COLOR→lib/catalogs 분리 (1.9.342)', run: () => { const c = require('../lib/catalogs'); const lblOk = c.ROADMAP_STATUS_LABEL && Object.keys(c.ROADMAP_STATUS_LABEL).length === 11 && c.ROADMAP_STATUS_LABEL.done === '완료' && c.ROADMAP_STATUS_LABEL.blocked === '오류'; const colOk = c.ROADMAP_STATUS_COLOR && Object.keys(c.ROADMAP_STATUS_COLOR).length === 11 && c.ROADMAP_STATUS_COLOR.done === '#16a34a' && c.ROADMAP_STATUS_COLOR.skill === '#8b5cf6'; const src = read(__filename); const moved = ROADMAP_STATUS_LABEL === c.ROADMAP_STATUS_LABEL && ROADMAP_STATUS_COLOR === c.ROADMAP_STATUS_COLOR && !/const ROADMAP_STATUS_LABEL = \{/.test(src); return lblOk && colOk && moved; } },
|
|
3044
|
-
{ name: 'UR-0025 심층: SECRET_PATTERNS→lib/catalogs 보안 응집 분리 (1.9.343)', run: () => { const c = require('../lib/catalogs'); const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length ===
|
|
3048
|
+
{ name: 'UR-0025 심층: SECRET_PATTERNS→lib/catalogs 보안 응집 분리 (1.9.343)', run: () => { const c = require('../lib/catalogs'); const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 19 && c.SECRET_PATTERNS.every(p => p.name && p.re instanceof RegExp); const A = 'A'.repeat(40); const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const det = hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";'); const moved = SECRET_PATTERNS === c.SECRET_PATTERNS; return catOk && det && moved; } },
|
|
3045
3049
|
{ name: 'UR-0025 심층: SKILL_CATALOG_PRESETS→lib/catalogs 분리 (1.9.344)', run: () => { const c = require('../lib/catalogs'); const catOk = c.SKILL_CATALOG_PRESETS && Object.keys(c.SKILL_CATALOG_PRESETS).length === 2 && c.SKILL_CATALOG_PRESETS.vercel && c.SKILL_CATALOG_PRESETS.vercel.owner === 'vercel-labs' && c.SKILL_CATALOG_PRESETS.anthropic && c.SKILL_CATALOG_PRESETS.anthropic.repo === 'skills'; const moved = SKILL_CATALOG_PRESETS === c.SKILL_CATALOG_PRESETS; return catOk && moved; } },
|
|
3046
3050
|
{ name: 'UR-0025 심층: _esc(HTML escape)→pure-utils 분리 + XSS 가드 (1.9.345)', run: () => { const m = require('../lib/pure-utils'); const work = m._esc('&<>"\'') === '&<>"'' && m._esc('<script>x</script>') === '<script>x</script>' && m._esc(null) === '' && m._esc(undefined) === '' && m._esc(42) === '42'; const moved = _esc === m._esc; return work && moved; } },
|
|
3047
3051
|
{ name: 'UR-0025 심층: _roadmapTokenStyles→pure-utils 분리 (1.9.346)', run: () => { const m = require('../lib/pure-utils'); const out = m._roadmapTokenStyles({ 'color.primary': '#2563eb' }, { 'color-surface': '#fff', 'custom': '#abc' }); const work = out.startsWith(':root {') && out.includes('--lr-primary: #2563eb') && out.includes('--lr-surface: #fff') && out.includes('--lr-custom: #abc') && out.includes('--lr-card-bg') && m._roadmapTokenStyles(null, null).startsWith(':root {'); const moved = _roadmapTokenStyles === m._roadmapTokenStyles; return work && moved; } },
|
|
3048
3052
|
{ name: 'UR-0025 심층: _parseSkillMd(SKILL.md frontmatter, BOM-aware)→pure-utils 분리 (1.9.347)', run: () => { const m = require('../lib/pure-utils'); const r = m._parseSkillMd('---\nname: s1\ndescription: "d1"\n---\nbody'); const work = r.meta.name === 's1' && r.meta.description === 'd1' && r.body === 'body' && m._parseSkillMd('---\nname: b\n---\nx').meta.name === 'b' && Object.keys(m._parseSkillMd('plain text').meta).length === 0 && m._parseSkillMd('plain text').body === 'plain text' && m._parseSkillMd(null).body === ''; const moved = _parseSkillMd === m._parseSkillMd; return work && moved; } },
|
|
3049
3053
|
{ name: 'UR-0059(외부리뷰 P0): --path 라우팅 일관화 — bare args[N]||cwd 제거 + arg(--path) wrap', run: () => { const src = read(__filename); const bare1 = '(args[1] ' + '|| process.cwd())'; const bare2 = '(args[2] ' + '|| process.cwd())'; const noBare = !src.includes(bare1) && !src.includes(bare2); const wrapped = src.includes('arg(' + "'--path', args[1] || process.cwd())") && src.includes('arg(' + "'--path', args[2] || process.cwd())"); const argDefault = arg('--definitely-not-real-xyz', 'SENT') === 'SENT'; return noBare && wrapped && argDefault; } },
|
|
3054
|
+
{ name: 'UR-0061(외부리뷰 P1): roadmap CSS 값 살균 — :root/</style> breakout 차단', run: () => { const m = require('../lib/pure-utils'); const css = m._roadmapTokenStyles({ 'color.primary': 'red;}' + '</style><script>alert(1)</script>' }, {}); const blocked = !css.includes('<') && !css.includes('>'); const primaryLine = (css.split('\n').find(l => l.includes('--lr-primary')) || ''); const noBreakout = !primaryLine.replace(/;$/, '').includes('}'); const preserved = m._roadmapTokenStyles({ 'color.primary': '#2563eb' }, {}).includes('--lr-primary: #2563eb'); return blocked && noBreakout && preserved; } },
|
|
3055
|
+
{ name: 'UR-0060(외부리뷰 P1): SECRET_PATTERNS 19종 — GitLab/JWT/DB-URI/SendGrid/AWS-secret/Bearer 보강 + 오탐 가드', run: () => { const c = require('../lib/catalogs'); const hit = s => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const det = hit('glpat-' + 'x'.repeat(20)) && hit('eyJ' + 'x'.repeat(15) + '.eyJ' + 'y'.repeat(15) + '.' + 'z'.repeat(15)) && hit('postgres://u:p@host:5432/db') && hit('SG.' + 'x'.repeat(22) + '.' + 'y'.repeat(43)) && hit('aws_secret_access_key = "' + 'x'.repeat(40) + '"') && hit('Bearer ' + 'x'.repeat(25)); const clean = !hit('const u = "john' + '_doe_2024";') && !hit('https://example.com/path/to/page'); return c.SECRET_PATTERNS.length === 19 && det && clean; } },
|
|
3050
3056
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3051
3057
|
];
|
|
3052
3058
|
}
|
|
@@ -7099,11 +7105,12 @@ function scanSecrets(root) {
|
|
|
7099
7105
|
try { text = read(file); } catch { continue; }
|
|
7100
7106
|
if (text.length > 1024 * 1024) continue;
|
|
7101
7107
|
const fileRel = rel(root, file);
|
|
7108
|
+
// 1.9.350 (UR-0060 외부리뷰): leerness 자기 harness.js(regex 소스) + 생성 secret-policy 템플릿만 제외 — 정확 경로(사용자 파일명 substring false-negative 제거)
|
|
7109
|
+
if (path.resolve(file) === path.resolve(__filename) || /(^|[\\/])\.(?:harness|leerness)[\\/]secret-policy\.md$/.test(fileRel)) continue;
|
|
7102
7110
|
for (const { name, re } of SECRET_PATTERNS) {
|
|
7103
7111
|
re.lastIndex = 0;
|
|
7104
7112
|
let m;
|
|
7105
7113
|
while ((m = re.exec(text))) {
|
|
7106
|
-
if (fileRel.includes('harness.js') || fileRel.includes('secret-policy.md')) break;
|
|
7107
7114
|
const line = text.slice(0, m.index).split('\n').length;
|
|
7108
7115
|
findings.push({ file: fileRel, line, name, snippet: m[0].slice(0, 32) });
|
|
7109
7116
|
break;
|
package/lib/catalogs.js
CHANGED
|
@@ -377,6 +377,13 @@ const SECRET_PATTERNS = [
|
|
|
377
377
|
{ name: 'Slack token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
378
378
|
{ name: 'Generic private key', re: /-----BEGIN (?:RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/g },
|
|
379
379
|
{ name: 'Hardcoded password assignment', re: /\b(?:password|passwd|pwd|secret|api_key|apikey)\s*[:=]\s*["'][^"'\s]{6,}["']/gi },
|
|
380
|
+
// 1.9.350 (UR-0060 외부리뷰 3모델): 누락 패턴 보강
|
|
381
|
+
{ name: 'GitLab PAT', re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
|
|
382
|
+
{ name: 'JWT', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
383
|
+
{ name: 'DB connection string (embedded password)', re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:\s/@]+:[^@\s/]+@/gi },
|
|
384
|
+
{ name: 'SendGrid API key', re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g },
|
|
385
|
+
{ name: 'AWS Secret Access Key (context)', re: /\baws[^\n]{0,40}?(?:secret_access_key|secret_key|secret)[^\n]{0,12}?["']?[A-Za-z0-9/+]{40}["']?/gi },
|
|
386
|
+
{ name: 'Hardcoded Bearer token', re: /\bBearer\s+[A-Za-z0-9_\-.=]{20,}/g },
|
|
380
387
|
];
|
|
381
388
|
|
|
382
389
|
// 1.9.344 (UR-0025 심층): skill discover GitHub preset catalog (vercel/anthropic) — harness 에서 분리.
|
package/lib/pure-utils.js
CHANGED
|
@@ -412,7 +412,9 @@ function _roadmapTokenStyles(designTokens, cssVariables) {
|
|
|
412
412
|
if (!vars['lr-card-bg']) vars['lr-card-bg'] = vars['lr-surface'] || '#ffffff';
|
|
413
413
|
if (!vars['lr-edge']) vars['lr-edge'] = vars['lr-muted'] || '#cbd5e1';
|
|
414
414
|
if (!vars['lr-page-bg']) vars['lr-page-bg'] = '#f8fafc';
|
|
415
|
-
|
|
415
|
+
// 1.9.350 (UR-0060/0061 외부리뷰): CSS 값 살균 — whitelist 로 } < > ; { @ : / 등 제거(:root 규칙 breakout + </style> HTML 탈출 차단). 색상/길이 형식은 보존.
|
|
416
|
+
const _safeCss = v => String(v == null ? '' : v).replace(/[^#a-zA-Z0-9(),.%\s_-]/g, '').slice(0, 80);
|
|
417
|
+
return ':root {\n' + Object.entries(vars).map(([k, v]) => ` --${k}: ${_safeCss(v)};`).join('\n') + '\n }';
|
|
416
418
|
}
|
|
417
419
|
|
|
418
420
|
// 1.9.347 (UR-0025 심층): SKILL.md frontmatter 파서 — { meta, body }, BOM-aware (Windows Notepad 호환). 순수.
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -4434,7 +4434,7 @@ total++;
|
|
|
4434
4434
|
const c = require(path.resolve(__dirname, '..', 'lib', 'catalogs.js'));
|
|
4435
4435
|
const A = 'A'.repeat(40);
|
|
4436
4436
|
const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); });
|
|
4437
|
-
const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length ===
|
|
4437
|
+
const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 19
|
|
4438
4438
|
&& hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";');
|
|
4439
4439
|
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
|
|
4440
4440
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
@@ -4594,5 +4594,54 @@ total++;
|
|
|
4594
4594
|
if (!ok) failed++;
|
|
4595
4595
|
}
|
|
4596
4596
|
|
|
4597
|
+
// 1.9.349 회귀 (외부리뷰 UR-0063, GPT5.5+Opus 교차검증): selftest/doctor 위치독립 — 비초기화 dir 에서도 통과(거짓 "설치 손상" 없음)
|
|
4598
|
+
total++;
|
|
4599
|
+
{
|
|
4600
|
+
let ok = false;
|
|
4601
|
+
try {
|
|
4602
|
+
const ni = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-noinit-')); // .harness 없는 비초기화 dir
|
|
4603
|
+
const sr = cp.spawnSync(process.execPath, [CLI, 'selftest'], { cwd: ni, encoding: 'utf8', timeout: 30000 });
|
|
4604
|
+
const sout = (sr.stdout || '') + (sr.stderr || '');
|
|
4605
|
+
const selftestOk = sr.status === 0 && /전체 \d+건 통과/.test(sout) && !/설치 손상/.test(sout);
|
|
4606
|
+
const dr = cp.spawnSync(process.execPath, [CLI, 'doctor'], { cwd: ni, encoding: 'utf8', timeout: 30000 });
|
|
4607
|
+
const dout = (dr.stdout || '') + (dr.stderr || '');
|
|
4608
|
+
const doctorOk = !/문제 감지|설치 손상|재설치/.test(dout) && /설치 정상|통과/.test(dout);
|
|
4609
|
+
ok = selftestOk && doctorOk;
|
|
4610
|
+
fs.rmSync(ni, { recursive: true, force: true });
|
|
4611
|
+
} catch {}
|
|
4612
|
+
console.log(ok ? '✓ B(1.9.349) 외부리뷰 UR-0063: selftest/doctor 위치독립 — 비초기화 dir 통과 (UR-0063)' : '✗ selftest 위치독립 실패');
|
|
4613
|
+
if (!ok) failed++;
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
// 1.9.350 회귀 (외부리뷰 P1 보안 하드닝): UR-0061 CSS breakout 차단 + UR-0062 skill traversal 차단 + UR-0060 scan false-neg/패턴 보강
|
|
4617
|
+
total++;
|
|
4618
|
+
{
|
|
4619
|
+
let ok = false;
|
|
4620
|
+
try {
|
|
4621
|
+
const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
|
|
4622
|
+
// UR-0061: roadmap CSS 값 살균 (순수)
|
|
4623
|
+
const css = m._roadmapTokenStyles({ 'color.primary': 'red;}' + '</style><script>x</script>' }, {});
|
|
4624
|
+
const cssOk = !css.includes('<') && !css.includes('>') && css.includes('--lr-primary:');
|
|
4625
|
+
// UR-0062: skill install name:.. traversal 차단 (end-to-end)
|
|
4626
|
+
const sd = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-trav-'));
|
|
4627
|
+
cp.spawnSync(process.execPath, [CLI, 'init', sd, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
4628
|
+
const skf = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skf-'));
|
|
4629
|
+
fs.writeFileSync(path.join(skf, 'SKILL.md'), '---\nname: ..\ndescription: t\n---\n# b');
|
|
4630
|
+
const ir = cp.spawnSync(process.execPath, [CLI, 'skill', 'install', skf, '--path', sd], { encoding: 'utf8', timeout: 20000 });
|
|
4631
|
+
const travBlocked = /traversal|유효하지 않은 skill id|jail/.test((ir.stdout || '') + (ir.stderr || '')) && !fs.existsSync(path.join(sd, '.harness', 'SKILL.md'));
|
|
4632
|
+
fs.rmSync(skf, { recursive: true, force: true });
|
|
4633
|
+
// UR-0060: 사용자 harness.js 파일도 스캔(false-neg 제거) + 신규 GitLab 패턴 탐지
|
|
4634
|
+
const ud = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-scan-'));
|
|
4635
|
+
fs.writeFileSync(path.join(ud, 'harness.js'), 'const k = "glpat-' + 'x'.repeat(20) + '";\n');
|
|
4636
|
+
const scr = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', ud], { encoding: 'utf8', timeout: 20000 });
|
|
4637
|
+
const scanOk = /GitLab PAT/.test((scr.stdout || '') + (scr.stderr || ''));
|
|
4638
|
+
fs.rmSync(ud, { recursive: true, force: true });
|
|
4639
|
+
fs.rmSync(sd, { recursive: true, force: true });
|
|
4640
|
+
ok = cssOk && travBlocked && scanOk;
|
|
4641
|
+
} catch {}
|
|
4642
|
+
console.log(ok ? '✓ B(1.9.350) 외부리뷰 P1보안: CSS breakout/skill traversal 차단 + secret scan false-neg/패턴 보강 (UR-0060/0061/0062)' : '✗ P1 보안 하드닝 실패');
|
|
4643
|
+
if (!ok) failed++;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4597
4646
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
4598
4647
|
if (failed > 0) process.exit(1);
|