@chrono-meta/fh-gate 2.6.0 → 2.7.0
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/rules/fh_4axis_gate.md +26 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +24 -3
- package/CLAUDE.md +39 -12
- package/README.ja.md +32 -8
- package/README.ko.md +32 -8
- package/README.md +112 -15
- package/README.zh.md +28 -8
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +19 -5
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +12 -2
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +32 -0
- package/knowledge/shared/harness-core/ship_readiness_gate.md +77 -8
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +90 -0
- package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
- package/package.json +13 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +111 -0
- package/plugins/fh-meta/agents/persona-innovator.md +170 -0
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +16 -1
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +97 -0
- package/scripts/adapters/peer_resolve.sh +58 -6
- package/scripts/cluster_capability_scan.sh +42 -13
- package/scripts/digest_landing_check.sh +142 -2
- package/scripts/fh_hub_identity.sh +83 -0
- package/scripts/fh_session_load.sh +53 -5
- package/scripts/fh_track_resolve.sh +114 -0
- package/scripts/field_canon_preload.sh +50 -5
- package/scripts/package_coverage_check.sh +8 -0
- package/scripts/prior_art_prompt.sh +168 -0
- package/scripts/psa_scan_lib.sh +201 -15
- package/scripts/residency_admission_check.sh +204 -0
- package/scripts/selfcheck.sh +88 -0
- package/scripts/test_adapter_lanes.sh +67 -2
- package/scripts/test_heavy_classifier_lanes.sh +144 -0
- package/scripts/test_marker_defense_lanes.sh +152 -0
- package/scripts/test_marker_soul_check_lanes.sh +211 -0
- package/scripts/test_prior_art_prompt_lanes.sh +128 -0
- package/scripts/test_psa_singlefile_lanes.sh +351 -1
- package/scripts/test_residency_admission_lanes.sh +60 -0
- package/scripts/test_track_resolve_lanes.sh +158 -0
- package/templates/.git-hooks/pre-commit +400 -4
- package/templates/.git-hooks/pre-push +17 -2
- package/templates/settings.PriorArt.snippet.json +15 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# prior_art_prompt.sh — T2 «새로 짓기 시작» 시점에 **내 컨텍스트로** 한 줄을 넣는다.
|
|
3
|
+
#
|
|
4
|
+
# ── 왜 이 자리인가 (2026-08-21, 챔버 런 consolidation-path-gate) ──
|
|
5
|
+
# 운영자 실경험이 준 두 시점: T1 착수(세션 갓 시작) · **T2 컨텍스트가 길어진 뒤 또 새로 빌드업할 때**.
|
|
6
|
+
# 이 훅이 T2 다. PreToolUse(Write) 는 «새 파일을 쓰기 직전» 이라 그 경계와 정확히 겹친다.
|
|
7
|
+
#
|
|
8
|
+
# ── 왜 산문이 아니라 훅인가 (실측) ──
|
|
9
|
+
# 같은 날 sim: **명시 지시 3/3 · advisory 0/3 · 프레이밍 0/3**. CLAUDE.md 에 규율을 더 적는 것은
|
|
10
|
+
# 프레이밍이라 0/3 자리다. `additionalContext` 는 훅이 **모델 컨텍스트에 지시로** 넣는 유일한
|
|
11
|
+
# 채널이고([[feedback_advisory_invisible_to_actor]]), 그게 3/3 자리다.
|
|
12
|
+
#
|
|
13
|
+
# ── 왜 서브에이전트가 아닌가 ──
|
|
14
|
+
# 같은 날 먼저 지은 `persona-innovator` Mode X 는 **불렀을 때** 돈다. 남에게 시킨 숙고는 그 순간의
|
|
15
|
+
# 세션을 안 바꾼다 — 오늘 자력 적발 0 · 외부(운영자5·codex·challenger·레인·CI) 적발 10+ 이 그 증거다.
|
|
16
|
+
#
|
|
17
|
+
# ── 이건 브레이크가 아니라 곱셈기다 (운영자, 2026-08-21) ──
|
|
18
|
+
# *"충분히 세상을 찾아봤으면 그 이후에는 가진 능력을 활용해서 개발을 해내야 한다. 이때 내부의
|
|
19
|
+
# 능력을 100% 활용해야지. 조합하면 배수는 지수적으로 올라갈 수 있고. 그게 가능한 이유는 바깥의
|
|
20
|
+
# 문제를 풀어낸 데이터들과 내부의 능력 오케스트레이션이 서로 엮이고 곱해지기 때문이다."*
|
|
21
|
+
# ⇒ 이 훅의 목적은 «짓지 마라» 가 아니라 «바깥을 입력으로 삼아 안을 100% 써라» 다.
|
|
22
|
+
# 금지로 읽히면 무시당하고, 곱셈기로 읽히면 쓰인다. 문안이 그렇게 끝나야 한다.
|
|
23
|
+
#
|
|
24
|
+
# ── 새 판단 0줄 ──
|
|
25
|
+
# 🟥 «명명 어휘» 가 이 훅의 실제 기전이다 (2026-08-21 실측). 같은 날 sim 에서 양팔 다 검색했는데
|
|
26
|
+
# 힌트 쥔 쪽만 정답에 닿았다(2/3 vs 0/3) — 차이는 검색 의지가 아니라 **무엇을 찾을지 이름 붙이는
|
|
27
|
+
# 능력**이었다. 운영자: *"늘 책을 읽는 사람이 AI 를 더 잘 다루어낸다."* 독서가 주는 것이 명명
|
|
28
|
+
# 어휘이고, 이 훅은 그걸 그 순간에 빌려주는 것이다. 그래서 문안이 «찾아라» 가 아니라
|
|
29
|
+
# «일반 어휘로 이름 붙여 찾아라» 로 끝난다.
|
|
30
|
+
#
|
|
31
|
+
# ── 사다리지 체크리스트가 아니다 (운영자, 2026-08-21) ──
|
|
32
|
+
# *"근처에 있는 책장 찾기 > 책장에도 없으면 도서관 가기"* — 순서가 하중이다. 셋이 그 순서를 지지한다:
|
|
33
|
+
# 코퍼스(ⓑ내부미조회 9 > ⓐ외부미조회 6) · 이 세션 sim(내부 부재판정 오류 6/6) · SAAS 2605.29796
|
|
34
|
+
# («internal knowledge suffices 인데 무턱대고 검색 = severe over-search»). 평평한 목록은 그 순서를
|
|
35
|
+
# 못 준다. 그리고 FH 자산 이름이 이미 `deep_research_capability_ladder` 다 — 사다리가 원래 형태다.
|
|
36
|
+
#
|
|
37
|
+
# 내용은 **이미 있는 스킬 넷을 가리키기만** 한다(운영자: *"우리 안에 그 스킬들이 있고 그 스킬들을
|
|
38
|
+
# 발휘하면 세계에 닿겠지"*). 내부=asset-placement-gate Step 0.6 · CATALOG · novelty_claim_check ·
|
|
39
|
+
# 외부=deep_research_capability_ladder · 숙고=fh-commons:deliberation.
|
|
40
|
+
#
|
|
41
|
+
# ── degrade ──
|
|
42
|
+
# 가역 표면(파일 쓰기)이라 **절대 안 막는다.** exit 0 고정, permissionDecision 없음.
|
|
43
|
+
# 판정도 안 한다 — 한 줄 제안이다. 조용한 실패(파싱 불가·python3 부재)는 무음 통과.
|
|
44
|
+
set -uo pipefail
|
|
45
|
+
RAW=$(cat 2>/dev/null || true)
|
|
46
|
+
# file_path 와 session_id 를 한 번에 뽑는다(파이프 두 번 안 탄다).
|
|
47
|
+
PARSED=$(printf '%s' "$RAW" | python3 -c '
|
|
48
|
+
import json,sys
|
|
49
|
+
try: d=json.load(sys.stdin)
|
|
50
|
+
except Exception: sys.exit(0)
|
|
51
|
+
ti=d.get("tool_input",{}) or {}
|
|
52
|
+
fp=(ti.get("file_path","") or "")
|
|
53
|
+
cmd=(ti.get("command","") or "").replace("\t"," ").replace("\n"," ")
|
|
54
|
+
sid=(d.get("session_id","") or "")
|
|
55
|
+
sys.stdout.buffer.write((fp+"\t"+sid+"\t"+cmd).encode("utf-8"))
|
|
56
|
+
' 2>/dev/null) || exit 0
|
|
57
|
+
FILE="${PARSED%%$'\t'*}"
|
|
58
|
+
_REST="${PARSED#*$'\t'}"
|
|
59
|
+
SID="${_REST%%$'\t'*}"
|
|
60
|
+
CMD="${_REST#*$'\t'}"
|
|
61
|
+
[ "$CMD" = "$_REST" ] && CMD=""
|
|
62
|
+
|
|
63
|
+
# ── 🟥 Bash 경로 (2026-08-21, 첫 실사용이 «비발화» 로 났다) ──
|
|
64
|
+
# 초판 matcher 는 `Write` 뿐이었다. 그런데 auto-mode 는 파일 생성을 **Bash 의 리다이렉트/heredoc**
|
|
65
|
+
# 으로 한다(이 저장소 CLAUDE.md 가 그렇게 지시한다: *"make file changes with sed, heredocs…"*).
|
|
66
|
+
# 즉 이 훅이 겨냥한 «새 메커니즘을 짓기 직전» 이 **구조적으로 Write 를 안 탄다.**
|
|
67
|
+
# ⇒ matcher 에 Bash 를 넣고, `file_path` 가 없으면 **명령의 리다이렉트 대상**에서 뽑는다.
|
|
68
|
+
#
|
|
69
|
+
# 🟥 heredoc 을 파싱하지 않는다 — 리다이렉트 «대상 경로» 만 본다. `cat > x.sh <<EOF` 든
|
|
70
|
+
# `printf … > x.sh` 든 대상은 `> ` 뒤에 있고, 본문은 볼 필요가 없다. 놓치는 형태
|
|
71
|
+
# (`tar x` · 에디터 · `cp`)는 **명시된 잔여**다 — 이 훅은 판정이 아니라 제안이라 미발화의
|
|
72
|
+
# 비용이 오발화보다 싸다.
|
|
73
|
+
if [ -z "$FILE" ] && [ -n "$CMD" ]; then
|
|
74
|
+
# 🟥 cross-family 4 HIGH 를 받고 다시 지었다 (2026-08-21). 넷 다 **오발화**였고,
|
|
75
|
+
# 이 훅에서 오발화는 미발화보다 훨씬 비싸다(소음 = 자기무력화 경로).
|
|
76
|
+
# ① `[[:space:]]tee` 에 **뒤 경계가 없어** `rg tee_probe.py` 가 tee 대상으로 읽혔다
|
|
77
|
+
# ② `>>?` 가 **`2>` 안의 `>`** 를 잡아 `pytest 2> /tmp/errors.sh` 가 발화했다
|
|
78
|
+
# ③ `git diff > /tmp/review_delta.sh` — 리포트인데 이름이 `.sh` 라 발화했다
|
|
79
|
+
# ④ `"$LOG_DIR/x.sh"` — 변수가 안 풀려 리터럴 `$LOG_DIR/...` 로 존재검사를 통과했다
|
|
80
|
+
# 수리 셋: ⓐ 리다이렉트는 **앞이 «줄머리 또는 공백»** 이어야 한다(→ `2>`·`1>`·`&>` 배제)
|
|
81
|
+
# ⓑ `tee` 는 **앞뒤 공백**을 요구한다 ⓒ 대상은 **이 프로젝트 안**이어야 한다
|
|
82
|
+
# ⓒ 가 ②③ 의 현실적 형태를 함께 죽인다 — 새 «메커니즘» 은 /tmp 에 안 산다.
|
|
83
|
+
for _c in $(printf '%s' "$CMD" \
|
|
84
|
+
| grep -oE '(^|[[:space:]])(>>?|tee[[:space:]])[[:space:]]*"?'"'"'?[^ "'"'"'|;&)<>]+' 2>/dev/null \
|
|
85
|
+
| sed -E 's/^[[:space:]]*(>>?|tee[[:space:]])[[:space:]]*//; s/^["'"'"']//'); do
|
|
86
|
+
case "$_c" in
|
|
87
|
+
''|/dev/*|\&*|-) continue ;;
|
|
88
|
+
*'$'*) continue ;; # ④ 미확장 변수 — 리터럴로 판정하면 거짓이다
|
|
89
|
+
*..*) continue ;; # 상위 탈출 경로는 «이 프로젝트 안» 이 아니다
|
|
90
|
+
esac
|
|
91
|
+
# ⓒ 프로젝트 밖 절대경로는 대상이 아니다. 상대경로는 프로젝트 기준이라 통과시킨다.
|
|
92
|
+
case "$_c" in
|
|
93
|
+
/*) case "$_c" in
|
|
94
|
+
"${CLAUDE_PROJECT_DIR:-/nonexistent-project-dir}"/*) ;;
|
|
95
|
+
*) continue ;;
|
|
96
|
+
esac ;;
|
|
97
|
+
esac
|
|
98
|
+
case "$_c" in
|
|
99
|
+
*.sh|*/SKILL.md|*/SKILL_detail.md|*/agents/*.md|*/rules/*.md|*.py|*.js|*.ts) FILE="$_c"; break ;;
|
|
100
|
+
esac
|
|
101
|
+
done
|
|
102
|
+
fi
|
|
103
|
+
[ -n "$FILE" ] || exit 0
|
|
104
|
+
|
|
105
|
+
# ── 발동 조건 — 소음이 이 능력을 죽인다(냉독자 «자기무력화 경로») ──
|
|
106
|
+
# ① 이미 있는 파일 편집은 «새로 짓기» 가 아니다
|
|
107
|
+
[ -e "$FILE" ] && exit 0
|
|
108
|
+
# ② 기록·산출물은 대상이 아니다. «메커니즘» 만.
|
|
109
|
+
# 🟥 상대경로도 잡아야 한다 (2026-08-21 레인 D4). 초판 제외는 `*/tracks/*` 뿐이라
|
|
110
|
+
# **절대경로만** 걸렀다 — Write 는 절대경로를 주지만 **Bash 는 상대경로를 준다**
|
|
111
|
+
# (`cat > tracks/_meta/note.sh`). Bash 팔을 붙이면서 이 비대칭이 드러났고,
|
|
112
|
+
# 제외가 안 먹으면 이 훅은 산출물마다 뜨는 소음이 된다(=자기무력화 경로).
|
|
113
|
+
case "$FILE" in
|
|
114
|
+
tracks/*|logs/*|.git/*|node_modules/*) exit 0 ;;
|
|
115
|
+
*/tracks/*|*/logs/*|*.log|*.json|*.lock|*/node_modules/*|*/.git/*) exit 0 ;;
|
|
116
|
+
esac
|
|
117
|
+
case "$FILE" in
|
|
118
|
+
*.sh|*/SKILL.md|*/SKILL_detail.md|*/agents/*.md|*/rules/*.md|*.py|*.js|*.ts) ;;
|
|
119
|
+
*) exit 0 ;;
|
|
120
|
+
esac
|
|
121
|
+
# ③ **세션당** 한 번. 매 Write 마다 뜨면 3회 거절로 조용히 죽는다.
|
|
122
|
+
# 🟥 초판은 스탬프를 **날짜**로 잡았다 — 주석은 «세션당» 인데 구현은 «하루당» 이었고, 하루에 세션을
|
|
123
|
+
# 여럿 도는 환경(이 운영자가 그렇다)에서는 **첫 세션만 뜨고 나머지는 전부 침묵**한다. 내 레인도
|
|
124
|
+
# 못 잡았다: 디렉터리만 갈랐지 날짜 의미론을 안 쟀다. 훅 입력의 `session_id` 로 고친다.
|
|
125
|
+
# session_id 가 없으면(구버전·수동 호출) 날짜로 degrade — 침묵이 아니라 «덜 자주» 쪽이다.
|
|
126
|
+
# 🟥 `session_id` 는 **신뢰할 수 없는 입력**이다 (cross-family HIGH#5). 실측: `x/../../../name`
|
|
127
|
+
# 이 접두 검사를 통과해 프로젝트 밖에 파일을 만들었고, 끝에 `/` 를 붙이면 스탬프가 아예
|
|
128
|
+
# 안 써져 **매번 발화**했다(세션당 1회가 무력화). 화이트리스트로 좁힌다 — 빈 값이 되면
|
|
129
|
+
# 날짜로 degrade(침묵이 아니라 «덜 자주» 쪽).
|
|
130
|
+
_SID_SAFE=$(printf '%s' "${SID:-}" | tr -cd 'A-Za-z0-9_-')
|
|
131
|
+
STAMP="${CLAUDE_PROJECT_DIR:-.}/.claude/.prior_art_prompted_${_SID_SAFE:-$(date +%Y%m%d)}"
|
|
132
|
+
[ -f "$STAMP" ] && exit 0
|
|
133
|
+
mkdir -p "$(dirname "$STAMP")" 2>/dev/null || true
|
|
134
|
+
: > "$STAMP" 2>/dev/null || true
|
|
135
|
+
|
|
136
|
+
MSG="🔎 새 메커니즘을 쓰려는 참이다 ($(basename "$FILE")). 짓기 전에 **책장부터, 없으면 도서관** —
|
|
137
|
+
안 했으면 지금 하고, 했으면 이 줄은 무시해라.
|
|
138
|
+
|
|
139
|
+
① 책장 바로 옆에 있나. **여기가 대부분이다.**
|
|
140
|
+
우리 안 : CATALOG.md · .claude/registry/ · plugins/**/SKILL.md · scripts/ 를 실제로 grep
|
|
141
|
+
옆 칸 : LOCAL_SKILL_REGISTRY · scripts/cluster_capability_scan.sh · §Cross-Project Skill Bus
|
|
142
|
+
🟥 여기서 나오면 «찾아온 것» 이 아니라 **바로 호출 가능한 능력**이다
|
|
143
|
+
🟥 «grep 0건» 을 «없다» 로 확정하지 마라 — 오늘 실측에서 세션 6/6 이 이 자리에서 틀렸다.
|
|
144
|
+
내 어휘로만 두드렸을 가능성을 먼저 의심해라.
|
|
145
|
+
|
|
146
|
+
② 도서관 **책장에 없을 때만** 간다. 있는데 가면 그건 over-search 다(arXiv 2605.29796 이 명명한 실패모드).
|
|
147
|
+
갈 때는 문제를 **일반 어휘로 이름 붙여서** — 내부 어휘로는 세계에 안 닿는다.
|
|
148
|
+
경로: knowledge/shared/harness-core/deep_research_capability_ladder.md
|
|
149
|
+
|
|
150
|
+
③ 숙고 ①②를 놓고 갈랐나 — fh-commons:deliberation (제안→반론→종합)
|
|
151
|
+
|
|
152
|
+
④ 짓기 🟥 **탐색은 끝이 아니라 입력이다.** 찾았으면 거기서 멈추지 말고 **가진 능력을 100% 써서
|
|
153
|
+
지어라** — 바깥이 푼 데이터 × 내부 오케스트레이션은 더해지는 게 아니라 **곱해진다**
|
|
154
|
+
(arXiv 2607.06906 실측: 품질/달러 +82%, 백만토큰당 완수 54.9→92.0).
|
|
155
|
+
합주 형식은 이미 있다: CLAUDE.md §Intent Marshaling.
|
|
156
|
+
|
|
157
|
+
안 밟은 칸이 있으면 한 줄로 물어라: «이 문제 이름이 X 같은데 확인해볼까?»
|
|
158
|
+
판정이 아니라 제안이다. 이미 밟았으면 진행해라 — 단, 진행할 때 **내부 능력을 덜 쓰지 마라.**"
|
|
159
|
+
|
|
160
|
+
if json_out=$(printf '%s' "$MSG" | PYTHONIOENCODING=utf-8 python3 -c '
|
|
161
|
+
import json,sys
|
|
162
|
+
h=sys.stdin.read()
|
|
163
|
+
print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":h}}))
|
|
164
|
+
' 2>/dev/null) && [ -n "$json_out" ]; then
|
|
165
|
+
printf '%s\n' "$json_out"; exit 0
|
|
166
|
+
fi
|
|
167
|
+
printf '%s\n' "$MSG" >&2
|
|
168
|
+
exit 0
|
package/scripts/psa_scan_lib.sh
CHANGED
|
@@ -112,7 +112,19 @@ psa_low_allowlisted() {
|
|
|
112
112
|
# Prints diagnostics for every degraded state; sets the flags above. NEVER exits — the caller owns
|
|
113
113
|
# the degrade direction, because it differs by surface (see the header).
|
|
114
114
|
psa_load() {
|
|
115
|
-
local defaults="$1" override="$2" raw ov
|
|
115
|
+
local defaults="$1" override="$2" raw ov _tabtest
|
|
116
|
+
# 🟥 R5-3: 이 라이브러리는 `$'\t'` (ANSI-C quoting) 에 의존한다 — **POSIX 가 아니다.**
|
|
117
|
+
# dash 에서는 리터럴 `$\t` 로 읽혀 정상 행이 «TAB 없음» 으로 떨어지고, 스트림이 통째로
|
|
118
|
+
# 비면서 **bad=1 · stream=[] 로 조용히 무력화**됐다(cross-family 실측). 그건 미측정이지
|
|
119
|
+
# 「패턴이 없다」가 아니다. 지원 범위는 bash/zsh 이고, 그걸 산문이 아니라 여기서 확인한다.
|
|
120
|
+
# 판별은 길이다 — 실측: bash/zsh `len=1`(진짜 TAB) · dash `len=3`(리터럴 `$\t`).
|
|
121
|
+
_tabtest=$'\t'
|
|
122
|
+
if [ "${#_tabtest}" -ne 1 ]; then
|
|
123
|
+
echo " ❌ INSTRUMENT DEAD — this shell does not support \$'\\t' (ANSI-C quoting)." >&2
|
|
124
|
+
echo " psa_scan_lib.sh requires bash or zsh. Re-run under one of them; NOT SCANNED." >&2
|
|
125
|
+
PSA_STREAM=""; PSA_DEFAULTS_OK=0; PSA_OVERRIDE_PRESENT=0; PSA_BAD_ROWS=1
|
|
126
|
+
return 3
|
|
127
|
+
fi
|
|
116
128
|
PSA_STREAM=""; PSA_DEFAULTS_OK=0; PSA_OVERRIDE_PRESENT=0; PSA_BAD_ROWS=0
|
|
117
129
|
|
|
118
130
|
# Layer 1 — committed defaults. This file ships with the repo, so missing/unreadable/empty is a
|
|
@@ -156,11 +168,23 @@ $ov"; PSA_OVERRIDE_PRESENT=1
|
|
|
156
168
|
echo " ❌ unusable pattern row (no TAB between severity and regex): ${line%%$'\t'*}"
|
|
157
169
|
PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
|
|
158
170
|
fi
|
|
159
|
-
|
|
171
|
+
# 🟥 R4-1: 여기도 `cmd; rc=$?` 였다 — P3 를 스캔 경로에서만 고치고 **로더 경로로 전파를 안 봤다**.
|
|
172
|
+
# 정상 패턴은 빈 입력에 rc=1 을 내므로 `set -e` 호출자는 psa_load 에서 죽는다(선재, 양 셸 실측).
|
|
173
|
+
if printf '' | grep -E "$re" >/dev/null 2>&1; then rc=0; else rc=$?; fi # >=2 = regex error
|
|
160
174
|
if [ "$rc" -ge 2 ]; then
|
|
161
175
|
echo " ❌ unusable pattern (invalid regex) — this detector would match nothing: [${line%%$'\t'*}]"
|
|
162
176
|
PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
|
|
163
177
|
fi
|
|
178
|
+
# 🟥 P4 (선재 결함): 판별력 없는 패턴을 거부한다 — 빈 패턴 · `^` · `a*` · `SECRET|^$`.
|
|
179
|
+
# 모든 입력에 매치하므로 검출기로 **쓸 수 없는데** 초판 로더는 유효로 받아들였다.
|
|
180
|
+
# 🟥 그리고 이 검사는 **위의 컴파일 검사로는 못 한다**: `printf ''` 는 줄을 0개 주므로
|
|
181
|
+
# grep 이 매치할 대상 자체가 없어 **어떤 패턴이든 rc=1** 이다(실측). 위 주석의
|
|
182
|
+
# 「0 = 컴파일됨」은 도달 불가능한 값을 서술한 것이었다. 빈 «줄» 을 따로 먹인다.
|
|
183
|
+
if printf '\n' | grep -E "$re" >/dev/null 2>&1; then rc=0; else rc=$?; fi
|
|
184
|
+
if [ "$rc" -eq 0 ]; then
|
|
185
|
+
echo " ❌ unusable pattern (matches the EMPTY string — no discrimination): [${line%%$'\t'*}]"
|
|
186
|
+
PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
|
|
187
|
+
fi
|
|
164
188
|
valid="$valid
|
|
165
189
|
$line"
|
|
166
190
|
done <<PSA_ROWS
|
|
@@ -208,31 +232,128 @@ psa_detect_operator_context() {
|
|
|
208
232
|
# Why path-tagged input: the LOW allowlist is per FILE, so a scanner that flattens content loses the
|
|
209
233
|
# only information needed to apply it. The push copy did exactly that and blocked its own first real
|
|
210
234
|
# push on a companion-store name inside the script whose job is to sync to the companion store.
|
|
235
|
+
# _psa_in_selftest — «지금 psa_require_live 안에서 불린 것인가»를 **호출 스택**으로 판별한다.
|
|
236
|
+
# 🟥 변수 표식을 쓰면 안 된다: 외부가 `_PSA_IN_SELFTEST=1` 을 미리 export 하는 것만으로
|
|
237
|
+
# 자가검사가 통째로 생략된다(cross-family 반례 1b, 실행 재현). 전역 상태를 권한 표식처럼
|
|
238
|
+
# 믿는 형태다. 호출 스택은 **호출자가 미리 심을 수 없다.**
|
|
239
|
+
# bash=FUNCNAME · zsh=funcstack. 둘 다 없으면 판별 불가이므로 **재진입이 아니라고 답한다** —
|
|
240
|
+
# 그러면 자가검사가 한 번 더 돌 뿐(무한재귀는 psa_require_live 가 이 함수를 한 겹만 부르므로
|
|
241
|
+
# 발생하지 않는다), 반대로 «재진입이다」로 접으면 그게 fail-open 이다.
|
|
242
|
+
_psa_in_selftest() {
|
|
243
|
+
local _st=""
|
|
244
|
+
# shellcheck disable=SC2154
|
|
245
|
+
if [ -n "${FUNCNAME[*]:-}" ]; then _st=" ${FUNCNAME[*]} "
|
|
246
|
+
elif [ -n "${funcstack[*]:-}" ]; then _st=" ${funcstack[*]} "
|
|
247
|
+
else return 1; fi
|
|
248
|
+
case "$_st" in *" psa_require_live "*) return 0 ;; esac
|
|
249
|
+
return 1
|
|
250
|
+
}
|
|
251
|
+
|
|
211
252
|
psa_scan_tagged() {
|
|
212
|
-
|
|
213
|
-
|
|
253
|
+
# 🟥 `path` 를 지역선언하지 마라 — zsh 에서 `path` 는 `PATH` 와 **tied 된 특수 배열**이라
|
|
254
|
+
# `local path` 하는 순간 그 스코프의 `PATH` 가 빈 문자열이 되고, 바로 다음 줄의
|
|
255
|
+
# `$(cat)` 부터 `command not found: cat` 으로 죽는다. 그런데 이 함수의 계약이
|
|
256
|
+
# 「빈 입력 = 신고할 것 없음 = return 0」이라 **계기 사망이 «깨끗한 스캔»과 바이트 단위로
|
|
257
|
+
# 같은 출력**이 된다 — 이 저장소가 이름 붙인 «미측정을 0으로 렌더» 의 가장 조용한 얼굴이다.
|
|
258
|
+
# 실측 2026-08-21: zsh 에서 known-positive·known-negative 가 **둘 다 rc=0** 이었다.
|
|
259
|
+
# 형제 특수변수(`fpath` `status` `argv` `cdpath` …)도 같은 함정이다.
|
|
260
|
+
local input hit=0 row sev re _psa_path body tok _line_hits _tok_hits _lg _tg
|
|
261
|
+
|
|
262
|
+
# ── E: 진입 생존성 가드 (2026-08-21, cross-family FAIL 판정 후 재작성) ─────────────
|
|
263
|
+
# 🟥 **초판 E 는 fail-open 을 못 닫았다.** codex/gpt-5.6-sol 이 반례 6개를 전부 실행으로
|
|
264
|
+
# 재현했고 판정은 FAIL 이었다. 초판의 구조적 맹점: **«자가검사가 한 번 통과했다»를
|
|
265
|
+
# «그 뒤의 실제 스캔도 실행됐다»로 확장**한 것. 그 사이엔 시간·전역상태·파이프라인
|
|
266
|
+
# 상태라는 단절점이 셋 있다. 재작성이 닫는 것과 못 닫는 것을 아래에 이름으로 적는다.
|
|
267
|
+
#
|
|
268
|
+
# 🟥 **캐시 제거.** 초판은 `_PSA_LIVE_OK` 로 프로세스당 1회만 검사했다 — 첫 양성 뒤
|
|
269
|
+
# grep 을 죽이면 같은 양성이 rc=0 이 됐다(codex 재현). 그리고 **캐시는 뚫린 쪽(긴
|
|
270
|
+
# 스킬 세션)에서만 위험하고 안전한 쪽(짧은 훅)에서만 이득**이라 방향이 반대였다.
|
|
271
|
+
# 비용은 **실측했다: psa_require_live 1회 ≈ 64ms** (훅 경로 최대 4회 = 0.26초).
|
|
272
|
+
# 초판은 이 비용을 **안 재고** 캐시를 정당화했다 — 그게 진짜 결함이었다.
|
|
273
|
+
#
|
|
274
|
+
# 🟥 **재진입 표식은 `local` 로 «동적 스코프»에 둔다.** 초판은 전역 변수를 권한 표식처럼
|
|
275
|
+
# 믿어서, 외부가 `_PSA_IN_SELFTEST=1` 을 미리 export 하면 자가검사가 **통째로 생략**됐다
|
|
276
|
+
# (codex 재현: `grep(){ return 127; }` 상태에서 known-positive 가 rc=0 무출력).
|
|
277
|
+
# `local` 선언은 외부 값을 **가리고**, 함수가 끝나면 **자동 복구**된다 — unset 실패나
|
|
278
|
+
# 시그널·트랩으로 값이 남는 경로가 없어진다.
|
|
279
|
+
if ! _psa_in_selftest; then
|
|
280
|
+
# 🟥 실제 패턴 스트림을 여기서 본다. 자가검사는 PSA_STREAM 을 카나리아로 **바꿔치기**
|
|
281
|
+
# 하므로 «진짜 패턴이 비었거나 깨졌는지»를 구조적으로 못 본다(codex 반례 3).
|
|
282
|
+
if [ -z "${PSA_STREAM:-}" ]; then
|
|
283
|
+
echo " ❌ INSTRUMENT DEAD — PSA_STREAM is empty; nothing to match against. NOT SCANNED."
|
|
284
|
+
echo " ❌ INSTRUMENT DEAD — PSA_STREAM is empty; NOT SCANNED (call psa_load first)." >&2
|
|
285
|
+
return 3
|
|
286
|
+
fi
|
|
287
|
+
# 🟥 R4-6: **부분 로드된 패턴 집합으로 «깨끗»을 말하지 않는다.** 초판은 이것을 «호출자
|
|
288
|
+
# 계약»으로 두었고(헤더가 그렇게 적어뒀다), cross-family 가 그 계약을 안 지키는 호출자를
|
|
289
|
+
# 실측 재현했다 — `SECRET|^$` 한 행이 떨어져 나간 집합으로 `SECRET` 입력이 rc=0 «깨끗».
|
|
290
|
+
# 산문 계약을 라이브러리 층으로 내린다: 안 실린 검출기가 있으면 그건 미측정이다.
|
|
291
|
+
# (psa_load 를 안 부른 호출자는 PSA_BAD_ROWS 가 미설정이고, 그 경로는 위 빈-스트림
|
|
292
|
+
# 검사가 이미 잡는다. `case` 로 보는 이유는 비수치 오염에서 fail-open 하지 않기 위해서다.)
|
|
293
|
+
case "${PSA_BAD_ROWS:-0}" in
|
|
294
|
+
0) ;;
|
|
295
|
+
*) echo " ❌ INSTRUMENT DEAD — ${PSA_BAD_ROWS} pattern row(s) were dropped; the detector set is PARTIAL. NOT SCANNED."
|
|
296
|
+
echo " ❌ INSTRUMENT DEAD — ${PSA_BAD_ROWS} pattern row(s) dropped — a partial set cannot report clean." >&2
|
|
297
|
+
return 3 ;;
|
|
298
|
+
esac
|
|
299
|
+
if ! psa_require_live >/dev/null 2>&1; then
|
|
300
|
+
echo " ❌ INSTRUMENT DEAD — psa_scan_tagged self-test failed. NOT SCANNED."
|
|
301
|
+
echo " ❌ INSTRUMENT DEAD — psa_scan_tagged self-test failed. NOT SCANNED." >&2
|
|
302
|
+
echo " A 0 from this scanner is UNMEASURED, not clean. Re-run under bash." >&2
|
|
303
|
+
return 3
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# 🟥 `cat` 의 상태를 버리지 마라 (codex 반례 1). 초판은 `input=$(cat)` 로 대입만 하고
|
|
308
|
+
# rc 를 버려서, cat 이 죽어 입력이 비면 그게 곧 «신고할 것 없음»이 됐다.
|
|
309
|
+
if ! input=$(cat); then
|
|
310
|
+
echo " ❌ INSTRUMENT DEAD — could not read stdin. NOT SCANNED."
|
|
311
|
+
echo " ❌ INSTRUMENT DEAD — could not read stdin. NOT SCANNED." >&2
|
|
312
|
+
return 3
|
|
313
|
+
fi
|
|
214
314
|
[ -n "$input" ] || return 0
|
|
215
315
|
while IFS= read -r row; do
|
|
216
316
|
case "$row" in ''|\#*) continue;; esac
|
|
217
317
|
re="${row#*$'\t'}"; [ "$re" = "$row" ] && continue
|
|
218
318
|
sev="${row%%$'\t'*}"
|
|
319
|
+
# 🟥 grep 상태를 «매치 없음» 으로 접지 마라 (cross-family 반례 2, 2026-08-21).
|
|
320
|
+
# `2>/dev/null || true` 는 rc 0(매치)·1(없음)·**2 이상(계기 오류)**을 하나로 뭉갠다.
|
|
321
|
+
# 실측 도달 경로: 패턴 파일에 **깨진 정규식**이 한 줄 있으면 그 행만 조용히 아무것도
|
|
322
|
+
# 안 잡고 스캔은 rc=0 «깨끗» 을 낸다. 자가검사는 자기 카나리아 패턴을 쓰므로
|
|
323
|
+
# **진짜 패턴의 깨짐을 구조적으로 못 본다** — 그래서 여기서 봐야 한다.
|
|
324
|
+
# 🟥 P3 동형: grep 의 정상 «미매치»(rc=1)가 `set -e` 호출자를 죽인다.
|
|
325
|
+
if _line_hits=$(printf '%s\n' "$input" | grep -iE "$re" 2>/dev/null); then _lg=0; else _lg=$?; fi
|
|
326
|
+
if [ "$_lg" -gt 1 ]; then
|
|
327
|
+
echo " ❌ INSTRUMENT DEAD — grep failed (rc=$_lg) on pattern: $re"
|
|
328
|
+
echo " ❌ INSTRUMENT DEAD — grep failed (rc=$_lg) on pattern: $re" >&2
|
|
329
|
+
echo " A broken pattern row scans NOTHING and reads as clean. NOT SCANNED." >&2
|
|
330
|
+
return 3
|
|
331
|
+
fi
|
|
219
332
|
while IFS= read -r line; do
|
|
220
333
|
[ -z "$line" ] && continue
|
|
221
|
-
|
|
334
|
+
_psa_path="${line%%$'\t'*}"; body="${line#*$'\t'}"
|
|
222
335
|
# EVERY match on the line. Taking only the first let a documented placeholder earlier on the
|
|
223
336
|
# line shield a real token later on it.
|
|
337
|
+
if _tok_hits=$(printf '%s' "$body" | grep -oiE "$re" 2>/dev/null); then _tg=0; else _tg=$?; fi
|
|
338
|
+
if [ "$_tg" -gt 1 ]; then
|
|
339
|
+
echo " ❌ INSTRUMENT DEAD — grep -o failed (rc=$_tg) on pattern: $re"
|
|
340
|
+
echo " ❌ INSTRUMENT DEAD — grep -o failed (rc=$_tg) on pattern: $re" >&2
|
|
341
|
+
return 3
|
|
342
|
+
fi
|
|
224
343
|
while IFS= read -r tok; do
|
|
225
344
|
[ -z "$tok" ] && continue
|
|
226
345
|
printf '%s' "$tok" | grep -qiE "$PSA_PLACEHOLDER" && continue
|
|
227
|
-
if [ "$sev" = "LOW" ] && psa_low_allowlisted "$
|
|
228
|
-
if psa_pair_allowlisted "$
|
|
229
|
-
echo
|
|
346
|
+
if [ "$sev" = "LOW" ] && psa_low_allowlisted "$_psa_path"; then continue; fi
|
|
347
|
+
if psa_pair_allowlisted "$_psa_path" "$tok"; then continue; fi
|
|
348
|
+
# 🟥 P5: zsh 의 `echo` 는 백슬래시를 해석한다 — 경로 `dir\two` 가 탭으로 변해
|
|
349
|
+
# 증거 문자열이 손상됐다(실측). fail-open 은 아니나 증거는 증거여야 한다.
|
|
350
|
+
printf ' ❌ %s leak — %s: \x27%s\x27\n' "$sev" "${_psa_path}" "$tok"
|
|
230
351
|
hit=1
|
|
231
352
|
done <<PSA_TOK
|
|
232
|
-
$
|
|
353
|
+
$_tok_hits
|
|
233
354
|
PSA_TOK
|
|
234
355
|
done <<PSA_LINES
|
|
235
|
-
$
|
|
356
|
+
$_line_hits
|
|
236
357
|
PSA_LINES
|
|
237
358
|
done <<PSA_PAT
|
|
238
359
|
$PSA_STREAM
|
|
@@ -285,15 +406,42 @@ _psa_can_assign() {
|
|
|
285
406
|
# the caller's shell before the restore line. Status capture is now inside an `if`.
|
|
286
407
|
psa_require_live() {
|
|
287
408
|
local saved_stream saved_allow saved_stream_set saved_allow_set tmpd out rc _canary
|
|
409
|
+
local _nonce _tag_rc
|
|
288
410
|
saved_stream="${PSA_STREAM-}"; saved_allow="${PSA_ALLOWLIST-}"
|
|
289
411
|
saved_stream_set=""; saved_allow_set=""
|
|
290
412
|
[ "${PSA_STREAM+x}" = "x" ] && saved_stream_set=1
|
|
291
413
|
[ "${PSA_ALLOWLIST+x}" = "x" ] && saved_allow_set=1
|
|
292
414
|
tmpd=$(mktemp -d 2>/dev/null) || { echo " ❌ INSTRUMENT DEAD — mktemp unavailable." >&2; return 1; }
|
|
415
|
+
# 🟥 P1 (cross-family 3라운드): 카나리아가 **고정 문자열**이면, 입력을 읽지도 않고
|
|
416
|
+
# 그 문자열을 rc=0 으로 뱉는 위조 생산자가 자가검사를 통과한다. 세 라운드가 공유한
|
|
417
|
+
# 얼굴 —「상태(rc=0)를 의미(생산이 유효하다)로 확장」— 의 마지막 잔재다.
|
|
418
|
+
# 실행마다 새 nonce 를 만들면 **하드코딩된 위조는 그 값을 알 수 없다.**
|
|
419
|
+
_nonce=$(head -c 12 /dev/urandom 2>/dev/null | od -An -tx1 2>/dev/null | tr -d ' \n')
|
|
420
|
+
# 🟥 **신뢰 경계 — 여기서 라운드를 멈추는 이유(2026-08-21, 6라운드 후 판정).**
|
|
421
|
+
# 이 자가검사가 막는 것: 생산자가 **깨졌거나**(awk 부재·crash·출력 훼손), **PATH 로
|
|
422
|
+
# 가려졌거나**(외부 바이너리 shadowing), 입력을 **안 읽고** 고정/유도값을 뱉는 경우.
|
|
423
|
+
# 이 자가검사가 **못** 막는 것: 이 라이브러리를 **소싱한 셸 안에서 함수를 정의할 수 있는**
|
|
424
|
+
# 상대. 그 상대는 `psa_scan_tagged(){ return 0; }` 한 줄이면 끝이라 nonce·cmp·심링크를
|
|
425
|
+
# 아무리 조여도 소용이 없다 — **이미 신뢰 안쪽**이다. 6라운드 반례 둘이 정확히 그 부류였고
|
|
426
|
+
# (`cmp(){ return 0; }` · `ln -sf` 로 tagged 바꿔치기), 값싼 두 수리(`command cmp` ·
|
|
427
|
+
# 심링크 거부)는 했지만 **그 축을 닫았다고 주장하지 않는다.**
|
|
428
|
+
# 🟥 R5-1: **fallback 을 없앴다.** 5라운드 실측 —
|
|
429
|
+
# `${tmpd##*/}` 파생 → 생산자가 경로를 인자로 받으므로 유도 가능 (4라운드에서 교체)
|
|
430
|
+
# `${RANDOM}${RANDOM}$$${SECONDS}` → **생산자가 같은 셸에서 그대로 재계산해 통과**(zsh 실측 rc=0),
|
|
431
|
+
# `RANDOM`/`SECONDS` 가 unset 이면 사실상 `$$` 하나라 `ps` 로도 읽힌다(bash 실측 rc=0)
|
|
432
|
+
# ⇒ 「예측 불가한 값」은 **커널 엔트로피에서만** 나온다. 못 얻으면 그건 약한 nonce 가 아니라
|
|
433
|
+
# **미측정**이다. 두 번째 출처(openssl)까지 시도하고, 그것도 없으면 fail-closed.
|
|
434
|
+
[ -n "$_nonce" ] || _nonce=$(openssl rand -hex 12 2>/dev/null | tr -cd 'A-Za-z0-9')
|
|
435
|
+
if [ -z "$_nonce" ]; then
|
|
436
|
+
echo " ❌ INSTRUMENT DEAD — no kernel entropy source (/dev/urandom, openssl); cannot mint an unguessable nonce." >&2
|
|
437
|
+
echo " A guessable nonce is not a weaker probe, it is NO probe — a producer can replay it." >&2
|
|
438
|
+
rm -rf "$tmpd" 2>/dev/null || :
|
|
439
|
+
return 1
|
|
440
|
+
fi
|
|
293
441
|
# Round 4 refuted the round-3 fix: `|| :` does NOT rescue an assignment to a readonly variable —
|
|
294
442
|
# bash aborts the shell before the `||` is ever considered. So the only safe move is to REFUSE
|
|
295
443
|
# before mutating anything. Detected, reported as DEAD, and the caller survives.
|
|
296
|
-
_canary=$(printf 'HIGH\
|
|
444
|
+
_canary=$(printf 'HIGH\tPSA_SELFTEST_CANARY_%s' "$_nonce")
|
|
297
445
|
if ! _psa_can_assign PSA_STREAM "$_canary" || ! _psa_can_assign PSA_ALLOWLIST /dev/null; then
|
|
298
446
|
echo " ❌ INSTRUMENT DEAD — PSA_STREAM/PSA_ALLOWLIST cannot take the values the self-test needs." >&2
|
|
299
447
|
echo " (readonly, or an attribute such as \`declare -i\` that rejects the value)" >&2
|
|
@@ -303,9 +451,47 @@ psa_require_live() {
|
|
|
303
451
|
fi
|
|
304
452
|
PSA_STREAM="$_canary"
|
|
305
453
|
PSA_ALLOWLIST=/dev/null
|
|
306
|
-
printf '
|
|
307
|
-
#
|
|
308
|
-
|
|
454
|
+
printf 'PSA_SELFTEST_CANARY_%s\n' "$_nonce" > "$tmpd/canary" 2>/dev/null
|
|
455
|
+
# 🟥 생산자와 매처를 **분리해서** 상태를 각각 본다 (cross-family 반례 4, 2026-08-21).
|
|
456
|
+
# 초판은 `awk … | psa_scan_tagged` 한 파이프의 최종 rc 만 봤는데, 그러면
|
|
457
|
+
# **«생산자 실패의 rc=1»** 과 **«스캐너가 유출을 찾음의 rc=1»** 이 같은 값으로 합쳐진다.
|
|
458
|
+
# 실측 위조: 실패하는 `awk` 가 카나리아 행을 인쇄하고 `return 1` 하게 만들면
|
|
459
|
+
# 양 셸에서 `psa_require_live` 가 **rc=0(살아있음)** 을 냈다. 즉 자가검사 판정 자체가
|
|
460
|
+
# 위조 가능했다. 태깅 결과를 먼저 **물질화**하고 그 rc 를 따로 확인한다.
|
|
461
|
+
# 🟥 P3: `x=$(cmd); rc=$?` 는 `set -e` 아래서 **셸을 즉시 죽인다** — 아래 복원문과
|
|
462
|
+
# `rm -rf` 에 도달하지 못하고 PSA_STREAM 이 **카나리아인 채로 남는다**(실측, bash).
|
|
463
|
+
# 조건문 안의 대입은 errexit 면제다.
|
|
464
|
+
# 🟥 R4-3: `$(...)` 는 **후행 개행을 지운다** — 정답 줄 뒤에 빈 줄을 아무리 붙여도
|
|
465
|
+
# 문자열 비교가 통과했다(실측 rc=0). 「정확한 문자열 대조」라고 적어놓고 정확하지 않았다.
|
|
466
|
+
# 생산자 출력을 **파일로 물질화**해서 `cmp` 로 바이트 비교한다. stderr 는 따로 받는다.
|
|
467
|
+
if awk -v P="psa/selftest" '{print P "\t" $0}' "$tmpd/canary" > "$tmpd/tagged" 2>"$tmpd/err"; then _tag_rc=0; else _tag_rc=$?; fi
|
|
468
|
+
# 🟥 P1: 상태(rc)만이 아니라 **내용**을 대조한다. 생산자가 «입력을 읽어서 태깅했는가» 는
|
|
469
|
+
# rc 로 알 수 없고, nonce 를 포함한 정확한 문자열 일치로만 알 수 있다.
|
|
470
|
+
# 🟥 R5-2: 초판은 기대값도 `$tmpd/expect` 파일에 뒀는데, **생산자는 `$tmpd/canary` 를 인자로
|
|
471
|
+
# 받으므로 `$tmpd` 를 알고 expect 를 덮어쓸 수 있다** — 틀린 바이트를 양쪽에 써넣으면
|
|
472
|
+
# `cmp` 가 통과했다(양 셸 실측 rc=0). 기대값은 **프로세스 안에만** 두고 파이프로 먹인다.
|
|
473
|
+
# (`cmp file -` 는 stdin 을 두 번째 피연산자로 읽는다. cmp 부재는 rc≠0 → fail-closed, 실측.)
|
|
474
|
+
# 🟥 R6-1: `cmp` 를 셸이 해석하면 **함수로 가려진다** — `cmp(){ return 0; }` 하나로 대조가
|
|
475
|
+
# 통째로 무력화됐다(양 셸 실측 rc=0). `command` 는 함수를 건너뛴다.
|
|
476
|
+
# 🟥 R6-2: 생산자는 `$tmpd` 를 알므로 stdout 에는 틀린 바이트를 쓰고 `tagged` 를 **정답 파일로
|
|
477
|
+
# 가는 심링크로 바꿔치기**할 수 있다(양 셸 실측 rc=0). 비교 대상이 «awk 가 실제로 쓴 파일»
|
|
478
|
+
# 인지 먼저 확인한다 — 심링크가 아닌 보통 파일이어야 한다.
|
|
479
|
+
if [ -L "$tmpd/tagged" ] || [ ! -f "$tmpd/tagged" ]; then
|
|
480
|
+
rm -rf "$tmpd" 2>/dev/null || :
|
|
481
|
+
if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
|
|
482
|
+
if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
|
|
483
|
+
echo " ❌ INSTRUMENT DEAD — the self-test output was replaced (symlink or non-regular file)." >&2
|
|
484
|
+
return 1
|
|
485
|
+
fi
|
|
486
|
+
if [ "$_tag_rc" -ne 0 ] || ! printf 'psa/selftest\tPSA_SELFTEST_CANARY_%s\n' "$_nonce" | command cmp -s "$tmpd/tagged" -; then
|
|
487
|
+
rm -rf "$tmpd" 2>/dev/null || :
|
|
488
|
+
if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
|
|
489
|
+
if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
|
|
490
|
+
echo " ❌ INSTRUMENT DEAD — the self-test PRODUCER did not tag this run's nonce (rc=$_tag_rc)." >&2
|
|
491
|
+
echo " A canary printed by a failing — or input-ignoring — producer is not evidence the matcher ran." >&2
|
|
492
|
+
return 1
|
|
493
|
+
fi
|
|
494
|
+
if out=$(psa_scan_tagged < "$tmpd/tagged" 2>&1); then
|
|
309
495
|
rc=0
|
|
310
496
|
else
|
|
311
497
|
rc=$?
|
|
@@ -317,7 +503,7 @@ psa_require_live() {
|
|
|
317
503
|
if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
|
|
318
504
|
if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
|
|
319
505
|
case "$out" in
|
|
320
|
-
*
|
|
506
|
+
*"PSA_SELFTEST_CANARY_$_nonce"*) [ "$rc" -eq 1 ] && return 0 ;;
|
|
321
507
|
esac
|
|
322
508
|
echo " ❌ INSTRUMENT DEAD — the scanner did not report a synthetic known-positive." >&2
|
|
323
509
|
echo " self-test rc=$rc out=[$out]" >&2
|