@chrono-meta/fh-gate 1.4.93 → 1.4.95

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.
@@ -48,11 +48,62 @@
48
48
  # would let a real token pass by merely CONTAINING a placeholder word.
49
49
  PSA_PLACEHOLDER='^(<[a-z0-9_-]+>|\{[a-z_]+\}|EXAMPLE|dummy|changeme|REDACTED|xxxx|/Users/(EXAMPLE|yourname|\{[a-z_]+\}|<[a-z0-9_-]+>)/|AKIAIOSFODNN7EXAMPLE)$'
50
50
 
51
+ # ── file::token allowlist (ALL severities) — the SKILL's Step 2, finally implemented ────────────
52
+ #
53
+ # public-surface-audit SKILL.md §Step 2 has always specified a `file path :: token` allowlist whose
54
+ # rule is severity-agnostic ("a hit on file F matching token T is suppressed iff a row exists with
55
+ # file == F and T in that row's allowed tokens"). The library implemented only the LOW subset below,
56
+ # so spec and implementation disagreed: a HIGH hit that the operator had deliberately published had
57
+ # **no expressible disposition at all** — the only ways out were weakening a severity class or
58
+ # editing the artifact. That is the divergent-normalizer shape, and it surfaced for real on
59
+ # 2026-08-11 (a published paper's author-contact line and a rename changelog entry).
60
+ #
61
+ # Why this does not weaken the floor:
62
+ # · rows live in a **gitignored** source (`.claude/rules/.public-surface-allowlist`), so writing one
63
+ # never puts an operator token on the public surface — the same two-layer discipline as patterns.
64
+ # · a row is `path<TAB>literal-token`. **Both sides are literal, whole-value comparisons** — the
65
+ # path with `=`, the token with `grep -qxF`. Neither is a pattern, so one row suppresses exactly
66
+ # one token on exactly one file.
67
+ # ⚠️ Two versions of this got it wrong, and the second is the instructive one. v1 claimed a row
68
+ # "cannot become a blanket mute" while the code used an UNANCHORED `grep -qE`: a `path<TAB>.*`
69
+ # row muted every hit in that file, silently, at every severity (pre-publish security pass proved
70
+ # it with a known pair). v2 force-anchored to `^(…)$` — and `^(.*)$` still matches everything, so
71
+ # the blanket mute survived the "fix". Only making the field a **literal** actually closes it.
72
+ # The lesson is the file's own doctrine: a claim is not a control, and neither is a repair that
73
+ # was not re-measured against the same known pair.
74
+ # · every suppression **prints a line**. A mute that leaves no trace is indistinguishable from a
75
+ # clean scan — the same `not found ≠ 0` shape the verdict enum exists to prevent.
76
+ # · absent file → no rows → behaviour is exactly as before (fail-closed by default).
77
+ # Each row is an operator decision, recorded where it can be audited, instead of an undocumented
78
+ # exception living in someone's head.
79
+ psa_pair_allowlisted() { # $1=path $2=matched token
80
+ local root="${PSA_REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || printf '.')}"
81
+ local src="${PSA_ALLOWLIST:-$root/.claude/rules/.public-surface-allowlist}"
82
+ [ -r "$src" ] || return 1
83
+ local apath atok
84
+ while IFS=$'\t' read -r apath atok || [ -n "$apath" ]; do
85
+ case "$apath" in ''|'#'*) continue ;; esac
86
+ [ -z "$atok" ] && continue
87
+ [ "$apath" = "$1" ] || continue
88
+ # LITERAL, whole-token (`-x -F`). Not a regex — anchoring alone was not enough: `^(.*)$` still
89
+ # matches everything, so an anchored `.*` row remained a blanket mute. Treating the field as a
90
+ # literal is what makes "one row = one token" true rather than merely asserted.
91
+ if printf '%s' "$2" | grep -qxF "$atok" 2>/dev/null; then
92
+ echo " ⚪ allowlisted — ${1}: '${2}' (row: ${apath} :: ${atok})"
93
+ return 0
94
+ fi
95
+ done < "$src"
96
+ return 1
97
+ }
98
+
51
99
  # Files that name wiring/companion tokens as part of doing their job. LOW severity only — HIGH/MED
52
100
  # block everywhere, including here. Kept as a function so the list has exactly one definition.
53
101
  psa_low_allowlisted() {
54
102
  case "$1" in
55
- .gitignore|scripts/sync-to-be.sh|.claude/rules/local_fh_context.md|templates/local_fh_context.md|templates/.claude/rules/*|templates/.git-hooks/*) return 0 ;;
103
+ # sync-from-be.sh 는 sync-to-be.sh **같은 일의 반대 방향**인데 목록에 없어서
104
+ # LOW 를 계속 냈다(2026-08-11, 두 렌즈 relay 런 첫 실사용에서 발화). 짝을 빠뜨린
105
+ # 목록은 그 짝만 상시 오탐이 된다.
106
+ .gitignore|scripts/sync-to-be.sh|scripts/sync-from-be.sh|.claude/rules/local_fh_context.md|templates/local_fh_context.md|templates/.claude/rules/*|templates/.git-hooks/*) return 0 ;;
56
107
  *) return 1 ;;
57
108
  esac
58
109
  }
@@ -174,6 +225,7 @@ psa_scan_tagged() {
174
225
  [ -z "$tok" ] && continue
175
226
  printf '%s' "$tok" | grep -qiE "$PSA_PLACEHOLDER" && continue
176
227
  if [ "$sev" = "LOW" ] && psa_low_allowlisted "$path"; then continue; fi
228
+ if psa_pair_allowlisted "$path" "$tok"; then continue; fi
177
229
  echo " ❌ $sev leak — ${path}: '$tok'"
178
230
  hit=1
179
231
  done <<PSA_TOK
@@ -92,7 +92,12 @@ ORDER_judge="mechanical model"
92
92
  # 집합 축
93
93
  AXES_SET="verdict_binding"
94
94
  # 선언 파일이 쓸 수 있는 비-축 키 (메타데이터)
95
- META_KEYS="id entry requires_cwd verdict_channel verdict_enum verdict_stdout_key upstream_argv echoes_upstream"
95
+ # 스펙 §ⓑ.2 `calibration:` 블록(known_positive/known_negative)은 등록 시점 필드다.
96
+ # 여기는 call moment 라 그 값을 **쓰지 않지만**, CLOSED 목록에서 빠뜨리면 스펙대로 쓴
97
+ # 선언이 VIOLATION 으로 거부된다 — 2026-08-11 실측: registry_check 를 통과한 capfile 2개가
98
+ # relay 에서 8건 VIOLATION. 같은 스펙에 대해 두 계기가 서로 다른 스키마를 든
99
+ # divergent-normalizer 이고, 관대함이 갈리면 한쪽만 통과하는 입력이 생긴다.
100
+ META_KEYS="id entry requires_cwd verdict_channel verdict_enum verdict_stdout_key upstream_argv echoes_upstream calibration_positive_args calibration_positive_expect calibration_negative_args calibration_negative_expect"
96
101
 
97
102
  _die() { printf '❌ %s\n' "$*" >&2; exit "$RC_HARNESS"; }
98
103
  _violation() { printf '⛔ COMPOSITION_VIOLATION — %s\n' "$*" >&2; VIOLATED=1; }