@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +1 -1
- package/knowledge/shared/harness-core/capability_composition_contract.md +25 -4
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +33 -0
- package/package.json +2 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +37 -5
- package/plugins/fh-commons/skills/mcp-circuit-breaker/SKILL.md +30 -13
- package/plugins/fh-commons/skills/token-budget-gate/SKILL.md +31 -12
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/field-harvest/SKILL.md +34 -11
- package/plugins/fh-meta/skills/marketplace-gate/SKILL.md +59 -31
- package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +13 -3
- package/plugins/fh-meta/skills/public-surface-audit/SKILL.md +56 -22
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +50 -11
- package/plugins/fh-meta/skills/return-path-gate/SKILL.md +36 -7
- package/scripts/capability_registry_check.sh +280 -0
- package/scripts/psa_scan_lib.sh +53 -1
- package/scripts/relay_channel.sh +6 -1
package/scripts/psa_scan_lib.sh
CHANGED
|
@@ -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
|
-
.
|
|
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
|
package/scripts/relay_channel.sh
CHANGED
|
@@ -92,7 +92,12 @@ ORDER_judge="mechanical model"
|
|
|
92
92
|
# 집합 축
|
|
93
93
|
AXES_SET="verdict_binding"
|
|
94
94
|
# 선언 파일이 쓸 수 있는 비-축 키 (메타데이터)
|
|
95
|
-
|
|
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; }
|