@chrono-meta/fh-gate 2.0.0 → 2.1.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/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/rules/fh_4axis_gate.md +77 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +25 -3
- package/CHEATSHEET.md +17 -1
- package/CLAUDE.md +241 -5
- 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/capability_composition_contract.md +68 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +122 -5
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +115 -3
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +83 -1
- package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +255 -0
- package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +234 -11
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +128 -0
- package/knowledge/shared/rules/sister_asset_protocol.md +12 -0
- package/package.json +12 -3
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/agents/quench-challenger.md +6 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/CHANGELOG.md +60 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +14 -2
- package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +13 -0
- package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +34 -1
- 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 +340 -20
- package/scripts/capability_registry_check.sh +271 -20
- package/scripts/chamber_run.sh +24 -3
- package/scripts/chamber_witness.sh +25 -0
- package/scripts/cluster_capability_scan.sh +629 -0
- package/scripts/fh-gate.sh +16 -1
- package/scripts/fh_session_load.sh +70 -0
- package/scripts/package_coverage_check.sh +10 -0
- package/scripts/portability_lint.sh +257 -0
- package/scripts/prepublish_scope_note.sh +139 -0
- package/scripts/relay_channel.sh +70 -7
- package/scripts/selfcheck.sh +167 -5
- package/scripts/test_adapter_lanes.sh +296 -0
- package/scripts/test_capability_entrypoint_shipping.sh +8 -1
- package/scripts/test_fh_gate_regressions.sh +21 -9
- package/scripts/test_marker_axes_run_lanes.sh +87 -3
- package/scripts/test_marker_crossfamily_lanes.sh +31 -1
- package/scripts/test_relay_channel_lanes.sh +76 -0
- package/templates/.git-hooks/pre-commit +239 -12
- package/templates/subagent-tally-hook.json +2 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# qasp_new_code_anchor.sh — FH 소유 **어댑터**. peer `qasp` 의
|
|
3
|
+
# `scripts/new_code_anchor_scan.sh` 를 클러스터 호출 가능한 typed capability 로 감싼다.
|
|
4
|
+
#
|
|
5
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
# 무엇을 재나
|
|
7
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
# **새로 추가된 실행 코드에 그것을 보는 앵커(테스트)가 있는가.** peer 헤더의 기원 실측:
|
|
9
|
+
# 앵커 0개인 신규 스크립트가 들어왔는데 CI 는 `success / 123 passed` 였다 — 그 123개 중
|
|
10
|
+
# 그 파일을 보는 게 하나도 없었을 뿐이다. 그 상태로 fail-open 3건이 같이 통과했다.
|
|
11
|
+
# 이 스캔이 닫는 것은 딱 하나: **「안 돌았다」가 「깨끗하다」로 읽히는 것.**
|
|
12
|
+
# 진입점은 **peer 소유 코드**이고 이 어댑터가 만든 것이 아니다.
|
|
13
|
+
#
|
|
14
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
# VERDICT ENUM (선언과 정확히 일치해야 한다)
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
# 0 PASS 앵커 없는 신규 실행 코드 없음
|
|
18
|
+
# 1 VIOLATION 앵커 없는 신규 실행 코드가 있다
|
|
19
|
+
# 2 ARGS 인자 오류 — 판정이 아니다
|
|
20
|
+
# 3 HARNESS_ERROR ref 해석 불가 · git 실패 · 계기 부재 — 판정이 아니다
|
|
21
|
+
# 4 OUT_OF_SCOPE **다른 언어의 신규 실행 코드가 있어 검사되지 않았다** — 판정이 아니다
|
|
22
|
+
# 20 PEER_ABSENT **peer 하네스가 이 머신에 없다**
|
|
23
|
+
#
|
|
24
|
+
# 🟥 값 넷이 「안 쟀다」의 서로 다른 얼굴이다. peer 헤더가 같은 말을 한다 —
|
|
25
|
+
# *"스캔이 못 돌았는데 1 을 내면 «위반» 으로, 0 을 내면 «깨끗» 으로 읽힌다. 둘 다 틀렸다."*
|
|
26
|
+
# 어댑터는 그 위에 한 층을 더 얹는다:
|
|
27
|
+
# PEER_ABSENT = 그 하네스가 이 머신에 없다 (클러스터에 대한 진술)
|
|
28
|
+
# HARNESS_ERROR = 하네스는 있는데 못 쟀다 (계기에 대한 진술)
|
|
29
|
+
# OUT_OF_SCOPE = 계기는 돌았는데 대상이 사각지대다 (범위에 대한 진술)
|
|
30
|
+
# `verdict_binding` 에 PASS 를 뺀 전부가 들어간다.
|
|
31
|
+
#
|
|
32
|
+
# ⚠️ 하위 계기의 exit 을 그대로 전파하지 않는다 — 아래 case 가 명시 재매핑이다.
|
|
33
|
+
# peer 헤더 자신이 그걸 규율로 적어 뒀다: *"같은 숫자가 이 레포 안에서 다른 사건을
|
|
34
|
+
# 뜻한다 — 값을 옮겨 쓰지 마라."* (`judge_rer.sh` 의 4=BLOCKED · `pull_cli.py` 의
|
|
35
|
+
# 4=NO_CORPUS 와 여기의 4=OUT_OF_SCOPE 는 전부 다른 사건이다.)
|
|
36
|
+
#
|
|
37
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
# 캘리브레이션 쌍 — **합성 레포**에서 돈다. 왜 peer 히스토리를 안 쓰나
|
|
39
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
# ① peer 의 이 진입점은 **오늘 수리 중**이다(qasp PR #174, 브랜치
|
|
41
|
+
# `fix/anchor-scan-out-of-scope-repairs`). 고정 SHA 를 쌍으로 박으면 그 브랜치가
|
|
42
|
+
# squash 머지되는 순간 커밋이 orphan 이 되어 쌍이 해석 불가로 죽는다.
|
|
43
|
+
# ② 상대 ref(`HEAD~N`)는 커밋이 쌓이면 가리키는 구간이 조용히 움직인다 — 같은 선언이
|
|
44
|
+
# 다른 것을 재게 된다.
|
|
45
|
+
# ③ peer 는 `residency: company` 다. peer 히스토리를 arm 으로 쓰면 캘리브레이션 출력에
|
|
46
|
+
# 조직 경로명이 실린다. 합성 레포는 그 표면을 아예 만들지 않는다.
|
|
47
|
+
# 대가: 쌍이 재는 것은 «peer 의 히스토리» 가 아니라 **«peer 의 계기가 답 아는 두 케이스를
|
|
48
|
+
# 가르는가»** 다. 등록 바가 M4 에서 묻는 것이 정확히 후자이므로 맞는 표적이다.
|
|
49
|
+
# 양성: 신규 `src/plain_config.py`(앵커 0) → VIOLATION
|
|
50
|
+
# 음성: 같은 파일 + `tests/test_plain_config.py`(앵커) → PASS
|
|
51
|
+
# ★`src/plain_config.py` 라는 이름은 peer 자신의 B1 실측에서 온 **대조군**이다
|
|
52
|
+
# (`latest_config.py` 는 `test_` 부분문자열 때문에 무검사 통과했고, `plain_config.py`
|
|
53
|
+
# 는 정상 차단됐다). 즉 이 양성 arm 은 peer 가 이미 손으로 가른 케이스와 같은 것이다.
|
|
54
|
+
#
|
|
55
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
# CLEANUP-TRAP 규율 (물려받은 실패)
|
|
57
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
# OWNED_TMP(내가 만든 것, 지운다)와 peer 트리는 **다른 변수**다. OWNED_TMP 는 최초 1회만
|
|
59
|
+
# 대입하고 트랩이 임시경로인지 한 번 더 검문한다. peer 트리는 rm 에 절대 안 넘어간다.
|
|
60
|
+
#
|
|
61
|
+
# 사용법
|
|
62
|
+
# qasp_new_code_anchor.sh --range <base-ref> [<head-ref>] # peer 레포에서 실행
|
|
63
|
+
# qasp_new_code_anchor.sh --known-positive # expect VIOLATION
|
|
64
|
+
# qasp_new_code_anchor.sh --known-negative # expect PASS
|
|
65
|
+
set -o pipefail
|
|
66
|
+
|
|
67
|
+
PEER_NAME="qasp"
|
|
68
|
+
RC_PASS=0; RC_VIOLATION=1; RC_ARGS=2; RC_HARNESS=3; RC_OUT_OF_SCOPE=4; RC_PEER_ABSENT=20
|
|
69
|
+
|
|
70
|
+
_harness_error() { printf 'qasp-adapter: HARNESS_ERROR — %s\n' "$1" >&2; exit "$RC_HARNESS"; }
|
|
71
|
+
_args_error() { printf 'qasp-adapter: ARGS — %s\n' "$1" >&2; exit "$RC_ARGS"; }
|
|
72
|
+
_peer_absent() { printf 'qasp-adapter: PEER_ABSENT — %s\n' "$1" >&2; exit "$RC_PEER_ABSENT"; }
|
|
73
|
+
|
|
74
|
+
SELF_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
75
|
+
_LIB="$SELF_DIR/peer_resolve.sh"
|
|
76
|
+
[ -r "$_LIB" ] || _harness_error "peer 해석 라이브러리 부재: $_LIB"
|
|
77
|
+
# shellcheck source=/dev/null
|
|
78
|
+
. "$_LIB"
|
|
79
|
+
|
|
80
|
+
# ── peer 해석 — tracked 파일에 운영자 홈 절대경로를 싣지 않는다 ──────────────
|
|
81
|
+
PEER_ROOT="$(fh_peer_resolve "$PEER_NAME")"; _rrc=$?
|
|
82
|
+
case "$_rrc" in
|
|
83
|
+
0) ;;
|
|
84
|
+
1) _peer_absent "peer «$PEER_NAME» 가 이 머신에 없다 — 안 쟀다(통과가 아니다)" ;;
|
|
85
|
+
2) _harness_error "peer «$PEER_NAME» 해석이 모호하다 — 어느 트리를 쟀는지 말할 수 없으므로 판정하지 않는다" ;;
|
|
86
|
+
*) _harness_error "peer 루트 목록 자체가 없다 — 전제 파손" ;;
|
|
87
|
+
esac
|
|
88
|
+
|
|
89
|
+
ENTRY="$PEER_ROOT/scripts/new_code_anchor_scan.sh"
|
|
90
|
+
[ -r "$ENTRY" ] || _harness_error "peer 진입점 부재: $ENTRY (하네스는 있으나 계기가 없다)"
|
|
91
|
+
command -v git >/dev/null 2>&1 || _harness_error "git 부재 — 이 계기는 git 없이는 못 돈다"
|
|
92
|
+
|
|
93
|
+
OWNED_TMP="" # 캘리브레이션 arm 에서만 만든다. 1회 대입, 재대입 금지.
|
|
94
|
+
_cleanup() {
|
|
95
|
+
[ -n "$OWNED_TMP" ] || return 0
|
|
96
|
+
case "$OWNED_TMP" in
|
|
97
|
+
/tmp/*|/var/folders/*|/private/var/folders/*)
|
|
98
|
+
[ -d "$OWNED_TMP" ] && rm -rf "$OWNED_TMP" ;;
|
|
99
|
+
*) printf 'qasp-adapter: cleanup 거부 — OWNED_TMP 가 임시경로가 아니다: %s\n' "$OWNED_TMP" >&2 ;;
|
|
100
|
+
esac
|
|
101
|
+
}
|
|
102
|
+
trap _cleanup EXIT
|
|
103
|
+
|
|
104
|
+
# 합성 레포를 세운다. $1 = "anchored" 면 앵커 파일을 같이 넣는다.
|
|
105
|
+
# stdout 은 오염시키지 않는다(경로는 전역 변수로 넘긴다).
|
|
106
|
+
FIXTURE_REPO=""; FIXTURE_BASE=""
|
|
107
|
+
_build_fixture_repo() {
|
|
108
|
+
local anchored="${1:-}"
|
|
109
|
+
OWNED_TMP="$(mktemp -d)" || _harness_error "mktemp 실패"
|
|
110
|
+
local R="$OWNED_TMP/fixture"
|
|
111
|
+
mkdir -p "$R/src" "$R/tests" || _harness_error "픽스처 레포를 만들 수 없다"
|
|
112
|
+
# `-c` 로 신원을 준다 — 전역 gitconfig 를 읽지도 쓰지도 않기 위해서다.
|
|
113
|
+
local G=(git -c user.email=fixture@localhost -c user.name=fixture -c commit.gpgsign=false)
|
|
114
|
+
( cd "$R" && "${G[@]}" init -q . ) >/dev/null 2>&1 || _harness_error "픽스처 레포 init 실패"
|
|
115
|
+
printf 'fixture base\n' > "$R/README.md"
|
|
116
|
+
( cd "$R" && "${G[@]}" add -A && "${G[@]}" commit -qm "base" ) >/dev/null 2>&1 \
|
|
117
|
+
|| _harness_error "픽스처 base 커밋 실패"
|
|
118
|
+
FIXTURE_BASE="$( cd "$R" && git rev-parse HEAD 2>/dev/null )"
|
|
119
|
+
[ -n "$FIXTURE_BASE" ] || _harness_error "픽스처 base ref 해석 실패"
|
|
120
|
+
# 신규 실행 코드 1개. peer 의 대상 case(`src/*.py`)에 걸린다.
|
|
121
|
+
printf 'def compute(a, b):\n return a + b\n' > "$R/src/plain_config.py"
|
|
122
|
+
if [ "$anchored" = "anchored" ]; then
|
|
123
|
+
# peer 의 앵커 판정은 «HEAD 시점의 tests/ 가 이 파일의 stem 을 언급하는가» 다.
|
|
124
|
+
printf 'from src.plain_config import compute\n\n\ndef test_compute():\n assert compute(1, 2) == 3\n' \
|
|
125
|
+
> "$R/tests/test_plain_config.py"
|
|
126
|
+
fi
|
|
127
|
+
( cd "$R" && "${G[@]}" add -A && "${G[@]}" commit -qm "add new code" ) >/dev/null 2>&1 \
|
|
128
|
+
|| _harness_error "픽스처 head 커밋 실패"
|
|
129
|
+
FIXTURE_REPO="$R"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
RUN_CWD=""; BASE=""; HEAD_REF=""
|
|
133
|
+
case "${1:-}" in
|
|
134
|
+
--known-positive) _build_fixture_repo ""; RUN_CWD="$FIXTURE_REPO"; BASE="$FIXTURE_BASE"; HEAD_REF="HEAD" ;;
|
|
135
|
+
--known-negative) _build_fixture_repo "anchored"; RUN_CWD="$FIXTURE_REPO"; BASE="$FIXTURE_BASE"; HEAD_REF="HEAD" ;;
|
|
136
|
+
--range)
|
|
137
|
+
BASE="${2:-}"; HEAD_REF="${3:-HEAD}"
|
|
138
|
+
[ -n "$BASE" ] || _args_error "--range 에 base-ref 가 필요하다"
|
|
139
|
+
RUN_CWD="$PEER_ROOT"
|
|
140
|
+
;;
|
|
141
|
+
'') _args_error "모드 미지정 — 돌지 않은 실행을 판정으로 렌더하지 않는다" ;;
|
|
142
|
+
*) _args_error "알 수 없는 모드 '$1'" ;;
|
|
143
|
+
esac
|
|
144
|
+
|
|
145
|
+
# peer 트리를 **변이하지 않는다** — `--range` 에서는 cwd 만 peer 로 두고 읽기만 한다.
|
|
146
|
+
# 판정은 파이프를 통과시키지 않는다(PIPE-VERDICT).
|
|
147
|
+
out=$(cd "$RUN_CWD" && bash "$ENTRY" "$BASE" "$HEAD_REF" 2>&1); rc=$?
|
|
148
|
+
printf '%s\n' "$out"
|
|
149
|
+
|
|
150
|
+
# ── 명시 재매핑 (같은 숫자, 다른 사건 — 전파가 아니다) ───────────────────────
|
|
151
|
+
case "$rc" in
|
|
152
|
+
0) printf 'qasp-adapter: PASS — 앵커 없는 신규 실행 코드 없음\n'; exit "$RC_PASS" ;;
|
|
153
|
+
1) printf 'qasp-adapter: VIOLATION — 앵커 없는 신규 실행 코드가 있다\n'; exit "$RC_VIOLATION" ;;
|
|
154
|
+
2) printf 'qasp-adapter: ARGS — peer 계기가 인자를 거부했다(판정이 아니다)\n' >&2; exit "$RC_ARGS" ;;
|
|
155
|
+
3) printf 'qasp-adapter: HARNESS_ERROR — ref 해석 불가/git 실패(판정이 아니다)\n' >&2; exit "$RC_HARNESS" ;;
|
|
156
|
+
4) printf 'qasp-adapter: OUT_OF_SCOPE — 검사되지 않은 신규 실행 코드가 있다(깨끗함이 아니다)\n' >&2
|
|
157
|
+
exit "$RC_OUT_OF_SCOPE" ;;
|
|
158
|
+
*) _harness_error "peer 진입점이 자기 계약 {0,1,2,3,4} 밖의 $rc 를 반환했다" ;;
|
|
159
|
+
esac
|
package/scripts/branch_claim.sh
CHANGED
|
@@ -21,6 +21,19 @@
|
|
|
21
21
|
# 세션 정체성은 배관이 필요 없다. 실측(2026-08-09): git 훅은 Claude Code 가 주입한
|
|
22
22
|
# `CLAUDE_CODE_SESSION_ID` · `CLAUDE_PID` 를 그대로 상속한다.
|
|
23
23
|
#
|
|
24
|
+
# `show` 를 읽는 사람(사람이든 세션이든)에게: claim 수 ≠ 실제 동시편집 스레드 수 ★ 관측 보강, 2026-08-16
|
|
25
|
+
# `show` 는 `.git/fh-claims/*` 를 그대로 나열하고 pid 생존만 본다. 그래서 **claim 이 "live"라는
|
|
26
|
+
# 것은 «그 pid 가 아직 살아있다»는 뜻이지 «그 세션이 지금 이 브랜치를 편집 중」이란 뜻이 아니다.**
|
|
27
|
+
# 실측(2026-08-16, 이 감사-remediation 세션 자신): `show` 가 이 브랜치에 live claim 5개를 냈다.
|
|
28
|
+
# 교차확인(peer 에게 직접 SendMessage) 결과 5개 중 실질 편집 스레드는 **1~2개** 뿐이었다 —
|
|
29
|
+
# 나머지는 ⓐ idle bg 세션(작업 없음) ⓑ pid 는 살아있지만 **다른 레포로 이동한 세션의 stale claim**
|
|
30
|
+
# (release 를 안 하고 옮겨감) ⓒ 이미 handoff 되어 ListAgents 에도 안 잡히는 완료 세션의 잔존 파일.
|
|
31
|
+
# 즉 이 스크립트의 판정(§판정 키 = 세션)은 여전히 옳다 — «내 claim vs HEAD» 비교는 claim 수와
|
|
32
|
+
# 무관하게 성립한다. 틀렸던 건 **그 옆에서 `show` 출력을 읽고 "N개 세션이 동시에 이 브랜치를
|
|
33
|
+
# 건드리고 있다"고 위험도를 추정하는 것**이다. 그 추정을 하려면 `show` 가 아니라 ⓐ`reap` 으로
|
|
34
|
+
# 죽은 claim 을 먼저 걷어내고 ⓑ 남은 claim 의 세션에 **직접 물어라**(ListAgents+SendMessage) —
|
|
35
|
+
# claim 파일 자체엔 "지금 이 트리를 실제로 만지고 있는가"를 답할 필드가 없다.
|
|
36
|
+
#
|
|
24
37
|
# WHAT THIS DOES / DOESN'T
|
|
25
38
|
# 막는다 : «내가 마지막으로 claim 한 브랜치» ≠ HEAD 인 채로 커밋
|
|
26
39
|
# 안 막는다 : `git switch` 자체 (git 에 switch 훅이 없다 — 막을 자리가 없다)
|
|
@@ -223,6 +236,7 @@ cmd_show() {
|
|
|
223
236
|
"$(_field "$f" branch)" "$(_field "$f" label)" "$alive"
|
|
224
237
|
done
|
|
225
238
|
else echo " (기록 없음)"; fi
|
|
239
|
+
echo " (live = pid 생존일 뿐, 지금 이 트리를 편집 중이란 뜻 아님 — 위험도 추정은 reap 후 직접 문의)"
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
# 0 = 통과, 1 = 차단
|
|
@@ -64,10 +64,49 @@ _key() { # $1=capfile $2=key
|
|
|
64
64
|
| sed 's/[[:space:]]*$//'
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
# `GIT_META_WATCH` = 격리 클론 안에서 **추가로** 해싱할 경로들(코드실행으로 이어지는 git
|
|
68
|
+
# 메타데이터 표면). 비어 있으면 종전과 동일하게 트리만 잰다.
|
|
69
|
+
GIT_META_WATCH=""
|
|
70
|
+
|
|
71
|
+
# 🟥 **`GIT_META_NAMEONLY` 는 폐기했다(2026-08-16, cross-family 지목 + 손 재현).**
|
|
72
|
+
# 한때 `.git/objects` 를 «이름만» 세는 별도 목록으로 뒀다 — 근거는 「객체 파일명이 곧 내용
|
|
73
|
+
# 해시라 이름 집합이 안 바뀌면 내용도 안 바뀐다」였다. **그 추론은 정직한 액터에만 참이다.**
|
|
74
|
+
# 재현: `writes: read-only` 로 선언한 진입점이 기존 loose object 하나에
|
|
75
|
+
# `printf CORRUPT > "$p"` 를 하면 이름 집합은 그대로라 **`✅ VERIFIED · 관측=no-write`** 가 났다.
|
|
76
|
+
# 「같은 이름, 다른 내용」을 못 본다. 팩파일 수동 덮어쓰기도 같은 얼굴이다.
|
|
77
|
+
# ⇒ 이름만 세는 건 **정직성을 가정하는 계기**이고, 이 프로브의 대상은 정확히 그 가정이 깨진
|
|
78
|
+
# 경우다. 되돌리고, 대신 **비용 쪽을 고친다**(아래 배치 해싱).
|
|
79
|
+
#
|
|
80
|
+
# 비용 이력(적어 둔다 — 같은 실수를 두 번 하지 않게): 처음엔 파일당 `shasum` 프로세스라
|
|
81
|
+
# 2871개 × before/after × 양·음 arm 으로 **2분 타임아웃**이 났다. 원인은 `.git/objects` 가
|
|
82
|
+
# 아니라 **파일당 프로세스 스폰**이었다. `xargs` 배치로 한 번에 넘기면 같은 대상을 재면서
|
|
83
|
+
# 비용이 떨어진다 — 정확도를 깎아 비용을 맞추려던 게 잘못된 트레이드였다.
|
|
84
|
+
|
|
85
|
+
_snapshot() { # $1=dir → "sha path" 목록 (정렬)
|
|
86
|
+
{
|
|
87
|
+
# `.git` 은 통째로 제외한다 — `git status` 류가 `.git/index` 를 정당하게 갱신하므로
|
|
88
|
+
# 전체를 재면 읽기 전용 capability 가 거짓 VIOLATION 을 받는다. 대신 아래에서
|
|
89
|
+
# **실행으로 이어지는 표면만** 골라 넣는다.
|
|
90
|
+
find "$1" -type f -not -path '*/.git/*' 2>/dev/null
|
|
91
|
+
local w
|
|
92
|
+
for w in $GIT_META_WATCH; do
|
|
93
|
+
[ -e "$w" ] || continue
|
|
94
|
+
# 🟥 **경로 소속을 확인한다(2026-08-16).** 초판은 `$1` 과 무관하게 감시면을 **항상**
|
|
95
|
+
# 붙였다. `GIT_META_WATCH` 는 **샌드박스 안** 경로이므로, 바깥 카나리아를 잴 때
|
|
96
|
+
# `_snapshot "$outside"` 가 **샌드박스의 `.git` 을 같이 재는** 상태였다.
|
|
97
|
+
# 증상: 샌드박스 안 git 메타를 건드린 capability 가 VIOLATION 은 맞게 받되
|
|
98
|
+
# 이유가 **«샌드박스 밖이 바뀌었다»** 로 찍혔다 — 판정은 맞고 **처방이 오라우팅**된다.
|
|
99
|
+
# 레인이 `rc=1` 만 단언하고 **이유를 안 쟀기 때문에** 여태 안 보였다
|
|
100
|
+
# ([[feedback_metric_measures_presence_not_relation]]).
|
|
101
|
+
case "$w" in "$1"/*|"$1") ;; *) continue ;; esac
|
|
102
|
+
find "$w" -type f 2>/dev/null
|
|
103
|
+
done
|
|
104
|
+
} | LC_ALL=C sort -u \
|
|
105
|
+
| tr '\n' '\0' \
|
|
106
|
+
| xargs -0 -n 400 shasum -a 256 2>/dev/null \
|
|
107
|
+
| LC_ALL=C sort
|
|
108
|
+
# ⚠️ 명명된 한계: 개행이 든 파일명은 `find` 출력에서 쪼개진다. 이 프로브가 도입될 때부터
|
|
109
|
+
# 있던 성질이고 이번에 안 고쳤다 — 고치려면 `find -print0` 로 전 구간을 널 구분해야 한다.
|
|
71
110
|
}
|
|
72
111
|
|
|
73
112
|
# ★정리 트랩 — 신호/중단에도 temp 가 안 남게 한다(cross-family 지목).
|
|
@@ -106,9 +145,29 @@ probe_one() { # $1=capfile → rc
|
|
|
106
145
|
# (2026-08-16 병렬 세션 실측: 같은 순서 문제로 한 레인이 두 번 장식이 됐다).
|
|
107
146
|
local req_cwd wt=""
|
|
108
147
|
req_cwd=$(_key "$cap" requires_cwd)
|
|
148
|
+
# ★`SELF` 를 여기서도 푼다. 초판은 `capability_registry_check.sh` 에만 넣고 이쪽에 안 넣어서,
|
|
149
|
+
# 프로브가 `-d "SELF/.git"` 을 보고 «git 레포 아님» 으로 판정 → 빈 mktemp 샌드박스 →
|
|
150
|
+
# 진입점 rc=127 → **UNVERIFIABLE** 이 났다. 등록 검사기는 통과시키는데 프로브만 막는
|
|
151
|
+
# 형태라 원인이 안 보인다. **반쪽-픽스 전파 경계**의 세 번째 재현이고
|
|
152
|
+
# ([[feedback_half_fix_propagation_boundary]]), 계기가 잡았다.
|
|
153
|
+
# 해석은 검사기와 **같은 규칙**이어야 한다 — 다르면 두 축이 서로 다른 트리를 잰다.
|
|
154
|
+
if [ "$req_cwd" = "SELF" ]; then
|
|
155
|
+
req_cwd="$(git -C "$(dirname "$cap")" rev-parse --show-toplevel 2>/dev/null)"
|
|
156
|
+
if [ -z "$req_cwd" ]; then
|
|
157
|
+
printf '⚠️ UNVERIFIABLE %s — requires_cwd: SELF 인데 선언이 git 레포 안에 없다\n' "$cap" >&2
|
|
158
|
+
return "$RC_UNVERIFIABLE"
|
|
159
|
+
fi
|
|
160
|
+
fi
|
|
109
161
|
|
|
110
162
|
local sandbox outside wt_parent=""
|
|
111
|
-
|
|
163
|
+
# ★`-d .git` 이 아니라 **git 자신에게 묻는다**. 워크트리·submodule 에서 `.git` 은 **파일**이라
|
|
164
|
+
# `-d` 가 거짓이 되고, 그러면 이 프로브만 빈 mktemp 로 떨어져 같은 선언이
|
|
165
|
+
# 등록 검사기에선 VERIFIED · 프로브에선 UNVERIFIABLE 로 갈린다(cross-family 지목, 손 재현:
|
|
166
|
+
# 일반 레포 rc=0 / 워크트리 rc=3, 동일 capfile). 검사기는 이미 `rev-parse` 를 쓴다 —
|
|
167
|
+
# 두 계기가 다른 규칙을 들면 그게 이 레포가 이름 붙인 divergent-normalizer 다.
|
|
168
|
+
local _is_repo=""
|
|
169
|
+
[ -n "$req_cwd" ] && _is_repo="$(git -C "$req_cwd" rev-parse --show-toplevel 2>/dev/null)"
|
|
170
|
+
if [ -n "$_is_repo" ]; then
|
|
112
171
|
# 🟥 **`git worktree` 를 쓰지 않는다 — 워크트리는 정의상 실물 레포의 `.git` 을 공유한다.**
|
|
113
172
|
# 2026-08-16 보안 패스가 지목하고 거버너가 손으로 재현했다: `writes: read-only` 를 선언한
|
|
114
173
|
# 진입점이 워크트리 안에서 `git config --local core.pager "sh -c <cmd>"` 를 심으면
|
|
@@ -120,23 +179,67 @@ probe_one() { # $1=capfile → rc
|
|
|
120
179
|
# 이건 헤더의 명명된 잔여(«절대경로로 다른 데를 쓰면 놓친다»)와 **다르다**: 그건 capability 가
|
|
121
180
|
# 고른 경로이고, 이건 **프로브가 스스로 열어 준 통로**다.
|
|
122
181
|
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
182
|
+
# 처방(2026-08-16 2차) — **격리된 로컬 클론**. 초판은 `git archive` 로 `.git` 없는 사본을
|
|
183
|
+
# 만들었는데, 그러면 안전하지만 **git 을 쓰는 capability 전부가 영구히 등록 불가**가 된다
|
|
184
|
+
# (실측: `qasp-dev:new-code-anchor-scan` 이 rc=127 → UNVERIFIABLE → REJECTED).
|
|
185
|
+
# 엣지가 아니라 **한 부류 통째의 배제**이고, 이 레포 자신의 규율이 그걸 금한다 —
|
|
186
|
+
# *"만족할 수 없는 게이트는 엄격한 게이트가 아니라 우회 훈련기다."*
|
|
187
|
+
#
|
|
188
|
+
# `git clone --local --no-hardlinks` 는 **자기 `.git` 을 가진 독립 레포**를 만든다:
|
|
189
|
+
# · 진입점의 git 명령이 실제로 돈다(측정 가능해진다)
|
|
190
|
+
# · 쓰기는 **클론 안**에 갇힌다 — 실물 레포의 `.git` 은 공유되지 않는다
|
|
191
|
+
# · `--no-hardlinks` 로 오브젝트를 복사한다(하드링크는 같은 inode 를 공유한다)
|
|
192
|
+
# 🟥 그리고 **`origin` 을 지운다** — 클론의 origin 은 소스 경로를 가리키므로
|
|
193
|
+
# `git push origin` 이 **실물 레포로 되돌아가는 통로**가 된다. 우발 경로를 닫는다
|
|
194
|
+
# (절대경로를 직접 쓰는 공격은 헤더의 기존 명명 잔여이지 이 통로가 아니다).
|
|
127
195
|
wt_parent=$(mktemp -d) || return "$RC_HARNESS"
|
|
128
|
-
_PROBE_WT_PARENT="$wt_parent" # ★등록을 mktemp **직후**에
|
|
129
|
-
wt="$wt_parent/
|
|
130
|
-
if !
|
|
131
|
-
printf '⚠️ UNVERIFIABLE %s —
|
|
196
|
+
_PROBE_WT_PARENT="$wt_parent" # ★등록을 mktemp **직후**에 (누수 수리)
|
|
197
|
+
wt="$wt_parent/clone"
|
|
198
|
+
if ! git clone --local --no-hardlinks -q "$req_cwd" "$wt" >/dev/null 2>&1; then
|
|
199
|
+
printf '⚠️ UNVERIFIABLE %s — 격리 클론을 못 만들었다(%s)\n' "$cap" "$req_cwd" >&2
|
|
132
200
|
return "$RC_UNVERIFIABLE"
|
|
133
201
|
fi
|
|
134
|
-
|
|
135
|
-
|
|
202
|
+
git -C "$wt" remote remove origin >/dev/null 2>&1 || true
|
|
203
|
+
# 🟥 **alternates 를 지운다** (2026-08-16 cross-family 지목, 거버너 손 재현).
|
|
204
|
+
# `--no-hardlinks` 는 오브젝트를 **복사**하지만, **소스가 이미 alternates 를 갖고 있으면
|
|
205
|
+
# 클론이 그 alternates 를 상속한다** — 즉 클론의 오브젝트 저장소가 **샌드박스 밖 경로**를
|
|
206
|
+
# 참조하게 된다. 내 초판 격리 측정은 alternates 없는 소스만 봐서 이 채널을 못 봤다.
|
|
207
|
+
# 재현: 소스에 `objects/info/alternates` 를 심고 `--local --no-hardlinks` 클론 →
|
|
208
|
+
# 클론의 alternates 에 그 경로가 그대로 남는다.
|
|
209
|
+
# 지운 뒤 **오브젝트가 실제로 다 있는지**는 아래 단언이 본다 — 지우기만 하고 깨진 채로
|
|
210
|
+
# 돌리면 진입점이 이상하게 실패하고 그게 «위반 없음» 으로 렌더될 수 있다.
|
|
211
|
+
rm -f "$wt/.git/objects/info/alternates" 2>/dev/null || true
|
|
212
|
+
# 격리 전제를 **단언한다** — 하나라도 어긋나면 판정하지 않는다(측정 대신 침묵).
|
|
213
|
+
if [ ! -d "$wt/.git" ] || [ -n "$(git -C "$wt" remote 2>/dev/null)" ] \
|
|
214
|
+
|| [ -f "$wt/.git/objects/info/alternates" ]; then
|
|
215
|
+
printf '❌ %s: 격리 전제 파손(.git 부재 · origin 잔존 · alternates 잔존). 판정하지 않는다\n' "$cap" >&2
|
|
136
216
|
return "$RC_HARNESS"
|
|
137
217
|
fi
|
|
218
|
+
# alternates 를 지우고도 HEAD 트리가 온전한가 — 아니면 «격리했지만 못 쓰는» 사본이다.
|
|
219
|
+
if ! git -C "$wt" rev-parse --verify -q HEAD >/dev/null 2>&1 \
|
|
220
|
+
|| ! git -C "$wt" cat-file -e HEAD^{tree} 2>/dev/null; then
|
|
221
|
+
printf '⚠️ UNVERIFIABLE %s — alternates 제거 후 오브젝트가 불완전하다(소스가 alternates 에 의존)\n' "$cap" >&2
|
|
222
|
+
return "$RC_UNVERIFIABLE"
|
|
223
|
+
fi
|
|
138
224
|
sandbox="$wt"
|
|
139
225
|
_PROBE_SANDBOX="$wt"
|
|
226
|
+
# 🟥 **코드실행으로 이어지는 git 메타데이터 표면**을 스냅샷 대상에 넣는다.
|
|
227
|
+
# `.git` 전체를 넣지 않는 이유: `git status` 류가 `.git/index` 를 정당하게 갱신하므로
|
|
228
|
+
# 전체를 재면 **읽기 전용 capability 가 거짓 VIOLATION** 을 받는다. 그래서 «나중에
|
|
229
|
+
# 명령을 실행시킬 수 있는» 표면만 고른다 — config(core.pager·alias) · hooks · refs.
|
|
230
|
+
# ★`info/attributes` 와 `modules/` 도 본다(cross-family 지목). 전자는 `filter`/`clean`/
|
|
231
|
+
# `smudge` 속성으로 **체크아웃·add 시 임의 명령을 실행**시킬 수 있고, 후자는 submodule
|
|
232
|
+
# 의 실제 gitdir 라 그 안의 config·hooks 가 같은 실행 표면이다. config·hooks·refs 만
|
|
233
|
+
# 보면 그 둘로 새는 경로가 감시 밖에 남는다.
|
|
234
|
+
# 🟥 `.git/objects` 추가(2026-08-16). 이 축이 빠져 있는 동안 **`writes: read-only` 로
|
|
235
|
+
# 거짓 선언해도 VERIFIED 가 나왔다** — pmh-dev 자기선언 작업에서 실측으로 지목됐다
|
|
236
|
+
# (`git merge-tree --write-tree` 가 objects 2143→2144, 스냅샷은 그걸 안 봄).
|
|
237
|
+
# 즉 M6 는 «선언 진위» 를 잰다고 하면서 **쓰기의 한 종류를 구조적으로 못 보고 있었다.**
|
|
238
|
+
# 객체 생성은 읽기가 아니다 — 그래서 감시면에 넣는다. `.git/index` 는 여전히 제외다
|
|
239
|
+
# (`git status` 류가 정당하게 갱신하므로 넣으면 거짓 VIOLATION 이 된다).
|
|
240
|
+
# `.git/objects` 는 **내용까지** 해싱한다 — 이름만 세면 「같은 이름, 다른 내용」
|
|
241
|
+
# (기존 객체 덮어쓰기)을 못 본다(§_snapshot 의 폐기 기록 참조). 비용은 배치 해싱으로 낸다.
|
|
242
|
+
GIT_META_WATCH="$wt/.git/config $wt/.git/hooks $wt/.git/packed-refs $wt/.git/refs $wt/.git/info/attributes $wt/.git/modules $wt/.git/objects"
|
|
140
243
|
else
|
|
141
244
|
sandbox=$(mktemp -d) || return "$RC_HARNESS"
|
|
142
245
|
_PROBE_SANDBOX="$sandbox"
|
|
@@ -240,6 +343,30 @@ $out2"
|
|
|
240
343
|
return "$RC_UNVERIFIABLE"
|
|
241
344
|
fi
|
|
242
345
|
|
|
346
|
+
# 🟥 **cross-family 지목 + 손 재현(2026-08-16)**: `entry: bash -c 'false'` 를 `writes: read-only`
|
|
347
|
+
# 로 선언하면 **`✅ VERIFIED · 관측=no-write`** 가 나왔다. 진입점은 아무 일도 안 했는데
|
|
348
|
+
# 프로브가 「선언이 참임을 확인했다」고 말한 것이다 — **선언 거짓말을 잡으라고 만든 계기가
|
|
349
|
+
# «안 돌았다» 를 «깨끗하다» 로 렌더**했다([[feedback_not_found_is_not_zero_family]] 의 자기 판본).
|
|
350
|
+
# 같은 결함이 그 리뷰어의 환경에서 실제로 발현했다: 샌드박스가 `$HOME` 쓰기를 막아
|
|
351
|
+
# L7 의 위반 arm 이 `rc≠0 + 무변화` 가 되자 **레인이 통과 방향으로 무너졌다.**
|
|
352
|
+
#
|
|
353
|
+
# 판별 규칙 — rc 자체가 신호가 아니다. **선언된 enum 에 있는 rc = 「진입점이 돌고 판정을
|
|
354
|
+
# 냈다」**(정상 관측), **선언 밖 rc = 「이 실행이 무엇을 했는지 모른다」**(미측정).
|
|
355
|
+
# 실물 capability 는 음성 arm 에서 정당하게 비영을 내므로(LEAK·FINDINGS) rc≠0 을 통째로
|
|
356
|
+
# 막으면 과차단이 된다 — 그래서 enum 소속 여부로 가른다.
|
|
357
|
+
if [ "$rc" -ne 0 ]; then
|
|
358
|
+
local _enum _rc_declared=0 _pair
|
|
359
|
+
_enum="$(_key "$cap" verdict_enum)"
|
|
360
|
+
for _pair in $_enum; do
|
|
361
|
+
[ "${_pair%%=*}" = "$rc" ] && _rc_declared=1
|
|
362
|
+
done
|
|
363
|
+
if [ "$_rc_declared" = "0" ]; then
|
|
364
|
+
printf '⚠️ UNVERIFIABLE %s — 진입점이 rc=%s 로 끝났고 그 값은 선언된 enum 밖이다(enum=[%s]). 이 실행이 선언된 일을 실제로 했는지 알 수 없다 — 무변화를 «안 썼다» 로 읽지 않는다\n' \
|
|
365
|
+
"$cap" "$rc" "${_enum:-없음}"
|
|
366
|
+
return "$RC_UNVERIFIABLE"
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
|
|
243
370
|
printf '✅ VERIFIED %s — 선언=%s · 관측=%s\n' "$cap" "$declared" \
|
|
244
371
|
"$([ "$changed" = "1" ] && echo "wrote" || echo "no-write")"
|
|
245
372
|
return "$RC_OK"
|
|
@@ -354,13 +481,206 @@ writes: read-only
|
|
|
354
481
|
EOF
|
|
355
482
|
probe_one "$d/meta.cap" >/dev/null 2>&1; rc=$?
|
|
356
483
|
local _planted; _planted=$(git -C "$_tgt" config --local --get core.pager 2>/dev/null)
|
|
357
|
-
#
|
|
358
|
-
#
|
|
359
|
-
|
|
360
|
-
|
|
484
|
+
# ★기대값이 2026-08-16 2차에 **3 → 1 로 올라갔다.** 초판(`.git` 없는 사본)에서는 git 이
|
|
485
|
+
# 아예 안 돌아 «못 쟀다»(UNVERIFIABLE) 였다. 격리 클론으로 바꾸면서 진입점이 실제로
|
|
486
|
+
# 돌고, `.git/config` 쓰기가 **관측되어 VIOLATION** 이 된다 — 「못 쟀다」보다 강하다.
|
|
487
|
+
# 두 명제를 **각각** 단언한다:
|
|
488
|
+
# ① rc=1 → 탐지했다 (rc=0 이면 「안 썼다」를 거짓 주장한 것)
|
|
489
|
+
# ② 실물 무변경 → 격리했다 (planted 가 있으면 격리 자체가 뚫린 것)
|
|
490
|
+
# 둘은 독립이다. 하나만 보면 «막았는데 실물이 오염» 되거나 «깨끗한데 못 봤다» 를 놓친다.
|
|
491
|
+
if [ "$rc" = "1" ] && [ -z "$_planted" ]; then
|
|
492
|
+
pass=$((pass+1)); echo "✅ L9 git 메타데이터 채널: **탐지**(VIOLATION) + 실물 무변경(격리)"
|
|
493
|
+
else
|
|
494
|
+
fails=$((fails+1)); echo "❌ L9 rc=$rc (1 기대) · 실물 core.pager=[$_planted]"
|
|
495
|
+
fi
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
# ── L10: alternates 상속 차단 (2026-08-16 cross-family 지목) ──────────────
|
|
499
|
+
# `--no-hardlinks` 는 오브젝트를 복사하지만 **소스가 alternates 를 갖고 있으면 상속한다** —
|
|
500
|
+
# 클론의 오브젝트 저장소가 샌드박스 **밖 경로**를 참조하게 된다. 격리 주장이 조건부가 된다.
|
|
501
|
+
# ★rc 만 보지 않는다: 「판정이 났다」와 「클론에 alternates 가 없다」는 다른 명제다.
|
|
502
|
+
local _alt="$d/altsrc"
|
|
503
|
+
mkdir -p "$_alt/donor" "$_alt/src"
|
|
504
|
+
( cd "$_alt/donor" && git init -q . && git config user.email t@t && git config user.name t \
|
|
505
|
+
&& echo d > d.txt && git add d.txt && git commit -qm d ) >/dev/null 2>&1
|
|
506
|
+
( cd "$_alt/src" && git init -q . && git config user.email t@t && git config user.name t \
|
|
507
|
+
&& echo x > f.txt && git add f.txt && git commit -qm i ) >/dev/null 2>&1
|
|
508
|
+
mkdir -p "$_alt/src/.git/objects/info"
|
|
509
|
+
printf '%s\n' "$_alt/donor/.git/objects" > "$_alt/src/.git/objects/info/alternates"
|
|
510
|
+
# ★관측 채널이 **프로브 자신의 판정**이어야 한다. 초판은 진입점의 stdout 에서 'LEAKED' 를
|
|
511
|
+
# 찾으려 했는데 `probe_one` 은 진입점 출력을 반환하지 않는다 — 그 grep 은 **원리적으로
|
|
512
|
+
# 못 맞는다.** 되돌림 프로브가 그걸 잡았다(수리를 지워도 10/10 초록이었다).
|
|
513
|
+
# 대신 «alternates 가 있으면 쓴다» 로 만들어 **read-only 선언 위반**으로 드러나게 한다:
|
|
514
|
+
# 상속됨 → 진입점이 파일을 쓴다 → VIOLATION(1)
|
|
515
|
+
# 차단됨 → 아무것도 안 쓴다 → VERIFIED(0)
|
|
516
|
+
cat > "$d/alt.cap" <<EOF
|
|
517
|
+
id: test:alt
|
|
518
|
+
entry: bash -c '[ -f .git/objects/info/alternates ] && echo x > ALT_INHERITED || true'
|
|
519
|
+
requires_cwd: $_alt/src
|
|
520
|
+
writes: read-only
|
|
521
|
+
EOF
|
|
522
|
+
probe_one "$d/alt.cap" >/dev/null 2>&1; rc=$?
|
|
523
|
+
if [ "$rc" = "0" ]; then
|
|
524
|
+
pass=$((pass+1)); echo "✅ L10 소스의 alternates 가 샌드박스로 상속되지 않는다"
|
|
525
|
+
else
|
|
526
|
+
fails=$((fails+1)); echo "❌ L10 rc=$rc (0 기대) — alternates 가 상속돼 진입점이 썼다"
|
|
527
|
+
fi
|
|
528
|
+
|
|
529
|
+
# ── L11: 워크트리에서도 같은 판정이 나오나 (`.git` 이 **파일**인 경우) ────────
|
|
530
|
+
# cross-family 지목 + 손 재현: `-d .git` 판정이면 워크트리가 빈 mktemp 로 떨어져
|
|
531
|
+
# 같은 capfile 이 일반 레포 rc=0 / 워크트리 rc=3 으로 갈렸다. 등록 검사기는 이미
|
|
532
|
+
# `rev-parse` 를 쓰므로 두 계기가 어긋난 상태였다.
|
|
533
|
+
local _wt="$d/wtsrc"
|
|
534
|
+
mkdir -p "$_wt"
|
|
535
|
+
( cd "$_wt" && git init -q . && git config user.email t@t && git config user.name t \
|
|
536
|
+
&& echo hello > f.txt && git add -A && git commit -qm init \
|
|
537
|
+
&& git worktree add -q "$d/wtree" HEAD ) >/dev/null 2>&1
|
|
538
|
+
if [ -f "$d/wtree/.git" ]; then # `.git` 이 파일이어야 이 레인이 의미가 있다
|
|
539
|
+
for _r in "$_wt" "$d/wtree"; do
|
|
540
|
+
mkdir -p "$_r/.claude/capabilities"
|
|
541
|
+
cat > "$_r/.claude/capabilities/c.cap" <<EOF
|
|
542
|
+
id: t:needsfile
|
|
543
|
+
entry: bash -c 'cat f.txt >/dev/null'
|
|
544
|
+
requires_cwd: SELF
|
|
545
|
+
writes: read-only
|
|
546
|
+
EOF
|
|
547
|
+
done
|
|
548
|
+
probe_one "$_wt/.claude/capabilities/c.cap" >/dev/null 2>&1; local _plain=$?
|
|
549
|
+
probe_one "$d/wtree/.claude/capabilities/c.cap" >/dev/null 2>&1; local _work=$?
|
|
550
|
+
if [ "$_plain" = "0" ] && [ "$_work" = "0" ]; then
|
|
551
|
+
pass=$((pass+1)); echo "✅ L11 워크트리(.git 파일)도 일반 레포와 같은 판정"
|
|
361
552
|
else
|
|
362
|
-
fails=$((fails+1)); echo "❌
|
|
553
|
+
fails=$((fails+1)); echo "❌ L11 일반=$_plain 워크트리=$_work (둘 다 0 기대)"
|
|
363
554
|
fi
|
|
555
|
+
git -C "$_wt" worktree remove --force "$d/wtree" >/dev/null 2>&1 || true # noqa: destructive-op
|
|
556
|
+
else
|
|
557
|
+
fails=$((fails+1)); echo "❌ L11 픽스처 실패 — 워크트리의 .git 이 파일이 아니다(레인 무의미)"
|
|
558
|
+
fi
|
|
559
|
+
|
|
560
|
+
# ── L12: `info/attributes` 쓰기가 관측되나 (filter/clean/smudge = 코드실행) ──
|
|
561
|
+
local _at="$d/atsrc"
|
|
562
|
+
mkdir -p "$_at"
|
|
563
|
+
( cd "$_at" && git init -q . && git config user.email t@t && git config user.name t \
|
|
564
|
+
&& echo x > f.txt && git add -A && git commit -qm init ) >/dev/null 2>&1
|
|
565
|
+
cat > "$d/attr.cap" <<EOF
|
|
566
|
+
id: t:attrs
|
|
567
|
+
entry: bash -c 'mkdir -p .git/info && echo "* filter=pwn" >> .git/info/attributes'
|
|
568
|
+
requires_cwd: $_at
|
|
569
|
+
writes: read-only
|
|
570
|
+
EOF
|
|
571
|
+
probe_one "$d/attr.cap" >/dev/null 2>&1; rc=$?
|
|
572
|
+
local _leaked; _leaked=$(cat "$_at/.git/info/attributes" 2>/dev/null)
|
|
573
|
+
if [ "$rc" = "1" ] && [ -z "$_leaked" ]; then
|
|
574
|
+
pass=$((pass+1)); echo "✅ L12 info/attributes 쓰기 탐지(VIOLATION) + 실물 무변경"
|
|
575
|
+
else
|
|
576
|
+
fails=$((fails+1)); echo "❌ L12 rc=$rc (1 기대) · 실물 attributes=[$_leaked]"
|
|
577
|
+
fi
|
|
578
|
+
|
|
579
|
+
# ── L13/L13b: git **객체 생성**이 `writes: read-only` 를 뚫던 자리 ──────────────
|
|
580
|
+
# 🟥 이 축이 없는 동안 M6 는 «선언 진위» 를 잰다면서 **쓰기의 한 종류를 못 봤다**.
|
|
581
|
+
# 실측 지목(pmh-dev 자기선언 작업, 2026-08-16): `git merge-tree --write-tree` 가
|
|
582
|
+
# `.git/objects` 를 2143→2144 로 늘렸는데 스냅샷이 `.git/` 을 통째 제외해 VERIFIED 가 났다.
|
|
583
|
+
# 양·음 **둘 다** 세운다 — 양성만 있으면 「.git 만 닿으면 무조건 막는 계기」와 구분이 안 된다.
|
|
584
|
+
local _ob="$d/obsrc"
|
|
585
|
+
mkdir -p "$_ob"
|
|
586
|
+
( cd "$_ob" && git init -q . && git config user.email t@t && git config user.name t \
|
|
587
|
+
&& echo x > f.txt && git add -A && git commit -qm init ) >/dev/null 2>&1
|
|
588
|
+
cat > "$d/obj_write.cap" <<EOF
|
|
589
|
+
id: t:objwrite
|
|
590
|
+
entry: bash -c 'echo payload | git hash-object -w --stdin >/dev/null'
|
|
591
|
+
requires_cwd: $_ob
|
|
592
|
+
writes: read-only
|
|
593
|
+
EOF
|
|
594
|
+
probe_one "$d/obj_write.cap" >/dev/null 2>&1; rc=$?
|
|
595
|
+
if [ "$rc" = "1" ]; then
|
|
596
|
+
pass=$((pass+1)); echo "✅ L13 git 객체 생성이 read-only 선언 위반으로 탐지된다"
|
|
597
|
+
else
|
|
598
|
+
fails=$((fails+1)); echo "❌ L13 rc=$rc (1 기대) — 객체 쓰기가 여전히 안 보인다"
|
|
599
|
+
fi
|
|
600
|
+
# L13b 컨트롤(과차단 방지): 진짜 읽기 전용 git 사용은 조용해야 한다
|
|
601
|
+
cat > "$d/obj_read.cap" <<EOF
|
|
602
|
+
id: t:objread
|
|
603
|
+
entry: bash -c 'git log --oneline -1 >/dev/null; git status --porcelain >/dev/null; git diff HEAD >/dev/null'
|
|
604
|
+
requires_cwd: $_ob
|
|
605
|
+
writes: read-only
|
|
606
|
+
EOF
|
|
607
|
+
probe_one "$d/obj_read.cap" >/dev/null 2>&1; rc=$?
|
|
608
|
+
if [ "$rc" = "0" ]; then
|
|
609
|
+
pass=$((pass+1)); echo "✅ L13b 컨트롤 — 읽기 전용 git 사용(log/status/diff)은 통과한다"
|
|
610
|
+
else
|
|
611
|
+
fails=$((fails+1)); echo "❌ L13b rc=$rc (0 기대) — 과차단: 읽기만 하는 능력을 막는다"
|
|
612
|
+
fi
|
|
613
|
+
|
|
614
|
+
# ── L14/L14b **이유**를 잰다 — 판정만 재면 오라우팅이 안 보인다 ────────────────
|
|
615
|
+
# 🟥 실측(2026-08-16): 샌드박스 안 git 메타를 건드린 capability 가 VIOLATION 은 맞게
|
|
616
|
+
# 받으면서 이유는 «**샌드박스 밖**이 바뀌었다» 로 찍혔다(원인: `_snapshot` 이 인자와 무관하게
|
|
617
|
+
# 샌드박스 안 감시면을 항상 붙임). 기존 레인은 **`rc=1` 만 단언해서 못 봤다.**
|
|
618
|
+
# 틀린 이유는 사람을 틀린 수리로 보낸다 — 그래서 이유를 별도 레인으로 못 박는다.
|
|
619
|
+
local _rs="$d/reasonsrc"
|
|
620
|
+
mkdir -p "$_rs"
|
|
621
|
+
( cd "$_rs" && git init -q . && git config user.email t@t && git config user.name t \
|
|
622
|
+
&& echo x > f.txt && git add -A && git commit -qm init ) >/dev/null 2>&1
|
|
623
|
+
cat > "$d/inside.cap" <<EOF
|
|
624
|
+
id: t:inside
|
|
625
|
+
entry: bash -c 'git config --local core.pager "sh -c id"'
|
|
626
|
+
requires_cwd: $_rs
|
|
627
|
+
writes: read-only
|
|
628
|
+
EOF
|
|
629
|
+
out=$(probe_one "$d/inside.cap" 2>&1); rc=$?
|
|
630
|
+
if [ "$rc" = "1" ] && printf '%s' "$out" | grep -q '샌드박스가 바뀌었다'; then
|
|
631
|
+
pass=$((pass+1)); echo "✅ L14 샌드박스 **안** 쓰기의 이유가 «안» 으로 찍힌다(오라우팅 없음)"
|
|
632
|
+
else
|
|
633
|
+
fails=$((fails+1)); echo "❌ L14 rc=$rc · 이유=[$(printf '%s' "$out" | tail -1)]"
|
|
634
|
+
fi
|
|
635
|
+
# L14b 컨트롤 — L14 를 고치면서 **밖 감시를 죽이지 않았는가.**
|
|
636
|
+
# ⚠️ 과녁은 **프로브가 실제로 보는 밖**이어야 한다. 초판은 self-test 자신의 temp 디렉토리를
|
|
637
|
+
# 과녁으로 잡았다가 정당하게 실패했다 — 그건 헤더가 이미 «못 본다» 로 명명한 잔여 구간이고,
|
|
638
|
+
# 거기서 탐지를 기대하는 것은 레인이 **문서화된 한계를 결함으로 오채점**하는 것이다.
|
|
639
|
+
# 실제 밖-감시면은 홈 엔트리 목록이므로 거기를 친다(L7 과 같은 채널, **이유**를 단언한다).
|
|
640
|
+
local _homemark="$HOME/.fh_probe_l14b_$$"
|
|
641
|
+
cat > "$d/outside.cap" <<EOF
|
|
642
|
+
id: t:outside
|
|
643
|
+
entry: bash -c 'echo pwned > $_homemark'
|
|
644
|
+
requires_cwd: $_rs
|
|
645
|
+
writes: read-only
|
|
646
|
+
EOF
|
|
647
|
+
out=$(probe_one "$d/outside.cap" 2>&1); rc=$?
|
|
648
|
+
rm -f "$_homemark"
|
|
649
|
+
if [ "$rc" = "1" ] && printf '%s' "$out" | grep -q '홈 디렉토리'; then
|
|
650
|
+
pass=$((pass+1)); echo "✅ L14b 컨트롤 — 밖(홈) 쓰기는 여전히 VIOLATION 이고 이유도 «홈» 이다"
|
|
651
|
+
else
|
|
652
|
+
fails=$((fails+1)); echo "❌ L14b rc=$rc · 이유=[$(printf '%s' "$out" | tail -1)] — 밖 감시가 죽었거나 이유가 틀렸다"
|
|
653
|
+
fi
|
|
654
|
+
|
|
655
|
+
# ── L15/L15b «안 돌았다» 를 «안 썼다» 로 렌더하지 않는다 ─────────────────────
|
|
656
|
+
# cross-family 지목 + 손 재현: `entry: bash -c 'false'` + `writes: read-only` 가
|
|
657
|
+
# **`✅ VERIFIED · 관측=no-write`** 를 받았다. 선언 거짓말을 잡는 계기가 미실행을 통과로 렌더.
|
|
658
|
+
cat > "$d/nz_undeclared.cap" <<EOF
|
|
659
|
+
id: t:nzu
|
|
660
|
+
entry: bash -c 'false'
|
|
661
|
+
requires_cwd: $_rs
|
|
662
|
+
writes: read-only
|
|
663
|
+
EOF
|
|
664
|
+
out=$(probe_one "$d/nz_undeclared.cap" 2>&1); rc=$?
|
|
665
|
+
if [ "$rc" = "3" ]; then
|
|
666
|
+
pass=$((pass+1)); echo "✅ L15 선언 밖 비영 종료는 UNVERIFIABLE(미측정≠통과)"
|
|
667
|
+
else
|
|
668
|
+
fails=$((fails+1)); echo "❌ L15 rc=$rc (3 기대) — 안 돈 실행이 통과로 렌더된다"
|
|
669
|
+
fi
|
|
670
|
+
# L15b 컨트롤(과차단 방지): 실물 capability 는 음성 arm 에서 정당하게 비영을 낸다.
|
|
671
|
+
# 선언된 enum 안의 비영은 **정상 관측**이어야 한다.
|
|
672
|
+
cat > "$d/nz_declared.cap" <<EOF
|
|
673
|
+
id: t:nzd
|
|
674
|
+
entry: bash -c 'exit 2'
|
|
675
|
+
requires_cwd: $_rs
|
|
676
|
+
writes: read-only
|
|
677
|
+
verdict_enum: 0=CLEAN 2=FINDINGS
|
|
678
|
+
EOF
|
|
679
|
+
out=$(probe_one "$d/nz_declared.cap" 2>&1); rc=$?
|
|
680
|
+
if [ "$rc" = "0" ]; then
|
|
681
|
+
pass=$((pass+1)); echo "✅ L15b 컨트롤 — 선언된 enum 안의 비영은 정상 관측(과차단 아님)"
|
|
682
|
+
else
|
|
683
|
+
fails=$((fails+1)); echo "❌ L15b rc=$rc (0 기대) — 정당한 비영 verdict 를 막는다"
|
|
364
684
|
fi
|
|
365
685
|
|
|
366
686
|
rm -rf "$d"
|