leerness 1.36.183 → 1.36.185

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/lib/pure-utils.js CHANGED
@@ -1,12 +1,27 @@
1
1
  'use strict';
2
2
 
3
+ // Model identifiers reach argv surfaces. Keep the shell-inert predicate in this
4
+ // side-effect-free module so persisted schemas and runtime dispatch cannot drift.
5
+ const SAFE_MODEL_IDENTIFIER_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$/;
6
+ function isValidModelIdentifier(value) {
7
+ const raw = String(value == null ? '' : value);
8
+ const visible = stripDefaultIgnorables(raw, { nfkc: true, visibleIdentity: true })
9
+ .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, '')
10
+ .trim();
11
+ return raw === visible && SAFE_MODEL_IDENTIFIER_RE.test(visible);
12
+ }
13
+
3
14
  // 1.9.274 (UR-0025 1단계, GPT-5.5 리뷰): bin/harness.js 단일 대형 파일 모듈 분리 — 점진적·비파괴 시작.
4
15
  // 여기에는 harness 내부 상태/다른 함수에 의존하지 않는 "순수 함수"만 추출한다 (부작용 0, 단위 테스트 대상).
5
16
  // harness.js 는 이 모듈을 require 해 동일 이름으로 사용한다. 동작 동일 — selftest 가 7종 모두 검증.
6
17
 
7
18
  // 보안: 환경변수 키가 시크릿(TOKEN/SECRET/PASSWORD/API_KEY/PRIVATE)인지 판별.
8
19
  function _isSecretKey(k) {
9
- return /TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE/i.test(k);
20
+ // Environment keys are identifiers, not display text. Classify their
21
+ // compatibility spelling so full-width and Default-Ignorable variants cannot
22
+ // pass an ASCII allow-list and reach a child process with their secret value.
23
+ const canonical = stripDefaultIgnorables(k, { nfkc: true, visibleIdentity: true });
24
+ return /TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE/i.test(canonical);
10
25
  }
11
26
 
12
27
  // 표시용 자유 텍스트에서 시크릿 값을 가린다 — 키 이름은 남겨 맥락을 유지하고 **값만** 지운다.
