@chrono-meta/fh-gate 1.4.99 → 2.0.1
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/capabilities/adapters/gstack-content-safety.cap +110 -0
- package/.claude/capabilities/adapters/mate-agent-boundary.cap +110 -0
- package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +116 -0
- package/.claude/capabilities/degrade-direction-scan.cap +24 -0
- package/.claude/capabilities/public-surface-scan.cap +24 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +9 -3
- package/CHEATSHEET.md +17 -1
- package/CLAUDE.md +2 -2
- package/README.ja.md +55 -20
- package/README.ko.md +86 -20
- package/README.md +57 -20
- package/README.zh.md +49 -19
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +96 -5
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +36 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +1 -1
- package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +214 -0
- package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +29 -10
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +73 -0
- package/package.json +13 -3
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/CHANGELOG.md +45 -0
- package/scripts/adapters/gstack_content_safety.sh +186 -0
- package/scripts/adapters/mate_agent_boundary.sh +120 -0
- package/scripts/adapters/peer_resolve.sh +99 -0
- package/scripts/adapters/qasp_new_code_anchor.sh +159 -0
- package/scripts/branch_claim.sh +14 -0
- package/scripts/capability_effect_probe.sh +701 -0
- package/scripts/capability_registry_check.sh +313 -16
- package/scripts/cluster_capability_scan.sh +629 -0
- package/scripts/fh_session_load.sh +70 -0
- package/scripts/portability_lint.sh +257 -0
- package/scripts/publish_freshness_check.sh +204 -0
- package/scripts/relay_channel.sh +70 -7
- package/scripts/selfcheck.sh +166 -1
- package/scripts/test_adapter_lanes.sh +296 -0
- package/scripts/test_capability_entrypoint_shipping.sh +83 -6
- package/scripts/test_relay_channel_lanes.sh +76 -0
- package/templates/.git-hooks/pre-commit +14 -2
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
# 돌리면 판정하지만 돌리도록 강제하지는 못한다(relay_channel.sh 의 같은 잔여와 동형).
|
|
21
21
|
# · **M3(모델 독립성)은 선언 검사다.** reps 로 재지 않는다 — `judge:` 축 선언과, mechanical
|
|
22
22
|
# 선언인데 entry 가 LLM CLI 를 부르는 명백한 모순만 잡는다. M4 를 reps=2 로 돌려 부분 방어.
|
|
23
|
-
# ·
|
|
23
|
+
# · ✅ **`writes:` 축은 2026-08-16 부터 관측된다(M6).** 아래 문단은 그 전의 상태이고,
|
|
24
|
+
# 구조 처방으로 적어둔 것이 실제로 지어졌다 — `capability_effect_probe.sh` 를 M6 에서
|
|
25
|
+
# 부른다. 사고 형태(`rm -rf scripts` + `read-only` 선언)는 이제 **REJECTED** 로 막힌다
|
|
26
|
+
# (self-test 레인으로 고정). 남은 한계는 그 프로브 헤더에 적혀 있다:
|
|
27
|
+
# 한 번의 실행만 본다 · 조건부 쓰기는 M4 쌍에 없으면 안 걸린다 · 네트워크는 안 본다.
|
|
28
|
+
#
|
|
29
|
+
# · (이력) 🟥 **`writes:` 축은 검증 불가였다 — 그리고 그게 이 파일에서 실제로 터졌다.**
|
|
24
30
|
# 2026-08-11, 이 검사기를 통과한 capability(`writes: read-only` 선언)의 진입점이
|
|
25
31
|
# 정리 트랩 결함으로 **레포의 `scripts/` 를 rm -rf 했다.** M1(실행 가능)·M2(닫힌 enum)·
|
|
26
32
|
# M3(mechanical)·M4(known-pair 통과)·M5(cwd) 를 **전부 통과한 채로** 그랬다.
|
|
@@ -28,14 +34,15 @@
|
|
|
28
34
|
# 를 보지 않는다. read-only 선언의 진위는 여기서 닫히지 않는다.
|
|
29
35
|
# 부분 처방(오늘 적용): 진입점 쪽 트랩 규율(정리 대상 변수 재대입 금지 + 임시경로 검문).
|
|
30
36
|
# 구조 처방(미구축): 샌드박스/읽기전용 마운트에서 M4 를 돌려 쓰기 시도를 관측하는 것.
|
|
31
|
-
#
|
|
37
|
+
# ~~그 전까지 `writes: read-only` 는 등록자 주장이지 이 검사기의 판정이 아니다.~~
|
|
38
|
+
# → 2026-08-16 부터 **판정이다**(M6). 단 UNVERIFIABLE 이 나오면 그때는 여전히 주장이다.
|
|
32
39
|
#
|
|
33
40
|
# 사용법
|
|
34
41
|
# capability_registry_check.sh <capfile> [<capfile> ...]
|
|
35
42
|
# capability_registry_check.sh --self-test
|
|
36
43
|
#
|
|
37
44
|
# exit code
|
|
38
|
-
# 0 REGISTRABLE 전 capfile 이 M1–
|
|
45
|
+
# 0 REGISTRABLE 전 capfile 이 M1–M6 + 추가조항 통과
|
|
39
46
|
# 1 REJECTED 하나 이상 기준 미달 (등록 불가 — 그 표면은 dispatch 로 남는다)
|
|
40
47
|
# 10 HARNESS_ERROR capfile 도달 불가·파손·검사기 자신의 전제 파손
|
|
41
48
|
#
|
|
@@ -44,7 +51,10 @@ set -f # noglob — 선언 파일의 값은 데이터지 파일 패턴이 아
|
|
|
44
51
|
|
|
45
52
|
RC_OK=0; RC_REJECT=1; RC_HARNESS=10
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
# `summary`/`tags` 는 **추천 전용**(2026-08-16, cluster-wizard). 판정에는 안 쓰이고
|
|
55
|
+
# `cluster_capability_scan.sh recommend` 의 어휘 매칭에만 쓰인다. 닫힌 목록에 넣는 이유는
|
|
56
|
+
# 이 목록의 목적이 «오타 축 무음 드롭 방지» 이기 때문이다 — 안 넣으면 정당한 키가 SCHEMA 로 막힌다.
|
|
57
|
+
CLOSED_KEYS="id entry requires_cwd summary tags verdict_channel verdict_enum verdict_stdout_key upstream_argv echoes_upstream approval reversibility residency degrade tier_floor writes judge verdict_binding calibration_positive_args calibration_positive_expect calibration_negative_args calibration_negative_expect"
|
|
48
58
|
|
|
49
59
|
# 「안 돌았다」를 뜻하는 이름들 — 추가조항(§ⓑ.4 B1)이 요구하는 구분항
|
|
50
60
|
DIDNOTRUN_NAMES="DID_NOT_RUN DIDNOTRUN NOT_RUN NO_TARGET SKIPPED UNMEASURED NOT_CONFIGURED HARNESS_ERROR"
|
|
@@ -60,7 +70,10 @@ _parse() {
|
|
|
60
70
|
CAP_id=""; CAP_entry=""; CAP_requires_cwd=""; CAP_verdict_channel=""
|
|
61
71
|
CAP_verdict_enum=""; CAP_verdict_stdout_key=""; CAP_judge=""; CAP_writes=""
|
|
62
72
|
CAP_cal_pos_args=""; CAP_cal_pos_expect=""; CAP_cal_neg_args=""; CAP_cal_neg_expect=""
|
|
63
|
-
UNKNOWN_KEYS=""
|
|
73
|
+
UNKNOWN_KEYS=""; CAP_requires_cwd_was_self=0
|
|
74
|
+
# 「선언했는데 값이 비었다」와 「선언 자체가 없다」는 다른 사실이다 — 관측 범위 블록이
|
|
75
|
+
# 둘을 구분해 적으려면 파서가 그 구분을 보존해야 한다(빈 문자열 하나로는 못 나눈다).
|
|
76
|
+
CAP_cal_pos_args_seen=0; CAP_cal_neg_args_seen=0
|
|
64
77
|
local line key val
|
|
65
78
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
66
79
|
case "$line" in ''|'#'*) continue ;; esac
|
|
@@ -77,9 +90,9 @@ _parse() {
|
|
|
77
90
|
requires_cwd) CAP_requires_cwd="$val" ;; verdict_channel) CAP_verdict_channel="$val" ;;
|
|
78
91
|
verdict_enum) CAP_verdict_enum="$val" ;; verdict_stdout_key) CAP_verdict_stdout_key="$val" ;;
|
|
79
92
|
judge) CAP_judge="$val" ;; writes) CAP_writes="$val" ;;
|
|
80
|
-
calibration_positive_args) CAP_cal_pos_args="$val" ;;
|
|
93
|
+
calibration_positive_args) CAP_cal_pos_args="$val"; CAP_cal_pos_args_seen=1 ;;
|
|
81
94
|
calibration_positive_expect) CAP_cal_pos_expect="$val" ;;
|
|
82
|
-
calibration_negative_args) CAP_cal_neg_args="$val" ;;
|
|
95
|
+
calibration_negative_args) CAP_cal_neg_args="$val"; CAP_cal_neg_args_seen=1 ;;
|
|
83
96
|
calibration_negative_expect) CAP_cal_neg_expect="$val" ;;
|
|
84
97
|
esac
|
|
85
98
|
done < "$1"
|
|
@@ -114,15 +127,88 @@ _run_arm() { # $1=extra args → ARM_RC / ARM_NAME. 파이프로 읽지 않는
|
|
|
114
127
|
ARM_NAME="$(_enum_name_of "$ARM_RC" "$CAP_verdict_enum")"
|
|
115
128
|
}
|
|
116
129
|
|
|
130
|
+
# ── 관측 범위 블록 (2026-08-16) — «판정» 이 아니라 «채널» ────────────────────────
|
|
131
|
+
#
|
|
132
|
+
# 🟥 이 블록은 **exit code 에 아무 영향을 주지 않는다.** `_fail` 을 부르지 않고, 어떤 축도
|
|
133
|
+
# 새로 판정하지 않는다. 순수 정보 추가다 — 이 레포의 규율("기계는 비가역 경계와 채널에만,
|
|
134
|
+
# 판단은 안 짓는다")대로, 여기서 하는 일은 **무엇을 관측했고 무엇을 안 했는지 적는 것**뿐이다.
|
|
135
|
+
#
|
|
136
|
+
# 왜 필요한가 — 2026-08-16 실측 두 건이 같은 얼굴을 하고 있었다:
|
|
137
|
+
# ① **M4 는 enum↔구현 일치를 구조적으로 안 본다.** 같은 stale 선언이 캘리브레이션 쌍에
|
|
138
|
+
# 따라 판정이 갈린다 — 쌍이 우연히 «선언 밖 exit» 을 밟으면 REJECTED, 구 enum 안에서만
|
|
139
|
+
# 갈리면 M1–M6 전부 통과. 즉 M4 는 «자기가 부른 두 번의 실행에서 본 것» 만 판정한다.
|
|
140
|
+
# ② **M6 도 선언된 arm 만 관측한다.** `pmh-dev:merge-noop-check` 는 분기 입력에서 git 객체를
|
|
141
|
+
# 쓰지만(격리 클론 실측: `.git/objects` 2145 → 2146), 선언된 두 arm 이 그 경로를 안 지나므로
|
|
142
|
+
# `writes: read-only` 로 **거짓 선언해도 통과한다**(실측 확인).
|
|
143
|
+
#
|
|
144
|
+
# 둘 다 «판정이 틀렸다» 가 아니라 **«판정의 관측 범위가 출력에 안 적혀 있다»** 이다. 그래서
|
|
145
|
+
# 고치는 방향은 새 판정 축이 아니라 이 블록이다: 읽는 사람이 초록을 «선언 전체가 참이다» 로
|
|
146
|
+
# 읽지 않게, 이 실행이 실제로 밟은 곳을 이름으로 적는다.
|
|
147
|
+
# ★ 「없음」도 **반드시 찍는다.** 침묵은 «관측되지 않은 값이 없다» 와 «계산하지 않았다» 를
|
|
148
|
+
# 구분하지 못한다 — 미측정을 0 으로 렌더하지 않는다([[feedback_not_found_is_not_zero_family]]).
|
|
149
|
+
_observation_scope() {
|
|
150
|
+
printf ' ── 관측 범위 (이 판정이 «무엇을 안 봤는지») ──\n'
|
|
151
|
+
printf ' M4/M6 는 **선언된 캘리브레이션 arm 의 실행에서 본 것만** 판정한다.\n'
|
|
152
|
+
|
|
153
|
+
local _pa _na
|
|
154
|
+
if [ "${CAP_cal_pos_args_seen:-0}" = "1" ]; then
|
|
155
|
+
[ -n "$CAP_cal_pos_args" ] && _pa="$CAP_cal_pos_args" || _pa='<빈 값 — 인자 없이 실행>'
|
|
156
|
+
else _pa='<미선언>'; fi
|
|
157
|
+
if [ "${CAP_cal_neg_args_seen:-0}" = "1" ]; then
|
|
158
|
+
[ -n "$CAP_cal_neg_args" ] && _na="$CAP_cal_neg_args" || _na='<빈 값 — 인자 없이 실행>'
|
|
159
|
+
else _na='<미선언>'; fi
|
|
160
|
+
printf ' 실행된 arm: 양성 «%s» / 음성 «%s»\n' "$_pa" "$_na"
|
|
161
|
+
|
|
162
|
+
if [ "${OBS_STATE:-none}" = "run" ]; then
|
|
163
|
+
printf ' 관측된 exit: 양성 rc=%s(%s)%s / 음성 rc=%s(%s)\n' \
|
|
164
|
+
"$OBS_POS_RC" "${OBS_POS_NAME:-<enum 밖>}" \
|
|
165
|
+
"$( [ "$OBS_POS_RC" = "$OBS_POS_RC2" ] && printf ' ×2회 동일' || printf ' ×2회째 rc=%s(갈림)' "$OBS_POS_RC2" )" \
|
|
166
|
+
"$OBS_NEG_RC" "${OBS_NEG_NAME:-<enum 밖>}"
|
|
167
|
+
else
|
|
168
|
+
printf ' 관측된 exit: 없음 — arm 을 실행하지 않았다(%s)\n' "${OBS_WHY:-사유 미기록}"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# 미관측 enum = 선언 enum − 이 실행에서 실제로 나온 rc. **계산해서** 적는다.
|
|
172
|
+
if [ -z "$CAP_verdict_enum" ]; then
|
|
173
|
+
printf ' ⚠️ 선언된 enum 이 없어 「관측되지 않은 값」을 계산할 수 없다(미측정이지 0 이 아니다 — M2 가 이미 실패로 기록했다)\n'
|
|
174
|
+
else
|
|
175
|
+
local _pair _code _unobs="" _seenrc=""
|
|
176
|
+
[ "${OBS_STATE:-none}" = "run" ] && _seenrc=" $OBS_POS_RC $OBS_POS_RC2 $OBS_NEG_RC "
|
|
177
|
+
for _pair in $CAP_verdict_enum; do
|
|
178
|
+
_code="${_pair%%=*}"
|
|
179
|
+
case "$_seenrc" in *" $_code "*) ;; *) _unobs="$_unobs $_pair" ;; esac
|
|
180
|
+
done
|
|
181
|
+
if [ -n "$_unobs" ]; then
|
|
182
|
+
printf ' ⚠️ 선언된 enum 중 이 실행에서 **관측되지 않은** 값:%s\n' "$_unobs"
|
|
183
|
+
else
|
|
184
|
+
printf ' · 선언된 enum 중 이 실행에서 관측되지 않은 값: 없음 (선언된 전 값이 이 실행에서 관측됐다)\n'
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
printf ' ⚠️ 이 실행이 지나지 않은 코드 경로의 부작용(M6 의 `writes:` 축 포함)은 관측 대상이 아니다.\n'
|
|
188
|
+
}
|
|
189
|
+
|
|
117
190
|
_check_one() {
|
|
118
191
|
local f="$1"
|
|
119
192
|
[ -r "$f" ] || _die "capfile 도달 불가: $f"
|
|
120
193
|
_parse "$f"
|
|
121
194
|
FILE_FAILED=0
|
|
195
|
+
# 관측 원장 — 「안 돌았다」가 디폴트다. 실행한 arm 만 이 값들을 채운다.
|
|
196
|
+
OBS_STATE=none; OBS_WHY=""; OBS_POS_RC=""; OBS_POS_RC2=""; OBS_NEG_RC=""
|
|
197
|
+
OBS_POS_NAME=""; OBS_NEG_NAME=""
|
|
122
198
|
printf '\n── %s (%s)\n' "${CAP_id:-<id 미선언>}" "$f"
|
|
123
199
|
|
|
124
200
|
[ -n "$UNKNOWN_KEYS" ] && _fail "SCHEMA" "닫힌 키 목록 밖:$UNKNOWN_KEYS (오타 축 무음드롭 방지 — 무시하지 않는다)"
|
|
125
201
|
|
|
202
|
+
# ── `requires_cwd: SELF` 를 **M1 보다 먼저** 푼다 (2026-08-16 순서 수리) ────
|
|
203
|
+
# 초판은 M5 에서 풀었는데, M1 이 그보다 먼저 돌면서 **상대 entry 를 현재 cwd 기준으로** 찾았다.
|
|
204
|
+
# FH 로컬 등록부 시절엔 entry 가 늘 절대경로라 안 터졌다 — 하네스 자기 선언(상대 entry +
|
|
205
|
+
# SELF)이 그 잠재 가정을 처음 실행에 노출시킨 것이다([[feedback_wiring_surfaces_hidden_failures]]).
|
|
206
|
+
# 해석 실패는 여기서 조용히 넘기고 M5 가 판정한다(한 결함을 두 축에서 두 번 세지 않는다).
|
|
207
|
+
if [ "$CAP_requires_cwd" = "SELF" ]; then
|
|
208
|
+
local _sr; _sr="$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null)"
|
|
209
|
+
[ -n "$_sr" ] && [ -d "$_sr" ] && CAP_requires_cwd="$_sr" && CAP_requires_cwd_was_self=1
|
|
210
|
+
fi
|
|
211
|
+
|
|
126
212
|
# ── M1 실행 가능한 진입점 ──────────────────────────────────────────────────
|
|
127
213
|
local first second
|
|
128
214
|
first="$(printf '%s' "$CAP_entry" | awk '{print $1}')"
|
|
@@ -132,8 +218,15 @@ _check_one() {
|
|
|
132
218
|
_fail "M1" "entry 가 argv 가 아니라 셸 문자열이다(파이프/리다이렉트/치환 포함) — 인젝션 표면" ;;
|
|
133
219
|
'') _fail "M1" "entry 미선언" ;;
|
|
134
220
|
*)
|
|
221
|
+
# 상대 경로는 **선언된 cwd 기준**으로도 본다. 선언은 자기 레포를 기준으로 쓰는 것이
|
|
222
|
+
# 자연스럽고(그래야 이식된다), 검사기가 자기 cwd 로만 보면 정당한 선언을 못 읽는다.
|
|
223
|
+
local _second_abs="$second"
|
|
224
|
+
case "$second" in
|
|
225
|
+
/*) ;;
|
|
226
|
+
*) [ -n "$CAP_requires_cwd" ] && [ -r "$CAP_requires_cwd/$second" ] && _second_abs="$CAP_requires_cwd/$second" ;;
|
|
227
|
+
esac
|
|
135
228
|
if [ -x "$first" ] 2>/dev/null; then _ok "M1" "실행 가능: $first"
|
|
136
|
-
elif command -v "$first" >/dev/null 2>&1 && [ -r "$
|
|
229
|
+
elif command -v "$first" >/dev/null 2>&1 && [ -r "$_second_abs" ]; then
|
|
137
230
|
_ok "M1" "선언된 인터프리터($first) + 읽을 수 있는 스크립트: $second"
|
|
138
231
|
else _fail "M1" "entry 를 셸이 모델 없이 실행할 수 없다: $CAP_entry"; fi ;;
|
|
139
232
|
esac
|
|
@@ -175,25 +268,64 @@ _check_one() {
|
|
|
175
268
|
|
|
176
269
|
# ── M5 선언된 cwd (M4 를 그 자리에서 돌리므로 먼저) ────────────────────────
|
|
177
270
|
case "$CAP_requires_cwd" in
|
|
178
|
-
/*)
|
|
179
|
-
|
|
271
|
+
/*)
|
|
272
|
+
# `SELF` 는 위(M1 앞)에서 이미 레포 루트로 풀렸다. 그 사실을 판정문에 남긴다 —
|
|
273
|
+
# 안 남기면 독자가 선언 원문(`SELF`)과 검사 결과(절대경로)를 연결하지 못한다.
|
|
274
|
+
if [ -d "$CAP_requires_cwd" ]; then
|
|
275
|
+
if [ "${CAP_requires_cwd_was_self:-0}" = "1" ]; then
|
|
276
|
+
_ok "M5" "requires_cwd: SELF → 선언을 품은 레포 루트: $CAP_requires_cwd"
|
|
277
|
+
else
|
|
278
|
+
_ok "M5" "requires_cwd 실재: $CAP_requires_cwd"
|
|
279
|
+
fi
|
|
280
|
+
else
|
|
281
|
+
_fail "M5" "requires_cwd 가 절대경로지만 존재하지 않는다: $CAP_requires_cwd"
|
|
282
|
+
fi ;;
|
|
283
|
+
SELF)
|
|
284
|
+
# ★`SELF` = «이 선언을 품은 레포» (2026-08-16 신설, 정체성 ①-(a) cluster-wizard).
|
|
285
|
+
#
|
|
286
|
+
# 왜 필요했나: 이 스키마는 원래 **FH 로컬 등록부**(gitignored)를 위한 것이라 절대경로가
|
|
287
|
+
# 문제없었다. 그런데 (c) 를 닫으려면 선언이 **그 하네스 자신의 레포에 tracked 로** 살아야
|
|
288
|
+
# 하고, 그 순간 절대경로는 두 가지로 깨진다:
|
|
289
|
+
# ① 이식 불가 — 다른 머신·다른 체크아웃에서 그 경로는 없다
|
|
290
|
+
# ② **공개표면 위반** — 운영자 홈 경로가 tracked 파일에 실린다
|
|
291
|
+
# 그래서 «자기 자신» 을 가리키는 이식 가능한 표현이 필요했다.
|
|
292
|
+
#
|
|
293
|
+
# 해석은 **위치 가정이 아니라 git 레포 루트**다. `../../..` 같은 상대 오프셋으로 풀면
|
|
294
|
+
# `.claude/capabilities/` 배치를 하드코딩하게 되고, 배치가 바뀌는 순간 조용히 엉뚱한
|
|
295
|
+
# 디렉토리를 가리킨다. 레포 루트는 그 파일이 어디 놓이든 같은 답을 준다.
|
|
296
|
+
local _self_root
|
|
297
|
+
_self_root="$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null)"
|
|
298
|
+
if [ -n "$_self_root" ] && [ -d "$_self_root" ]; then
|
|
299
|
+
CAP_requires_cwd="$_self_root"
|
|
300
|
+
_ok "M5" "requires_cwd: SELF → 선언을 품은 레포 루트로 해석: $_self_root"
|
|
301
|
+
else
|
|
302
|
+
# 🟥 fail-closed. git 레포 밖의 선언은 «자기» 가 무엇인지 정의되지 않는다 —
|
|
303
|
+
# 추측해서 cwd 를 정하면 M4 가 **엉뚱한 트리에서** 진입점을 돌린다.
|
|
304
|
+
_fail "M5" "requires_cwd: SELF 인데 이 선언이 git 레포 안에 없다 — «자기» 를 해석할 수 없다"
|
|
305
|
+
fi ;;
|
|
180
306
|
'') _fail "M5" "requires_cwd 미선언 — 콕핏이 어디서 부를지 알 수 없다" ;;
|
|
181
|
-
*) _fail "M5" "requires_cwd 가
|
|
307
|
+
*) _fail "M5" "requires_cwd 가 절대경로도 SELF 도 아니다: $CAP_requires_cwd" ;;
|
|
182
308
|
esac
|
|
183
309
|
|
|
184
310
|
# ── M4 캘리브레이션 쌍 — 선언 + **실행** ──────────────────────────────────
|
|
185
311
|
if [ -z "$CAP_cal_pos_expect" ] || [ -z "$CAP_cal_neg_expect" ]; then
|
|
186
312
|
_fail "M4" "캘리브레이션 쌍 미선언(양성·음성 expect 둘 다 필요) — 답을 아는 케이스를 못 가르는 계기는 재는 게 아니다"
|
|
313
|
+
OBS_WHY="캘리브레이션 쌍 미선언"
|
|
187
314
|
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M4:-}" ]; then
|
|
188
315
|
printf ' ⏭ M4 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
|
|
316
|
+
OBS_WHY="앞선 축 실패로 M4 생략"
|
|
189
317
|
elif ! _validate_arm_args "$CAP_cal_pos_args" || ! _validate_arm_args "$CAP_cal_neg_args"; then
|
|
190
318
|
printf ' ⏭ M4 — args 검문 실패로 arm 을 실행하지 않았다(SKIPPED, PASS 아님)\n'
|
|
319
|
+
OBS_WHY="args 검문 실패"
|
|
191
320
|
else
|
|
192
|
-
local pos_name neg_name pos_rc neg_rc pos_name2
|
|
321
|
+
local pos_name neg_name pos_rc neg_rc pos_name2 pos_rc2
|
|
193
322
|
_run_arm "$CAP_cal_pos_args"; pos_rc="$ARM_RC"; pos_name="$ARM_NAME"
|
|
194
323
|
[ "$pos_rc" = "127" ] && _fail "M4" "requires_cwd 로 진입 실패 — arm 을 돌릴 수 없다"
|
|
195
|
-
_run_arm "$CAP_cal_pos_args"; pos_name2="$ARM_NAME"
|
|
324
|
+
_run_arm "$CAP_cal_pos_args"; pos_name2="$ARM_NAME"; pos_rc2="$ARM_RC" # reps=2 (M3 부분 방어)
|
|
196
325
|
_run_arm "$CAP_cal_neg_args"; neg_rc="$ARM_RC"; neg_name="$ARM_NAME"
|
|
326
|
+
# 관측 원장에 남긴다 — 판정과 무관하게, 이 실행이 **실제로 밟은 exit** 만 적는다.
|
|
327
|
+
OBS_STATE=run; OBS_POS_RC="$pos_rc"; OBS_POS_RC2="$pos_rc2"; OBS_NEG_RC="$neg_rc"
|
|
328
|
+
OBS_POS_NAME="$pos_name"; OBS_NEG_NAME="$neg_name"
|
|
197
329
|
|
|
198
330
|
if [ -z "$pos_name" ]; then
|
|
199
331
|
_fail "M4" "양성 arm 의 exit $pos_rc 가 선언된 enum 밖 — enum 밖 값은 HARNESS_ERROR 이지 PASS 가 아니다"
|
|
@@ -212,9 +344,71 @@ _check_one() {
|
|
|
212
344
|
fi
|
|
213
345
|
fi
|
|
214
346
|
|
|
215
|
-
# ──
|
|
216
|
-
|
|
217
|
-
|
|
347
|
+
# ── M6 선언 진위 — `writes:` 를 **관측**한다 (2026-08-16) ──────────────────
|
|
348
|
+
#
|
|
349
|
+
# 이 파일 헤더가 «구조 처방(미구축): 샌드박스/읽기전용 마운트에서 M4 를 돌려 쓰기 시도를
|
|
350
|
+
# 관측하는 것» 이라고 적어놓은 자리다. `capability_effect_probe.sh` 가 그 처방이고,
|
|
351
|
+
# 여기서 **부른다** — 도구가 존재하는 것과 게이트가 그것을 부르는 것은 다르다.
|
|
352
|
+
#
|
|
353
|
+
# ★UNVERIFIABLE 은 **PASS 가 아니다.** 등록을 막지는 않되(과차단이 우회를 훈련시킨다)
|
|
354
|
+
# «못 쟀다» 를 출력에 남긴다. 미측정을 0 으로 렌더하지 않는다.
|
|
355
|
+
if [ -z "$CAP_writes" ]; then
|
|
356
|
+
: # writes 미선언은 M2 가 이미 처리한다
|
|
357
|
+
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M6:-}" ]; then
|
|
358
|
+
# ★앞선 축이 실패했으면 **진입점을 실행하지 않는다**(2026-08-16 cross-family 지목).
|
|
359
|
+
# M6 는 `eval` 로 entry 를 돌리므로, «등록 거부될 capfile» 의 진입점을 굳이 실행하는
|
|
360
|
+
# 것은 그 자체가 위험 노출이다. M4 가 이미 같은 가드를 갖고 있었고 M6 만 없었다.
|
|
361
|
+
printf ' ⏭ M6 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
|
|
362
|
+
else
|
|
363
|
+
# SYMLINK-RESOLVED, not `${0%/*}` (fixed 2026-08-16, reproduced on both sides).
|
|
364
|
+
# `${0%/*}` yields the directory of the NAME USED TO INVOKE, so a symlink to this script made
|
|
365
|
+
# the probe lookup land next to the LINK — where nothing lives — and M6 then fail-closed with
|
|
366
|
+
# "probe 부재" about a probe that exists. A gate that blames a missing file for its own path bug
|
|
367
|
+
# is worse than one that simply fails: it sends the reader to the wrong repair.
|
|
368
|
+
# `BASH_SOURCE[0]` is the real file even when invoked through a link; the loop then walks any
|
|
369
|
+
# remaining links in the path. `readlink -f` is deliberately not used — it is GNU-only and this
|
|
370
|
+
# ships to macOS (BSD `readlink` has no `-f`).
|
|
371
|
+
# ★반복 상한. 보안 패스 [B] 가 «순환 심링크에서 영원히 돈다» 로 지목했고, 캡을 넣은 뒤
|
|
372
|
+
# **실제 순환 쌍으로 재보니 그 시나리오는 도달 불가였다**: `a.sh → b.sh → a.sh` 는 OS 가
|
|
373
|
+
# ELOOP 로 먼저 막아 bash 가 rc=126 으로 죽는다 — 이 루프는 시작조차 안 한다.
|
|
374
|
+
# 지적은 **루프 본문을 격리해서** 돌린 결과였다(부품 테스트지 도달성 테스트가 아니다).
|
|
375
|
+
# 그래도 캡은 남긴다: 값이 싸고, `$0` 가 파일이 아닌 경로로 들어오는 미래 호출 형태까지
|
|
376
|
+
# 내가 열거하지 못하기 때문이다. **다만 «무한루프를 고쳤다» 고 주장하지 않는다.**
|
|
377
|
+
local _src="${BASH_SOURCE[0]:-$0}" _dir _hops=0
|
|
378
|
+
while [ -L "$_src" ]; do
|
|
379
|
+
_hops=$((_hops+1))
|
|
380
|
+
if [ "$_hops" -gt 40 ]; then
|
|
381
|
+
_fail "M6" "심링크가 40회를 넘어간다 — 순환으로 본다. 프로브 경로를 못 정한다"
|
|
382
|
+
_observation_scope # 이 이른 return 에서도 관측 범위는 찍힌다(조건부로 찍히면 채널이 아니다)
|
|
383
|
+
return 0 # _fail 이 이미 기록했다. 판정 불가를 통과로 렌더하지 않는다
|
|
384
|
+
fi
|
|
385
|
+
_dir=$(cd -P "$(dirname "$_src")" >/dev/null 2>&1 && pwd) || break
|
|
386
|
+
_src=$(readlink "$_src")
|
|
387
|
+
case "$_src" in /*) ;; *) _src="$_dir/$_src" ;; esac
|
|
388
|
+
done
|
|
389
|
+
_dir=$(cd -P "$(dirname "$_src")" >/dev/null 2>&1 && pwd) || _dir="${0%/*}"
|
|
390
|
+
local _probe="$_dir/capability_effect_probe.sh"
|
|
391
|
+
if [ ! -r "$_probe" ]; then
|
|
392
|
+
# 🟥 **fail-CLOSED.** 등록은 «이후 자동 호출» 로 이어지는 비가역 표면이다
|
|
393
|
+
# (CLAUDE.md §Irreversibility Surface-Class Degrade Invariant).
|
|
394
|
+
# 계기가 없으면 «못 쟀다» 이지 «괜찮다» 가 아니고, 여기서 통과시키면 이 축이
|
|
395
|
+
# 존재하기 전과 같은 상태가 된다.
|
|
396
|
+
_fail "M6" "effect probe 부재($_probe) — 선언 진위를 **못 쟀다**. 비가역 표면이므로 fail-closed"
|
|
397
|
+
else
|
|
398
|
+
local _pout _prc
|
|
399
|
+
_pout=$(bash "$_probe" "$f" 2>&1); _prc=$?
|
|
400
|
+
case "$_prc" in
|
|
401
|
+
0) _ok "M6" "선언 진위 관측 통과: writes=$CAP_writes (양 arm 샌드박스 실행에서 위반 없음)" ;;
|
|
402
|
+
1) _fail "M6" "선언 진위 **위반**: $(printf '%s' "$_pout" | head -1)" ;;
|
|
403
|
+
3) _fail "M6" "관측 불가(UNVERIFIABLE) — 등록은 이후 자동 호출로 이어지므로 **미측정을 통과로 렌더하지 않는다**: $(printf '%s' "$_pout" | head -1)" ;;
|
|
404
|
+
*) _fail "M6" "프로브 하네스 오류 rc=$_prc — 판정 아님이므로 fail-closed" ;;
|
|
405
|
+
esac
|
|
406
|
+
fi
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
# ★무조건 찍는다 — PASS 든 REJECTED 든, arm 을 돌렸든 안 돌렸든. 관측 범위가 조건부로
|
|
410
|
+
# 찍히면 그건 채널이 아니라 또 하나의 판정이 된다.
|
|
411
|
+
_observation_scope
|
|
218
412
|
}
|
|
219
413
|
|
|
220
414
|
# ── self-test (known-pair: 통과해야 할 선언 1 · 막혀야 할 선언 6) ─────────────
|
|
@@ -259,6 +453,109 @@ EOF
|
|
|
259
453
|
_t "M1: entry 가 셸 문자열" 1 "$T/shellstr.cap"
|
|
260
454
|
sed 's|^requires_cwd: .*|requires_cwd: relative/path|' "$T/good.cap" > "$T/relcwd.cap"
|
|
261
455
|
_t "M5: requires_cwd 상대경로" 1 "$T/relcwd.cap"
|
|
456
|
+
|
|
457
|
+
# ── M5 `SELF` (2026-08-16) — 하네스가 **자기 레포에 tracked 로** 선언할 수 있게 하는 값 ──
|
|
458
|
+
# known-pair 를 **양쪽 다** 세운다. 양성만 세우면 「SELF 를 그냥 통과시킨다」와
|
|
459
|
+
# 「SELF 를 옳게 해석한다」가 구분되지 않는다.
|
|
460
|
+
local SR="$T/selfrepo"
|
|
461
|
+
mkdir -p "$SR/.claude/capabilities"
|
|
462
|
+
( cd "$SR" && git init -q . && git config user.email t@t && git config user.name t \
|
|
463
|
+
&& echo x > seed.txt && git add seed.txt && git commit -qm init ) >/dev/null 2>&1
|
|
464
|
+
cp "$T/probe.sh" "$SR/probe.sh" 2>/dev/null || true
|
|
465
|
+
sed -e 's|^requires_cwd: .*|requires_cwd: SELF|' \
|
|
466
|
+
-e "s|^entry: .*|entry: bash $SR/probe.sh|" "$T/good.cap" > "$SR/.claude/capabilities/self.cap"
|
|
467
|
+
_t "M5: SELF 는 레포 루트로 해석된다" 0 "$SR/.claude/capabilities/self.cap"
|
|
468
|
+
|
|
469
|
+
# known-negative(=fail-closed 방향): git 레포 **밖**의 SELF 는 해석 불가여야 한다.
|
|
470
|
+
# 이 레인이 없으면 「SELF 면 무조건 통과」가 되고, 그건 M5 를 없앤 것과 같다.
|
|
471
|
+
sed -e 's|^requires_cwd: .*|requires_cwd: SELF|' "$T/good.cap" > "$T/self_outside.cap"
|
|
472
|
+
_t "M5: git 밖의 SELF 는 fail-closed" 1 "$T/self_outside.cap"
|
|
473
|
+
|
|
474
|
+
# ── M6: 선언 진위 (2026-08-16) — 2026-08-11 실사고 형태를 그대로 재현한다 ──
|
|
475
|
+
# `writes: read-only` 를 선언하고 진입점이 `rm -rf scripts` 를 한다. 그날 이 검사기는
|
|
476
|
+
# M1–M5 를 전부 통과시켰다. 이제 M6 가 막는다.
|
|
477
|
+
# ★픽스처가 **M1~M5 를 통과해야** 이 레인이 M6 를 잰다(2026-08-16 cross-family 지목).
|
|
478
|
+
# 초판은 `bash -c 'rm -rf scripts'` 였는데 그건 M1 에서 이미 걸린다(`-c` 는 읽을 수 있는
|
|
479
|
+
# 스크립트가 아니다) — 즉 M6 가 없어도 REJECTED 라 **자기충족**이었다.
|
|
480
|
+
cat > "$T/liar_entry.sh" <<'LIARSH'
|
|
481
|
+
#!/usr/bin/env bash
|
|
482
|
+
# 2026-08-11 실사고 형태: read-only 를 선언하고 실제로는 파괴한다.
|
|
483
|
+
case "${1:-}" in
|
|
484
|
+
--known-negative) rm -rf scripts; exit 0 ;;
|
|
485
|
+
--known-positive) rm -rf scripts; exit 1 ;;
|
|
486
|
+
*) exit 0 ;;
|
|
487
|
+
esac
|
|
488
|
+
LIARSH
|
|
489
|
+
chmod +x "$T/liar_entry.sh"
|
|
490
|
+
cat > "$T/liar.cap" <<LIAR
|
|
491
|
+
id: test:liar-rmrf
|
|
492
|
+
entry: bash $T/liar_entry.sh
|
|
493
|
+
requires_cwd: $T
|
|
494
|
+
verdict_channel: exit
|
|
495
|
+
verdict_enum: 0=CLEAN 1=LEAK 3=DID_NOT_RUN
|
|
496
|
+
approval: auto
|
|
497
|
+
reversibility: reversible
|
|
498
|
+
residency: public
|
|
499
|
+
degrade: fail-closed
|
|
500
|
+
tier_floor: none
|
|
501
|
+
writes: read-only
|
|
502
|
+
judge: mechanical
|
|
503
|
+
verdict_binding: LEAK
|
|
504
|
+
calibration_positive_args: --known-negative
|
|
505
|
+
calibration_positive_expect: CLEAN
|
|
506
|
+
calibration_negative_args: --known-positive
|
|
507
|
+
calibration_negative_expect: LEAK
|
|
508
|
+
LIAR
|
|
509
|
+
_t "M6: read-only 선언인데 쓴다(2026-08-11 사고 형태)" 1 "$T/liar.cap"
|
|
510
|
+
# ★rc 만 보면 **어느 축이 막았는지** 모른다 — M6 를 재려면 M6 가 실패했다고 말해야 한다.
|
|
511
|
+
CRC_DEBUG_OUT=$(bash "$0" "$T/liar.cap" 2>&1)
|
|
512
|
+
[ -n "${CRC_DEBUG:-}" ] && printf '%s\n' "$CRC_DEBUG_OUT" >&2
|
|
513
|
+
if printf '%s' "$CRC_DEBUG_OUT" | grep -q '❌ M6'; then
|
|
514
|
+
pass=$((pass+1)); printf ' ✅ %-34s (M6 가 차단자임을 확인)\n' "M6: 차단 귀속"
|
|
515
|
+
else
|
|
516
|
+
fail=$((fail+1)); printf ' ❌ %-34s — rc 는 1 인데 M6 가 막은 게 아니다(다른 축이 먼저 걸렸다)\n' "M6: 차단 귀속"
|
|
517
|
+
fi
|
|
518
|
+
# ── 관측 범위 블록 (2026-08-16) — 「무엇을 안 봤는지」가 출력에 남는가 ───────────
|
|
519
|
+
# 이 레인들은 **판정을 재지 않는다**(rc 는 위 레인들이 이미 고정한다). 재는 것은
|
|
520
|
+
# «채널이 켜져 있나 · 계산이 맞나 · 조건부로 꺼지지 않나» 셋이다.
|
|
521
|
+
_tg() { # $1=이름 $2=기대 rc $3=capfile $4=반드시 포함할 문자열
|
|
522
|
+
local o r hit; o="$(bash "$0" "$3" 2>&1)"; r=$?
|
|
523
|
+
if printf '%s' "$o" | grep -qF "$4"; then hit=1; else hit=0; fi
|
|
524
|
+
if [ "$r" = "$2" ] && [ "$hit" = "1" ]; then
|
|
525
|
+
pass=$((pass+1)); printf ' ✅ %-34s rc=%s\n' "$1" "$r"
|
|
526
|
+
else
|
|
527
|
+
fail=$((fail+1))
|
|
528
|
+
printf ' ❌ %-34s rc=%s (기대 %s) · 부분문자열 «%s» %s\n%s\n' \
|
|
529
|
+
"$1" "$r" "$2" "$4" "$( [ "$hit" = 1 ] && printf 있음 || printf 없음 )" "$o"
|
|
530
|
+
fi
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
# ⓐ 부분 관측: 선언 enum 4값 · 이 실행이 밟는 건 2값 → **나머지 2값을 이름으로** 적어야 한다.
|
|
534
|
+
# (`good.cap` 의 probe 는 ok→0 / 그 외→1 만 낸다. 3·7 은 이 실행이 갈 수 없는 값이다.)
|
|
535
|
+
sed 's/^verdict_enum: .*/verdict_enum: 0=PASS 1=FAIL 3=DID_NOT_RUN 7=NO_TARGET/' \
|
|
536
|
+
"$T/good.cap" > "$T/obs_partial.cap"
|
|
537
|
+
_tg "관측범위: 미관측 enum 을 이름으로" 0 "$T/obs_partial.cap" \
|
|
538
|
+
"관측되지 않은** 값: 3=DID_NOT_RUN 7=NO_TARGET"
|
|
539
|
+
|
|
540
|
+
# ⓑ 🟥**컨트롤** — 전부 관측되면 「없음」을 찍어야 한다. 이게 없으면 「늘 경고하는 계기」와
|
|
541
|
+
# 「실제로 계산하는 계기」가 구분되지 않는다(경고만 하는 계기는 판별력이 0 이다).
|
|
542
|
+
# 선언 전 값이 관측되려면 음성 arm 이 「안 돌았다」 코드로 떨어져야 한다(M2+ 가 그 값을
|
|
543
|
+
# 요구하므로) — 그래서 전용 probe 를 쓴다.
|
|
544
|
+
printf '#!/bin/sh\n[ "$1" = ok ] && exit 0\nexit 3\n' > "$T/probe_allobs.sh"
|
|
545
|
+
sed -e 's/^verdict_enum: .*/verdict_enum: 0=PASS 3=DID_NOT_RUN/' \
|
|
546
|
+
-e "s|^entry: .*|entry: bash $T/probe_allobs.sh|" \
|
|
547
|
+
-e 's/^calibration_negative_expect: .*/calibration_negative_expect: DID_NOT_RUN/' \
|
|
548
|
+
"$T/good.cap" > "$T/obs_all.cap"
|
|
549
|
+
_tg "관측범위: 전부 관측되면 「없음」" 0 "$T/obs_all.cap" \
|
|
550
|
+
"관측되지 않은 값: 없음"
|
|
551
|
+
|
|
552
|
+
# ⓒ REJECTED 에서도 블록이 찍힌다 + arm 을 안 돌렸다는 사실과 사유가 적힌다.
|
|
553
|
+
# (`nocal.cap` = 캘리브레이션 키 전체 제거 → args 는 「미선언」, 관측 exit 은 「없음」.)
|
|
554
|
+
_tg "관측범위: REJECTED 에서도 찍힌다" 1 "$T/nocal.cap" \
|
|
555
|
+
"관측된 exit: 없음 — arm 을 실행하지 않았다(캘리브레이션 쌍 미선언)"
|
|
556
|
+
_tg "관측범위: 미선언 args 를 미선언이라 적는다" 1 "$T/nocal.cap" \
|
|
557
|
+
"실행된 arm: 양성 «<미선언>» / 음성 «<미선언>»"
|
|
558
|
+
|
|
262
559
|
printf '\n 통과 %d · 실패 %d\n' "$pass" "$fail"
|
|
263
560
|
rm -rf "$T"
|
|
264
561
|
[ "$fail" -eq 0 ] || return 1
|