@chrono-meta/fh-gate 1.4.98 → 2.0.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-plugin/marketplace.json +2 -2
- package/README.ja.md +219 -42
- package/README.ko.md +231 -45
- package/README.md +157 -31
- package/README.zh.md +209 -40
- package/docs/OUTPUT_EVIDENCE.md +21 -12
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +36 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +21 -0
- package/package.json +4 -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 +129 -0
- package/scripts/capability_effect_probe.sh +381 -0
- package/scripts/capability_registry_check.sh +115 -6
- package/scripts/consent_registry_check.sh +124 -1
- package/scripts/fh_node_check.sh +69 -1
- package/scripts/lane_runner_check.sh +70 -14
- package/scripts/publish_freshness_check.sh +204 -0
- package/scripts/selfcheck.sh +101 -1
- package/scripts/test_capability_entrypoint_shipping.sh +75 -5
- package/scripts/test_consent_registry.sh +99 -0
- package/scripts/test_lane_runner_lanes.sh +74 -0
- package/scripts/test_node_check_lanes.sh +217 -0
- package/scripts/test_selfcheck_state_lanes.sh +61 -0
- package/templates/consent_classes.yaml.example +30 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# capability_effect_probe.sh — 선언된 `writes:` 가 **참인지** 관측한다 (정체성 ① 블로커 (d)).
|
|
3
|
+
#
|
|
4
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
# 왜 지어졌나 — 등록 바가 «형식» 만 재고 «진위» 를 안 잰다
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
# `capability_registry_check.sh` 헤더가 직접 적어놨다:
|
|
8
|
+
#
|
|
9
|
+
# "🟥 `writes:` 축은 검증 불가 — 그리고 그게 이 파일에서 실제로 터졌다.
|
|
10
|
+
# 2026-08-11, 이 검사기를 통과한 capability(`writes: read-only` 선언)의 진입점이
|
|
11
|
+
# 정리 트랩 결함으로 레포의 `scripts/` 를 rm -rf 했다. M1–M5 를 전부 통과한 채로.
|
|
12
|
+
# 구조 처방(미구축): 샌드박스에서 M4 를 돌려 쓰기 시도를 관측하는 것."
|
|
13
|
+
#
|
|
14
|
+
# 이 파일이 그 구조 처방이다. `ship_readiness_gate.md` 정체성 ① 의 블로커 (d) —
|
|
15
|
+
# *"초록이 파괴 행위에 선행할 수 있는 검사기는 초록 정체성이 아니다"* — 를 겨눈다.
|
|
16
|
+
#
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
# 이 프로브가 **증명하지 않는** 것 (과잉주장 금지)
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
# · **한 번의 실행만 본다.** 조건부로 쓰는 capability(특정 입력에서만 쓴다)는 그 입력이
|
|
21
|
+
# M4 쌍에 없으면 안 걸린다. 이건 «이 실행에서 안 썼다» 이지 «절대 안 쓴다» 가 아니다.
|
|
22
|
+
# · **샌드박스 밖은 못 본다.** 절대경로로 다른 데를 쓰면 이 프로브는 놓친다.
|
|
23
|
+
# ⟹ 그래서 **카나리아를 샌드박스 밖에도 심는다**(아래 OUTSIDE_CANARY). 그게 지워지면
|
|
24
|
+
# «샌드박스 밖 쓰기» 로 잡힌다 — 완전하진 않지만 조용하진 않다.
|
|
25
|
+
# · **네트워크·프로세스 부작용은 안 본다.** 파일시스템 축 전용이다.
|
|
26
|
+
# · 🟥 **사본은 `HEAD` 기준이다**(`git archive HEAD`). 후보 파일이 워킹트리에만 있고 커밋 전이면,
|
|
27
|
+
# 프로브가 재는 트리는 **후보 상태가 아니다.** 등록 시점에 커밋된 상태를 재는 것이 정상
|
|
28
|
+
# 경로이나, 개발 중 호출하면 이 어긋남이 조용히 생긴다 — 그래서 여기 적는다.
|
|
29
|
+
# · 🟥 **사본에 `.git` 이 없다 — 이건 의도이고 대가가 있다.** 2026-08-16 이전에는 `git worktree`
|
|
30
|
+
# 를 썼는데, 워크트리는 실물 레포의 `.git` 을 **공유**하므로 `read-only` 를 선언한 진입점이
|
|
31
|
+
# `git config --local core.pager` 로 **그 레포에 영속 코드실행을 심고도 VERIFIED** 를 받았다
|
|
32
|
+
# (보안 패스 [S], 손으로 재현). 지금은 `.git` 이 아예 없다. 대가: **git 을 실제로 쓰는
|
|
33
|
+
# capability 는 여기서 못 돈다 → UNVERIFIABLE.** 미측정을 통과로 렌더하는 것보다 낫다.
|
|
34
|
+
# · 🟥 **`.git` 외의 메타데이터 채널은 여전히 안 본다.** 이 수리는 «프로브가 스스로 열어 준
|
|
35
|
+
# 통로» 하나를 닫은 것이지 «부작용 채널 전부» 를 닫은 게 아니다.
|
|
36
|
+
# · 🟥 **절대경로 감시는 «홈 엔트리 목록» 수준이다.** 홈 **하위 디렉토리 내부**의 변경
|
|
37
|
+
# (예: `$HOME/Documents/x`)이나 `/tmp` 의 남의 파일 수정은 여전히 못 본다.
|
|
38
|
+
# 전수 스냅샷은 비용이 크고 홈이 늘 시끄러워 오탐이 난다 — 그래서 **엔트리 목록**만 본다.
|
|
39
|
+
# 이건 «가장 흔한 표적을 막는다» 이지 «절대경로 쓰기를 막는다» 가 아니다.
|
|
40
|
+
# 2026-08-16 자기 공격으로 이 층을 찾았고, 그 위 층은 여전히 열려 있다고 적어 둔다.
|
|
41
|
+
# · 실행 불가는 **PASS 가 아니다** — `UNVERIFIABLE`(3) 로 낸다. 미측정을 0 으로 렌더하지 않는다.
|
|
42
|
+
#
|
|
43
|
+
# 사용법
|
|
44
|
+
# capability_effect_probe.sh <capfile> [<capfile> ...]
|
|
45
|
+
# capability_effect_probe.sh --self-test
|
|
46
|
+
#
|
|
47
|
+
# exit code
|
|
48
|
+
# 0 VERIFIED 선언과 관측이 일치
|
|
49
|
+
# 1 VIOLATION 선언보다 **넓게** 썼다 (read-only 선언인데 썼다 등)
|
|
50
|
+
# 3 UNVERIFIABLE 돌릴 수 없었다 — 판정 아님(미측정)
|
|
51
|
+
# 10 HARNESS_ERROR capfile 파손·전제 파손
|
|
52
|
+
set -o pipefail
|
|
53
|
+
set -f
|
|
54
|
+
|
|
55
|
+
RC_OK=0; RC_VIOLATION=1; RC_UNVERIFIABLE=3; RC_HARNESS=10
|
|
56
|
+
|
|
57
|
+
# writes 축 순서 (좁음 → 넓음). 관측이 선언보다 넓으면 VIOLATION.
|
|
58
|
+
ORDER_writes="read-only write-local write-remote"
|
|
59
|
+
|
|
60
|
+
_idx() { local v="$1" i=0 x; for x in $ORDER_writes; do [ "$x" = "$v" ] && { printf '%d' "$i"; return; }; i=$((i+1)); done; printf '%d' -1; }
|
|
61
|
+
|
|
62
|
+
_key() { # $1=capfile $2=key
|
|
63
|
+
sed -n "s/^[[:space:]]*$2:[[:space:]]*//p" "$1" 2>/dev/null | head -1 \
|
|
64
|
+
| sed 's/[[:space:]]*$//'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_snapshot() { # $1=dir → "path<TAB>sha" 목록 (정렬)
|
|
68
|
+
find "$1" -type f 2>/dev/null | LC_ALL=C sort | while IFS= read -r f; do
|
|
69
|
+
printf '%s\t%s\n' "$f" "$(shasum -a 256 "$f" 2>/dev/null | cut -d' ' -f1)"
|
|
70
|
+
done
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# ★정리 트랩 — 신호/중단에도 temp 가 안 남게 한다(cross-family 지목).
|
|
74
|
+
# 2026-08-16 이전에는 여기에 `git worktree remove` + `worktree prune` 분기가 있었다.
|
|
75
|
+
# 샌드박스가 워크트리가 아니게 되면서(§격리) **둘 다 지웠다**. `prune` 은 특히 지워야 했다 —
|
|
76
|
+
# 그건 **그 레포의 모든** stale 워크트리 등록을 청소하므로, 병렬 세션이 워크트리 규율을 쓰는
|
|
77
|
+
# 레포에서 **남의 등록을 건드릴 수 있었다**(보안 패스 [B]).
|
|
78
|
+
_PROBE_WT_PARENT=""; _PROBE_SANDBOX=""
|
|
79
|
+
_cleanup_probe() {
|
|
80
|
+
[ -n "$_PROBE_SANDBOX" ] && rm -rf "$_PROBE_SANDBOX"
|
|
81
|
+
[ -n "$_PROBE_WT_PARENT" ] && rm -rf "$_PROBE_WT_PARENT"
|
|
82
|
+
_PROBE_WT_PARENT=""; _PROBE_SANDBOX=""
|
|
83
|
+
}
|
|
84
|
+
trap _cleanup_probe EXIT INT TERM
|
|
85
|
+
|
|
86
|
+
probe_one() { # $1=capfile → rc
|
|
87
|
+
local cap="$1"
|
|
88
|
+
[ -r "$cap" ] || { printf '❌ capfile 도달 불가: %s\n' "$cap" >&2; return "$RC_HARNESS"; }
|
|
89
|
+
|
|
90
|
+
local declared entry pos_args neg_args
|
|
91
|
+
declared=$(_key "$cap" writes)
|
|
92
|
+
entry=$(_key "$cap" entry)
|
|
93
|
+
pos_args=$(_key "$cap" calibration_positive_args)
|
|
94
|
+
neg_args=$(_key "$cap" calibration_negative_args)
|
|
95
|
+
|
|
96
|
+
[ -n "$declared" ] || { printf '❌ %s: writes 축 선언 없음 — 검증 대상이 아니다\n' "$cap" >&2; return "$RC_HARNESS"; }
|
|
97
|
+
[ "$(_idx "$declared")" -ge 0 ] || { printf '❌ %s: writes 값이 enum 밖: %s\n' "$cap" "$declared" >&2; return "$RC_HARNESS"; }
|
|
98
|
+
[ -n "$entry" ] || { printf '❌ %s: entry 없음\n' "$cap" >&2; return "$RC_HARNESS"; }
|
|
99
|
+
|
|
100
|
+
# `requires_cwd` 가 있으면 그 레포의 **`.git` 없는 파일 사본**을 샌드박스로 쓴다.
|
|
101
|
+
# 실물 트리에서 돌리면 «파괴를 관측하려다 파괴하는» 꼴이고, 빈 임시디렉토리에서
|
|
102
|
+
# 돌리면 진입점이 전제를 못 찾아 UNVERIFIABLE 로만 끝난다(실측: 실물 capfile 10 중 4).
|
|
103
|
+
#
|
|
104
|
+
# 🟥 **리셋 먼저, 편집 나중.** 사본을 만든 뒤에 픽스처를 얹는다. 순서를 뒤집으면
|
|
105
|
+
# 사본 생성이 tracked 편집을 되돌려 프로브가 조용히 공허해진다
|
|
106
|
+
# (2026-08-16 병렬 세션 실측: 같은 순서 문제로 한 레인이 두 번 장식이 됐다).
|
|
107
|
+
local req_cwd wt=""
|
|
108
|
+
req_cwd=$(_key "$cap" requires_cwd)
|
|
109
|
+
|
|
110
|
+
local sandbox outside wt_parent=""
|
|
111
|
+
if [ -n "$req_cwd" ] && [ -d "$req_cwd/.git" ]; then
|
|
112
|
+
# 🟥 **`git worktree` 를 쓰지 않는다 — 워크트리는 정의상 실물 레포의 `.git` 을 공유한다.**
|
|
113
|
+
# 2026-08-16 보안 패스가 지목하고 거버너가 손으로 재현했다: `writes: read-only` 를 선언한
|
|
114
|
+
# 진입점이 워크트리 안에서 `git config --local core.pager "sh -c <cmd>"` 를 심으면
|
|
115
|
+
# · 그 config 는 **실물 레포**에 쓰인다(공유 `.git`)
|
|
116
|
+
# · `_snapshot` 은 샌드박스 **디렉토리 트리**만 해싱하므로 `.git/` 은 구조적으로 안 보인다
|
|
117
|
+
# · `$outside` 카나리아도 `$HOME` 감시도 이 채널을 못 본다
|
|
118
|
+
# → 프로브는 `✅ VERIFIED … 관측=no-write` 를 찍고, 그 레포의 다음 `git log` 가 공격자
|
|
119
|
+
# 명령을 실행한다. **M6 가 «위반 없음» 이라고 적는 바로 그 실행이 위반을 저지른다.**
|
|
120
|
+
# 이건 헤더의 명명된 잔여(«절대경로로 다른 데를 쓰면 놓친다»)와 **다르다**: 그건 capability 가
|
|
121
|
+
# 고른 경로이고, 이건 **프로브가 스스로 열어 준 통로**다.
|
|
122
|
+
#
|
|
123
|
+
# 처방: `git archive` 로 **`.git` 이 없는 순수 파일 사본**을 만든다. 진입점은 여전히 전제
|
|
124
|
+
# (레포의 파일 배치)를 찾고, 공유 메타데이터 채널은 존재하지 않는다.
|
|
125
|
+
# ⚠️ 대가를 명시한다: 사본에 `.git` 이 없으므로 **git 명령을 실제로 쓰는 capability 는
|
|
126
|
+
# 여기서 못 돈다** → UNVERIFIABLE 로 떨어진다. 미측정을 통과로 렌더하는 것보다 낫다.
|
|
127
|
+
wt_parent=$(mktemp -d) || return "$RC_HARNESS"
|
|
128
|
+
_PROBE_WT_PARENT="$wt_parent" # ★등록을 mktemp **직후**에 한다 — 아래 실패 경로에서
|
|
129
|
+
wt="$wt_parent/copy"; mkdir -p "$wt" # return 하면 트랩이 이걸 못 지우던 누수가 있었다
|
|
130
|
+
if ! ( cd "$req_cwd" && git archive --format=tar HEAD ) 2>/dev/null | ( cd "$wt" && tar xf - ) 2>/dev/null; then
|
|
131
|
+
printf '⚠️ UNVERIFIABLE %s — `.git` 없는 사본을 못 만들었다(%s)\n' "$cap" "$req_cwd" >&2
|
|
132
|
+
return "$RC_UNVERIFIABLE"
|
|
133
|
+
fi
|
|
134
|
+
if [ -e "$wt/.git" ]; then
|
|
135
|
+
printf '❌ %s: 사본에 .git 이 있다 — 격리 전제 파손. 판정하지 않는다\n' "$cap" >&2
|
|
136
|
+
return "$RC_HARNESS"
|
|
137
|
+
fi
|
|
138
|
+
sandbox="$wt"
|
|
139
|
+
_PROBE_SANDBOX="$wt"
|
|
140
|
+
else
|
|
141
|
+
sandbox=$(mktemp -d) || return "$RC_HARNESS"
|
|
142
|
+
_PROBE_SANDBOX="$sandbox"
|
|
143
|
+
fi
|
|
144
|
+
outside=$(mktemp -d) || return "$RC_HARNESS"
|
|
145
|
+
printf 'canary-inside\n' > "$sandbox/CANARY_INSIDE"
|
|
146
|
+
printf 'canary-outside\n' > "$outside/CANARY_OUTSIDE"
|
|
147
|
+
|
|
148
|
+
# 🟥 **홈 디렉토리 카나리아** — 2026-08-16 실측: 초판은 임시 카나리아 하나만 봐서
|
|
149
|
+
# `echo x > $HOME/...` 이 **VERIFIED 로 통과**했고 홈에 파일이 실제로 생겼다.
|
|
150
|
+
# (d) 가 말하는 결함을 프로브 자신이 갖고 있었던 것이다. 절대경로 쓰기의 가장 흔한
|
|
151
|
+
# 표적이 홈이므로 여기에도 감시를 둔다. ⚠️ 완전하지 않다 — 아래 §잔여 참조.
|
|
152
|
+
local home_before home_after
|
|
153
|
+
home_before=$(ls -A "$HOME" 2>/dev/null | LC_ALL=C sort | shasum -a 256 | cut -d" " -f1)
|
|
154
|
+
mkdir -p "$sandbox/scripts"; printf 'x\n' > "$sandbox/scripts/CANARY_SCRIPTS"
|
|
155
|
+
|
|
156
|
+
local before after out rc
|
|
157
|
+
before=$(_snapshot "$sandbox")
|
|
158
|
+
local out_before; out_before=$(_snapshot "$outside")
|
|
159
|
+
|
|
160
|
+
# 실행 — **양 arm 을 같은 샌드박스 정책으로** 돌린다(2026-08-16 cross-family 지목).
|
|
161
|
+
# 초판은 positive 만 돌렸는데, 등록 검사기 주석은 «M4 를 관측한다» 고 말했다 —
|
|
162
|
+
# 구현과 주장이 어긋났고, **negative arm 에서만 쓰는 capability 가 통과**했다.
|
|
163
|
+
# 실패는 VIOLATION 이 아니다 — 못 잰 것이다.
|
|
164
|
+
#
|
|
165
|
+
# 🟥 **args 를 검문한다** (2026-08-16 보안 패스 [A], 거버너 재현). `eval "$entry $pos_args"`
|
|
166
|
+
# 는 `entry` 가 M1 을 통과하는 순수 argv 여도 **args 하나로 탈출한다**:
|
|
167
|
+
# `calibration_positive_args: x; touch /tmp/PWNED` 가
|
|
168
|
+
# 샌드박스 밖 `/tmp` 에 쓰고도 `✅ VERIFIED · 관측=no-write` 를 받았다.
|
|
169
|
+
# 등록기(`capability_registry_check.sh`)에는 `_validate_arm_args` 가 이미 있었고
|
|
170
|
+
# **프로브에만 없었다** — 그런데 헤더 usage 가 standalone 호출을 광고하고 files[] 로 단독
|
|
171
|
+
# 출하되므로, 등록기 경유 M4 검문에 의존할 수 없다. 형제가 가진 가드를 이쪽에도 세운다.
|
|
172
|
+
_reject_arm_args() { # $1=args $2=arm 이름 → 0=통과 1=거부
|
|
173
|
+
case "$1" in
|
|
174
|
+
*'|'*|*';'*|*'&'*|*'>'*|*'<'*|*'`'*|*'$('*|*$'\n'*)
|
|
175
|
+
printf '🟥 VIOLATION %s — %s arm args 에 셸 메타문자(인젝션 표면): %s\n' "$cap" "$2" "$1"; return 1 ;;
|
|
176
|
+
*'../'*)
|
|
177
|
+
printf '🟥 VIOLATION %s — %s arm args 가 상위 경로로 탈출한다: %s\n' "$cap" "$2" "$1"; return 1 ;;
|
|
178
|
+
esac
|
|
179
|
+
return 0
|
|
180
|
+
}
|
|
181
|
+
_reject_arm_args "$pos_args" positive || return "$RC_VIOLATION"
|
|
182
|
+
_reject_arm_args "$neg_args" negative || return "$RC_VIOLATION"
|
|
183
|
+
|
|
184
|
+
# ⚠️ `eval` 은 **남긴다**, 그리고 그 이유가 측정으로 갈렸다. 처음엔 지적의 처방대로
|
|
185
|
+
# `set -- $entry $args; "$@"` 로 바꿨는데, 그러면 **셸 형태 진입점의 의미가 깨진다**:
|
|
186
|
+
# `bash -c 'git tag X; …'` 가 따옴표를 잃고 조각나 bash 가 `unexpected EOF` 로 rc=2 를 내고,
|
|
187
|
+
# 아무것도 안 바뀌었으니 프로브가 **`✅ VERIFIED`** 를 찍었다(실측). 즉 처방을 그대로
|
|
188
|
+
# 적용하면 [A] 를 닫으면서 «실행 실패를 무위반으로 렌더» 하는 더 조용한 결함이 생긴다.
|
|
189
|
+
# 이 프로브의 자기 self-test 조차 `entry: bash -c '…'` 를 쓴다.
|
|
190
|
+
# **닫아야 할 것은 args 였다** — 위 `_reject_arm_args` 가 그걸 닫는다. entry 자체는 이
|
|
191
|
+
# 도구가 «실행해서 관측하겠다» 고 선언한 대상이라 실행이 목적이지 결함이 아니다.
|
|
192
|
+
local out2 rc2
|
|
193
|
+
out=$(cd "$sandbox" && eval "$entry $pos_args" 2>&1); rc=$?
|
|
194
|
+
out2=""; rc2=0
|
|
195
|
+
if [ -n "$neg_args" ]; then
|
|
196
|
+
out2=$(cd "$sandbox" && eval "$entry $neg_args" 2>&1); rc2=$?
|
|
197
|
+
out="$out
|
|
198
|
+
$out2"
|
|
199
|
+
[ "$rc2" -gt "$rc" ] && rc="$rc2"
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
after=$(_snapshot "$sandbox")
|
|
203
|
+
local out_after; out_after=$(_snapshot "$outside")
|
|
204
|
+
|
|
205
|
+
local changed outside_changed
|
|
206
|
+
changed=$([ "$before" = "$after" ] && echo 0 || echo 1)
|
|
207
|
+
outside_changed=$([ "$out_before" = "$out_after" ] && echo 0 || echo 1)
|
|
208
|
+
|
|
209
|
+
_cleanup_probe
|
|
210
|
+
rm -rf "$outside"
|
|
211
|
+
|
|
212
|
+
home_after=$(ls -A "$HOME" 2>/dev/null | LC_ALL=C sort | shasum -a 256 | cut -d" " -f1)
|
|
213
|
+
local home_changed
|
|
214
|
+
home_changed=$([ "$home_before" = "$home_after" ] && echo 0 || echo 1)
|
|
215
|
+
|
|
216
|
+
if [ "$home_changed" = "1" ]; then
|
|
217
|
+
printf '🟥 VIOLATION %s — **홈 디렉토리 엔트리가 바뀌었다**(절대경로 쓰기). 선언=%s\n' "$cap" "$declared"
|
|
218
|
+
return "$RC_VIOLATION"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
if [ "$outside_changed" = "1" ]; then
|
|
222
|
+
printf '🟥 VIOLATION %s — **샌드박스 밖**이 바뀌었다(절대경로 쓰기). 선언=%s\n' "$cap" "$declared"
|
|
223
|
+
return "$RC_VIOLATION"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
if [ "$declared" = "read-only" ] && [ "$changed" = "1" ]; then
|
|
227
|
+
printf '🟥 VIOLATION %s — `writes: read-only` 선언인데 샌드박스가 바뀌었다\n' "$cap"
|
|
228
|
+
return "$RC_VIOLATION"
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# 못 돌았으면 «안 썼다» 가 아니다. rc 가 10(하네스 오류)이거나 실행 자체가 실패하고
|
|
232
|
+
# 아무 변화도 없으면 판정 불가로 낸다 — 이게 이 프로브의 fail-closed 방향이다.
|
|
233
|
+
# ★`not a git repository` 가 이 목록에 있는 이유는 **내가 만든 미측정이기 때문**이다.
|
|
234
|
+
# 위에서 격리를 위해 사본에서 `.git` 을 뺐으므로, git 을 실제로 쓰는 진입점은 여기서
|
|
235
|
+
# 조용히 실패한다. 그 실패를 「안 썼다」로 렌더하면 [S] 를 고치면서 같은 얼굴의 결함을
|
|
236
|
+
# 새로 만드는 것이다 — 실측: `core.pager` 를 심으려던 capability 가 수리 직후
|
|
237
|
+
# **VERIFIED** 를 받았다(실물 레포는 깨끗해졌지만 판정이 거짓). 미측정은 미측정으로 낸다.
|
|
238
|
+
if [ "$rc" -ge 10 ] || printf '%s' "$out" | grep -qiE 'command not found|no such file|not a git repository'; then
|
|
239
|
+
printf '⚠️ UNVERIFIABLE %s — 진입점이 이 샌드박스에서 못 돌았다(rc=%s). 미측정이지 통과 아님\n' "$cap" "$rc"
|
|
240
|
+
return "$RC_UNVERIFIABLE"
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
printf '✅ VERIFIED %s — 선언=%s · 관측=%s\n' "$cap" "$declared" \
|
|
244
|
+
"$([ "$changed" = "1" ] && echo "wrote" || echo "no-write")"
|
|
245
|
+
return "$RC_OK"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
self_test() {
|
|
249
|
+
local d rc fails=0 pass=0
|
|
250
|
+
d=$(mktemp -d)
|
|
251
|
+
|
|
252
|
+
# ── known-negative: 진짜 read-only 인 capability ──────────────────────────
|
|
253
|
+
cat > "$d/ro.cap" <<EOF
|
|
254
|
+
id: test:readonly
|
|
255
|
+
entry: bash -c 'ls >/dev/null'
|
|
256
|
+
writes: read-only
|
|
257
|
+
EOF
|
|
258
|
+
probe_one "$d/ro.cap" >/dev/null 2>&1; rc=$?
|
|
259
|
+
if [ "$rc" = "0" ]; then pass=$((pass+1)); echo "✅ L1 진짜 read-only 는 VERIFIED"; else fails=$((fails+1)); echo "❌ L1 rc=$rc (0 기대)"; fi
|
|
260
|
+
|
|
261
|
+
# ── known-positive: read-only 선언인데 쓴다 ───────────────────────────────
|
|
262
|
+
cat > "$d/liar.cap" <<EOF
|
|
263
|
+
id: test:liar
|
|
264
|
+
entry: bash -c 'echo x > LIAR_WROTE_THIS'
|
|
265
|
+
writes: read-only
|
|
266
|
+
EOF
|
|
267
|
+
probe_one "$d/liar.cap" >/dev/null 2>&1; rc=$?
|
|
268
|
+
if [ "$rc" = "1" ]; then pass=$((pass+1)); echo "✅ L2 거짓 read-only 선언은 VIOLATION"; else fails=$((fails+1)); echo "❌ L2 rc=$rc (1 기대)"; fi
|
|
269
|
+
|
|
270
|
+
# ── 2026-08-11 실사고 재현: 진입점이 scripts/ 를 rm -rf 한다 ──────────────
|
|
271
|
+
cat > "$d/rmrf.cap" <<EOF
|
|
272
|
+
id: test:rmrf
|
|
273
|
+
entry: bash -c 'rm -rf scripts'
|
|
274
|
+
writes: read-only
|
|
275
|
+
EOF
|
|
276
|
+
probe_one "$d/rmrf.cap" >/dev/null 2>&1; rc=$?
|
|
277
|
+
if [ "$rc" = "1" ]; then pass=$((pass+1)); echo "✅ L3 rm -rf scripts (실사고 형태) VIOLATION"; else fails=$((fails+1)); echo "❌ L3 rc=$rc (1 기대)"; fi
|
|
278
|
+
|
|
279
|
+
# ── write-local 선언은 써도 된다 (과차단 방지 컨트롤) ─────────────────────
|
|
280
|
+
cat > "$d/wl.cap" <<EOF
|
|
281
|
+
id: test:writelocal
|
|
282
|
+
entry: bash -c 'echo x > OK_TO_WRITE'
|
|
283
|
+
writes: write-local
|
|
284
|
+
EOF
|
|
285
|
+
probe_one "$d/wl.cap" >/dev/null 2>&1; rc=$?
|
|
286
|
+
if [ "$rc" = "0" ]; then pass=$((pass+1)); echo "✅ L4 write-local 선언은 써도 VERIFIED (과차단 아님)"; else fails=$((fails+1)); echo "❌ L4 rc=$rc (0 기대)"; fi
|
|
287
|
+
|
|
288
|
+
# ── 못 도는 진입점은 PASS 가 아니다 ──────────────────────────────────────
|
|
289
|
+
cat > "$d/dead.cap" <<EOF
|
|
290
|
+
id: test:dead
|
|
291
|
+
entry: this_command_does_not_exist_xyz
|
|
292
|
+
writes: read-only
|
|
293
|
+
EOF
|
|
294
|
+
probe_one "$d/dead.cap" >/dev/null 2>&1; rc=$?
|
|
295
|
+
if [ "$rc" = "3" ]; then pass=$((pass+1)); echo "✅ L5 못 도는 진입점은 UNVERIFIABLE (통과 아님)"; else fails=$((fails+1)); echo "❌ L5 rc=$rc (3 기대)"; fi
|
|
296
|
+
|
|
297
|
+
# ── 🟥 절대경로 탈출: $HOME 쓰기 (2026-08-16 자기 공격으로 발견) ──────────
|
|
298
|
+
# 초판은 임시 카나리아 하나만 봐서 이게 **VERIFIED 로 통과**했고 홈에 파일이 실제로
|
|
299
|
+
# 생겼다. 프로브 자신이 (d) 결함을 갖고 있었던 것이다.
|
|
300
|
+
# 🟥 **리셋 먼저, 실행 나중.** 잔재가 있으면 홈 엔트리 해시가 안 바뀌어 이 레인이
|
|
301
|
+
# 조용히 초록이 된다 — 순서 의존이고, cross-family 가 두 번 돌려 잡았다(2026-08-16).
|
|
302
|
+
rm -f "$HOME/.fh_probe_selftest_canary"
|
|
303
|
+
cat > "$d/abs.cap" <<EOF
|
|
304
|
+
id: test:abs-escape
|
|
305
|
+
entry: bash -c 'echo x > \$HOME/.fh_probe_selftest_canary'
|
|
306
|
+
writes: read-only
|
|
307
|
+
EOF
|
|
308
|
+
probe_one "$d/abs.cap" >/dev/null 2>&1; rc=$?
|
|
309
|
+
rm -f "$HOME/.fh_probe_selftest_canary"
|
|
310
|
+
if [ "$rc" = "1" ]; then pass=$((pass+1)); echo "✅ L7 \$HOME 절대경로 쓰기는 VIOLATION"; else fails=$((fails+1)); echo "❌ L7 rc=$rc (1 기대) — 절대경로 탈출이 뚫렸다"; fi
|
|
311
|
+
|
|
312
|
+
# ── writes 선언이 없으면 검증 대상이 아니다 ──────────────────────────────
|
|
313
|
+
cat > "$d/nowrites.cap" <<EOF
|
|
314
|
+
id: test:nowrites
|
|
315
|
+
entry: bash -c 'true'
|
|
316
|
+
EOF
|
|
317
|
+
probe_one "$d/nowrites.cap" >/dev/null 2>&1; rc=$?
|
|
318
|
+
if [ "$rc" = "10" ]; then pass=$((pass+1)); echo "✅ L6 writes 선언 부재는 HARNESS_ERROR"; else fails=$((fails+1)); echo "❌ L6 rc=$rc (10 기대)"; fi
|
|
319
|
+
|
|
320
|
+
# ── L8: args 를 통한 셸 인젝션 (2026-08-16 보안 패스 [A], 손으로 재현) ──────
|
|
321
|
+
# entry 는 M1 을 통과하는 순수 argv 인데 args 하나로 탈출한다. 초판은 이걸
|
|
322
|
+
# `✅ VERIFIED · 관측=no-write` 로 통과시켰고 샌드박스 **밖** `/tmp` 에 파일이 생겼다.
|
|
323
|
+
local _canary="$d/L8_INJECTION_CANARY"
|
|
324
|
+
cat > "$d/inj.cap" <<EOF
|
|
325
|
+
id: test:inject
|
|
326
|
+
entry: /bin/echo hi
|
|
327
|
+
writes: read-only
|
|
328
|
+
calibration_positive_args: x; touch $_canary
|
|
329
|
+
EOF
|
|
330
|
+
probe_one "$d/inj.cap" >/dev/null 2>&1; rc=$?
|
|
331
|
+
# ★rc 만 보지 않는다 — **카나리아가 안 생겼는지**까지 본다. 「막혔다」와 「막혔고
|
|
332
|
+
# 그 사이 아무것도 실행되지 않았다」는 다른 명제이고, 이 레인이 재려는 건 후자다.
|
|
333
|
+
if [ "$rc" = "1" ] && [ ! -e "$_canary" ]; then
|
|
334
|
+
pass=$((pass+1)); echo "✅ L8 args 셸 인젝션은 VIOLATION (카나리아 미생성)"
|
|
335
|
+
else
|
|
336
|
+
fails=$((fails+1)); echo "❌ L8 rc=$rc (1 기대) · 카나리아 존재=$([ -e "$_canary" ] && echo YES || echo no)"
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
# ── L9: git 메타데이터 채널 — 샌드박스가 실물 레포의 `.git` 을 공유하면 안 된다 ──
|
|
340
|
+
# (2026-08-16 보안 패스 [S], 손으로 재현) `writes: read-only` 를 선언한 진입점이
|
|
341
|
+
# `git config --local core.pager` 로 **실물 레포에 영속 코드실행**을 심고도 VERIFIED 를 받았다.
|
|
342
|
+
# 지금은 `.git` 없는 사본이라 git 이 아예 안 돌아 UNVERIFIABLE(3) 이고, 실물은 안 변한다.
|
|
343
|
+
local _tgt="$d/l9target"
|
|
344
|
+
mkdir -p "$_tgt" && ( cd "$_tgt" && git init -q . && git config user.email t@t \
|
|
345
|
+
&& git config user.name t && echo x > f.txt && git add f.txt && git commit -qm init ) >/dev/null 2>&1
|
|
346
|
+
if ! git -C "$_tgt" rev-parse --verify -q HEAD >/dev/null; then
|
|
347
|
+
fails=$((fails+1)); echo "❌ L9 SETUP 실패 — 대상 레포가 안 섰다(레인 무의미)"
|
|
348
|
+
else
|
|
349
|
+
cat > "$d/meta.cap" <<EOF
|
|
350
|
+
id: test:gitmeta
|
|
351
|
+
entry: bash -c 'git config --local core.pager "sh -c pwned"'
|
|
352
|
+
requires_cwd: $_tgt
|
|
353
|
+
writes: read-only
|
|
354
|
+
EOF
|
|
355
|
+
probe_one "$d/meta.cap" >/dev/null 2>&1; rc=$?
|
|
356
|
+
local _planted; _planted=$(git -C "$_tgt" config --local --get core.pager 2>/dev/null)
|
|
357
|
+
# 판정의 본체는 rc 가 아니라 **실물 레포가 안 변했다**는 것이다. rc=0(VERIFIED)이면
|
|
358
|
+
# 「안 썼다」를 거짓 주장한 것이고, _planted 가 있으면 격리 자체가 뚫린 것이다.
|
|
359
|
+
if [ "$rc" = "3" ] && [ -z "$_planted" ]; then
|
|
360
|
+
pass=$((pass+1)); echo "✅ L9 git 메타데이터 채널 차단 (UNVERIFIABLE · 실물 무변경)"
|
|
361
|
+
else
|
|
362
|
+
fails=$((fails+1)); echo "❌ L9 rc=$rc (3 기대) · 실물 core.pager=[$_planted]"
|
|
363
|
+
fi
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
rm -rf "$d"
|
|
367
|
+
echo "── capability_effect_probe lanes: $pass PASS / $fails FAIL ──"
|
|
368
|
+
[ "$fails" = "0" ]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case "${1:-}" in
|
|
372
|
+
--self-test) self_test; exit $?;;
|
|
373
|
+
"") echo "usage: $0 <capfile> [...] | --self-test" >&2; exit "$RC_HARNESS";;
|
|
374
|
+
esac
|
|
375
|
+
|
|
376
|
+
worst=0
|
|
377
|
+
for c in "$@"; do
|
|
378
|
+
probe_one "$c"; rc=$?
|
|
379
|
+
[ "$rc" -gt "$worst" ] && worst=$rc
|
|
380
|
+
done
|
|
381
|
+
exit "$worst"
|
|
@@ -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
|
#
|
|
@@ -212,9 +219,66 @@ _check_one() {
|
|
|
212
219
|
fi
|
|
213
220
|
fi
|
|
214
221
|
|
|
215
|
-
# ──
|
|
216
|
-
|
|
217
|
-
|
|
222
|
+
# ── M6 선언 진위 — `writes:` 를 **관측**한다 (2026-08-16) ──────────────────
|
|
223
|
+
#
|
|
224
|
+
# 이 파일 헤더가 «구조 처방(미구축): 샌드박스/읽기전용 마운트에서 M4 를 돌려 쓰기 시도를
|
|
225
|
+
# 관측하는 것» 이라고 적어놓은 자리다. `capability_effect_probe.sh` 가 그 처방이고,
|
|
226
|
+
# 여기서 **부른다** — 도구가 존재하는 것과 게이트가 그것을 부르는 것은 다르다.
|
|
227
|
+
#
|
|
228
|
+
# ★UNVERIFIABLE 은 **PASS 가 아니다.** 등록을 막지는 않되(과차단이 우회를 훈련시킨다)
|
|
229
|
+
# «못 쟀다» 를 출력에 남긴다. 미측정을 0 으로 렌더하지 않는다.
|
|
230
|
+
if [ -z "$CAP_writes" ]; then
|
|
231
|
+
: # writes 미선언은 M2 가 이미 처리한다
|
|
232
|
+
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M6:-}" ]; then
|
|
233
|
+
# ★앞선 축이 실패했으면 **진입점을 실행하지 않는다**(2026-08-16 cross-family 지목).
|
|
234
|
+
# M6 는 `eval` 로 entry 를 돌리므로, «등록 거부될 capfile» 의 진입점을 굳이 실행하는
|
|
235
|
+
# 것은 그 자체가 위험 노출이다. M4 가 이미 같은 가드를 갖고 있었고 M6 만 없었다.
|
|
236
|
+
printf ' ⏭ M6 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
|
|
237
|
+
else
|
|
238
|
+
# SYMLINK-RESOLVED, not `${0%/*}` (fixed 2026-08-16, reproduced on both sides).
|
|
239
|
+
# `${0%/*}` yields the directory of the NAME USED TO INVOKE, so a symlink to this script made
|
|
240
|
+
# the probe lookup land next to the LINK — where nothing lives — and M6 then fail-closed with
|
|
241
|
+
# "probe 부재" about a probe that exists. A gate that blames a missing file for its own path bug
|
|
242
|
+
# is worse than one that simply fails: it sends the reader to the wrong repair.
|
|
243
|
+
# `BASH_SOURCE[0]` is the real file even when invoked through a link; the loop then walks any
|
|
244
|
+
# remaining links in the path. `readlink -f` is deliberately not used — it is GNU-only and this
|
|
245
|
+
# ships to macOS (BSD `readlink` has no `-f`).
|
|
246
|
+
# ★반복 상한. 보안 패스 [B] 가 «순환 심링크에서 영원히 돈다» 로 지목했고, 캡을 넣은 뒤
|
|
247
|
+
# **실제 순환 쌍으로 재보니 그 시나리오는 도달 불가였다**: `a.sh → b.sh → a.sh` 는 OS 가
|
|
248
|
+
# ELOOP 로 먼저 막아 bash 가 rc=126 으로 죽는다 — 이 루프는 시작조차 안 한다.
|
|
249
|
+
# 지적은 **루프 본문을 격리해서** 돌린 결과였다(부품 테스트지 도달성 테스트가 아니다).
|
|
250
|
+
# 그래도 캡은 남긴다: 값이 싸고, `$0` 가 파일이 아닌 경로로 들어오는 미래 호출 형태까지
|
|
251
|
+
# 내가 열거하지 못하기 때문이다. **다만 «무한루프를 고쳤다» 고 주장하지 않는다.**
|
|
252
|
+
local _src="${BASH_SOURCE[0]:-$0}" _dir _hops=0
|
|
253
|
+
while [ -L "$_src" ]; do
|
|
254
|
+
_hops=$((_hops+1))
|
|
255
|
+
if [ "$_hops" -gt 40 ]; then
|
|
256
|
+
_fail "M6" "심링크가 40회를 넘어간다 — 순환으로 본다. 프로브 경로를 못 정한다"
|
|
257
|
+
return 0 # _fail 이 이미 기록했다. 판정 불가를 통과로 렌더하지 않는다
|
|
258
|
+
fi
|
|
259
|
+
_dir=$(cd -P "$(dirname "$_src")" >/dev/null 2>&1 && pwd) || break
|
|
260
|
+
_src=$(readlink "$_src")
|
|
261
|
+
case "$_src" in /*) ;; *) _src="$_dir/$_src" ;; esac
|
|
262
|
+
done
|
|
263
|
+
_dir=$(cd -P "$(dirname "$_src")" >/dev/null 2>&1 && pwd) || _dir="${0%/*}"
|
|
264
|
+
local _probe="$_dir/capability_effect_probe.sh"
|
|
265
|
+
if [ ! -r "$_probe" ]; then
|
|
266
|
+
# 🟥 **fail-CLOSED.** 등록은 «이후 자동 호출» 로 이어지는 비가역 표면이다
|
|
267
|
+
# (CLAUDE.md §Irreversibility Surface-Class Degrade Invariant).
|
|
268
|
+
# 계기가 없으면 «못 쟀다» 이지 «괜찮다» 가 아니고, 여기서 통과시키면 이 축이
|
|
269
|
+
# 존재하기 전과 같은 상태가 된다.
|
|
270
|
+
_fail "M6" "effect probe 부재($_probe) — 선언 진위를 **못 쟀다**. 비가역 표면이므로 fail-closed"
|
|
271
|
+
else
|
|
272
|
+
local _pout _prc
|
|
273
|
+
_pout=$(bash "$_probe" "$f" 2>&1); _prc=$?
|
|
274
|
+
case "$_prc" in
|
|
275
|
+
0) _ok "M6" "선언 진위 관측 통과: writes=$CAP_writes (양 arm 샌드박스 실행에서 위반 없음)" ;;
|
|
276
|
+
1) _fail "M6" "선언 진위 **위반**: $(printf '%s' "$_pout" | head -1)" ;;
|
|
277
|
+
3) _fail "M6" "관측 불가(UNVERIFIABLE) — 등록은 이후 자동 호출로 이어지므로 **미측정을 통과로 렌더하지 않는다**: $(printf '%s' "$_pout" | head -1)" ;;
|
|
278
|
+
*) _fail "M6" "프로브 하네스 오류 rc=$_prc — 판정 아님이므로 fail-closed" ;;
|
|
279
|
+
esac
|
|
280
|
+
fi
|
|
281
|
+
fi
|
|
218
282
|
}
|
|
219
283
|
|
|
220
284
|
# ── self-test (known-pair: 통과해야 할 선언 1 · 막혀야 할 선언 6) ─────────────
|
|
@@ -259,6 +323,51 @@ EOF
|
|
|
259
323
|
_t "M1: entry 가 셸 문자열" 1 "$T/shellstr.cap"
|
|
260
324
|
sed 's|^requires_cwd: .*|requires_cwd: relative/path|' "$T/good.cap" > "$T/relcwd.cap"
|
|
261
325
|
_t "M5: requires_cwd 상대경로" 1 "$T/relcwd.cap"
|
|
326
|
+
|
|
327
|
+
# ── M6: 선언 진위 (2026-08-16) — 2026-08-11 실사고 형태를 그대로 재현한다 ──
|
|
328
|
+
# `writes: read-only` 를 선언하고 진입점이 `rm -rf scripts` 를 한다. 그날 이 검사기는
|
|
329
|
+
# M1–M5 를 전부 통과시켰다. 이제 M6 가 막는다.
|
|
330
|
+
# ★픽스처가 **M1~M5 를 통과해야** 이 레인이 M6 를 잰다(2026-08-16 cross-family 지목).
|
|
331
|
+
# 초판은 `bash -c 'rm -rf scripts'` 였는데 그건 M1 에서 이미 걸린다(`-c` 는 읽을 수 있는
|
|
332
|
+
# 스크립트가 아니다) — 즉 M6 가 없어도 REJECTED 라 **자기충족**이었다.
|
|
333
|
+
cat > "$T/liar_entry.sh" <<'LIARSH'
|
|
334
|
+
#!/usr/bin/env bash
|
|
335
|
+
# 2026-08-11 실사고 형태: read-only 를 선언하고 실제로는 파괴한다.
|
|
336
|
+
case "${1:-}" in
|
|
337
|
+
--known-negative) rm -rf scripts; exit 0 ;;
|
|
338
|
+
--known-positive) rm -rf scripts; exit 1 ;;
|
|
339
|
+
*) exit 0 ;;
|
|
340
|
+
esac
|
|
341
|
+
LIARSH
|
|
342
|
+
chmod +x "$T/liar_entry.sh"
|
|
343
|
+
cat > "$T/liar.cap" <<LIAR
|
|
344
|
+
id: test:liar-rmrf
|
|
345
|
+
entry: bash $T/liar_entry.sh
|
|
346
|
+
requires_cwd: $T
|
|
347
|
+
verdict_channel: exit
|
|
348
|
+
verdict_enum: 0=CLEAN 1=LEAK 3=DID_NOT_RUN
|
|
349
|
+
approval: auto
|
|
350
|
+
reversibility: reversible
|
|
351
|
+
residency: public
|
|
352
|
+
degrade: fail-closed
|
|
353
|
+
tier_floor: none
|
|
354
|
+
writes: read-only
|
|
355
|
+
judge: mechanical
|
|
356
|
+
verdict_binding: LEAK
|
|
357
|
+
calibration_positive_args: --known-negative
|
|
358
|
+
calibration_positive_expect: CLEAN
|
|
359
|
+
calibration_negative_args: --known-positive
|
|
360
|
+
calibration_negative_expect: LEAK
|
|
361
|
+
LIAR
|
|
362
|
+
_t "M6: read-only 선언인데 쓴다(2026-08-11 사고 형태)" 1 "$T/liar.cap"
|
|
363
|
+
# ★rc 만 보면 **어느 축이 막았는지** 모른다 — M6 를 재려면 M6 가 실패했다고 말해야 한다.
|
|
364
|
+
CRC_DEBUG_OUT=$(bash "$0" "$T/liar.cap" 2>&1)
|
|
365
|
+
[ -n "${CRC_DEBUG:-}" ] && printf '%s\n' "$CRC_DEBUG_OUT" >&2
|
|
366
|
+
if printf '%s' "$CRC_DEBUG_OUT" | grep -q '❌ M6'; then
|
|
367
|
+
pass=$((pass+1)); printf ' ✅ %-34s (M6 가 차단자임을 확인)\n' "M6: 차단 귀속"
|
|
368
|
+
else
|
|
369
|
+
fail=$((fail+1)); printf ' ❌ %-34s — rc 는 1 인데 M6 가 막은 게 아니다(다른 축이 먼저 걸렸다)\n' "M6: 차단 귀속"
|
|
370
|
+
fi
|
|
262
371
|
printf '\n 통과 %d · 실패 %d\n' "$pass" "$fail"
|
|
263
372
|
rm -rf "$T"
|
|
264
373
|
[ "$fail" -eq 0 ] || return 1
|