@@ -263,13 +278,19 @@ function _tierRank(t) { const i = PERMISSION_TIERS.indexOf(String(t || '')); ret
263
278
  // 명령/capability → 요구 등급 (순수 매핑)
264
279
  function _requiredTier(cmd) {
265
280
  const c = String(cmd || '').toLowerCase();
281
+ // Lease operands are arbitrary file names. Classify the parsed command/action
282
+ // prefix before scanning generic keywords so a path such as
283
+ // `dir/release publish/report.js` cannot turn a read-only check into publish.
284
+ const leaseCommand = c.match(/^\s*(?:(?:npx(?:\s+(?:-y|--yes))?\s+)?leerness(?:@[^\s]+)?\s+)?lease\s+(list|check|acquire|release)(?:\s|$)/);
285
+ if (leaseCommand && (leaseCommand[1] === 'list' || leaseCommand[1] === 'check')) return 'read-only';
286
+ if (leaseCommand) return 'safe-write';
266
287
  if (/release\s+publish|npm\s+publish|\bpublish\b/.test(c)) return 'publish';
267
288
  if (/\bweb\b/.test(c)) return 'network';
268
289
  if (/git\s+push|sync-main/.test(c)) return 'git-write';
269
290
  if (/multi\s+--execute|dispatch\s+--write|--yolo|\bpc\b/.test(c)) return 'shell-write';
270
291
  if (/agents\s+(list|quota|bench)|--run-tests/.test(c)) return 'shell-read';
271
292
  if (/\binit\b|\badapter\b|update\s+--yes|\bmigrate\b/.test(c)) return 'project-write';
272
- if (/state\s+(start|record|verify|handoff)|decision|lesson|plan\s+add|task\s+add|rule\s+add/.test(c)) return 'safe-write';
293
+ if (/state\s+(start|record|verify|handoff)|decision|lesson|plan\s+add|task\s+add|rule\s+add|lease\s+(acquire|release)/.test(c)) return 'safe-write';
273
294
  return 'read-only';
274
295
  }
275
296
  function _policyAllows(allowedTier, requiredTier) { return _tierRank(requiredTier) <= _tierRank(allowedTier); }
@@ -868,10 +889,15 @@ function _renderDecisionsMd(decisions) {
868
889
  // preamble 의 코드펜스(```)는 single-quote 문자열로 안전 처리 (template literal 충돌 회피)
869
890
  const preamble = '# Decisions\n\n## Template (예시 — 실제 결정은 아래 코드블록 밖에 추가)\n\n'
870
891
  + '```md\n### YYYY-MM-DD — Decision 제목\n- Decision:\n- Reason:\n- Alternatives:\n- Impact:\n```\n';
892
+ const field = (name, value) => {
893
+ const safe = _lineSafe(value || '');
894
+ return `- ${name}:${safe ? ' ' + safe : ''}`;
895
+ };
871
896
  const body = (decisions || []).map(d => {
872
897
  // 1.9.402 (UR-0108): 필드 개행 → 공백(MD projection '### '/'- field:' 라인 위조 블록 주입 차단). canonical JSON 은 raw 유지.
898
+ // 1.36.185: 빈 canonical 필드는 colon 뒤 공백을 만들지 않는다 — decision add 후 git diff --check 위반 차단.
873
899
  const head = d.date ? `${_lineSafe(d.date)} — ${_lineSafe(d.title)}` : _lineSafe(d.title);
874
- return `\n### ${head}\n- Decision: ${_lineSafe(d.decision || '')}\n- Reason: ${_lineSafe(d.reason || '')}\n- Alternatives: ${_lineSafe(d.alternatives || '')}\n- Impact: ${_lineSafe(d.impact || '')}\n`;
900
+ return `\n### ${head}\n${field('Decision', d.decision)}\n${field('Reason', d.reason)}\n${field('Alternatives', d.alternatives)}\n${field('Impact', d.impact)}\n`;
875
901
  }).join('');
876
902
  return preamble + body;
877
903
  }
@@ -1886,6 +1912,7 @@ module.exports = { _decisionBlocksWithOffset, _blocksWithOffset, _lineOfOffset,
1886
1912
  _parseImplExports, _briefUnfilled, _planGoalUnfilled, _mdSectionBody, _draftAnchors, _replaceMdSection,
1887
1913
  _matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, GLOSSARY_START, GLOSSARY_END,
1888
1914
  _isSecretKey, redactSecrets, hasCredentialMarker, compareVer, parseHarnessVersion,
1915
+ SAFE_MODEL_IDENTIFIER_RE, isValidModelIdentifier,
1889
1916
  _isPlaceholderSecret, _looksSecretLike,
1890
1917
  _mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _splitPreserved, PRESERVED_TAG, _detectOrphanGuards, _parseSkillsValue,
1891
1918
  _parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide, _memorySurface, _renderPulseLine,
@@ -1949,7 +1976,7 @@ module.exports = { _decisionBlocksWithOffset, _blocksWithOffset, _lineOfOffset,
1949
1976
  // 1.9.399 (7번째 버그헌트 P1-A, UR-0104): markdown 테이블 셀 안전화(파이프/개행 injection 차단)
1950
1977
  _cellSafe, _cellUnescape,
1951
1978
  // 1.9.402 (7번째 버그헌트 P1-A 잔여, UR-0108): MD projection 라인 안전화(개행→공백)
1952
- _lineSafe, _milestoneSurface, _recentChangesSurface, _recentChangesAggregate,
1979
+ _lineSafe, stripDefaultIgnorables, _milestoneSurface, _recentChangesSurface, _recentChangesAggregate,
1953
1980
  // 1.9.407 (8번째 버그헌트, UR-0111): --limit 안전 파싱(NaN/음수/0 → 기본값)
1954
1981
  _parseLimit,
1955
1982
  // 1.9.416 (9th 외부평가, UR-0122): add 류 제목 파싱(flag/경로 break) 단일 출처
@@ -2234,6 +2261,27 @@ function _cellUnescape(s) { return String(s == null ? '' : s).replace(/\\\|/g, '
2234
2261
  // ESC/CSI·VT·FF·NEL도 제거해 진단 출력과 MD projection에서 가짜 줄뿐 아니라 화면 지우기/커서 이동도 차단한다.
2235
2262
  function _lineSafe(s) { return String(s == null ? '' : s).replace(/\r\n|[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, ' '); }
2236
2263
 
2264
+ // Security-sensitive comparisons must share one Unicode policy. A hand-maintained
2265
+ // list had diverged between routing and provenance and missed Shorthand Format
2266
+ // Controls (U+1BCA0..U+1BCAF). Prefer the Unicode binary property exposed by
2267
+ // supported Node runtimes, with a conservative fallback for older engines.
2268
+ let _defaultIgnorableRe;
2269
+ try { _defaultIgnorableRe = new RegExp('\\p{Default_Ignorable_Code_Point}', 'gu'); }
2270
+ catch {
2271
+ _defaultIgnorableRe = /[\u00ad\u034f\u061c\u115f-\u1160\u17b4-\u17b5\u180b-\u180f\u200b-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\u{1bca0}-\u{1bcaf}\u{1d173}-\u{1d17a}\u{e0000}-\u{e0fff}]/gu;
2272
+ }
2273
+ function stripDefaultIgnorables(value, options = {}) {
2274
+ let text = String(value == null ? '' : value);
2275
+ if (options.nfkc === true) {
2276
+ try { text = text.normalize('NFKC'); } catch {}
2277
+ }
2278
+ text = text.replace(_defaultIgnorableRe, '');
2279
+ // Braille blank is visible-space spoofing rather than a Unicode default
2280
+ // ignorable. Identity/approval callers opt into removing it as well.
2281
+ if (options.visibleIdentity === true) text = text.replace(/\u2800/g, '');
2282
+ return text;
2283
+ }
2284
+
2237
2285
  // Git-backed aggregate surfaces must not turn "could not measure" into a
2238
2286
  // believable zero. Handoff, health, and session-close share this projection.
2239
2287
  function _milestoneSurface(value) {