@chrono-meta/fh-gate 2.2.0 → 2.4.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/qasp-new-code-anchor.cap +5 -2
- package/.claude/capabilities/adapters/qasp-web-rules.cap +130 -0
- package/.claude/rules/fh_4axis_gate.md +55 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +51 -6
- package/README.ja.md +14 -2
- package/README.ko.md +14 -1
- package/README.md +16 -2
- package/README.zh.md +12 -1
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +231 -7
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +24 -0
- package/package.json +9 -1
- 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 +110 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
- package/scripts/adapters/fixtures/qasp_web_rules_known_negative.json +7 -0
- package/scripts/adapters/fixtures/qasp_web_rules_known_positive.json +7 -0
- package/scripts/adapters/mate_agent_boundary.sh +27 -7
- package/scripts/adapters/qasp_web_rules.sh +171 -0
- package/scripts/capability_effect_probe.sh +104 -0
- package/scripts/capability_registry_check.sh +69 -3
- package/scripts/relay_channel.sh +151 -6
- package/scripts/selfcheck.sh +18 -0
- package/scripts/target_freeze.sh +239 -0
- package/scripts/test_adapter_lanes.sh +54 -1
- package/scripts/test_marker_axes_run_lanes.sh +19 -0
- package/scripts/test_marker_standpoint_lanes.sh +83 -0
- package/scripts/test_marker_thirdparty_lanes.sh +73 -0
- package/scripts/test_regression_guard_ci_lanes.sh +170 -0
- package/scripts/test_relay_channel_lanes.sh +146 -17
- package/scripts/test_target_freeze_lanes.sh +203 -0
- package/templates/.git-hooks/pre-commit +262 -0
- package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
- package/templates/regression_guard.sh +32 -4
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# qasp_web_rules.sh — FH 소유 **어댑터**. peer `qasp` 의
|
|
3
|
+
# `scripts/web_rules_pr_scan.sh`(1.5막 웹 셀렉터-안정성 rule 세트)를 클러스터 호출 가능한
|
|
4
|
+
# typed capability 로 감싼다.
|
|
5
|
+
#
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
# 무엇을 재나
|
|
8
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
# **웹 E2E 테스트 diff 에 로케이터/대기 규율 위반이 들어왔는가.** rule 5종
|
|
10
|
+
# (`web-coordinate-click` · `web-wait-for-timeout` 등 — LOCATOR_DISCIPLINE ·
|
|
11
|
+
# ASSERTION_STRENGTH 렌즈). peer 헤더의 기원 실측: 이 rule 테이블은 2026-07-24 부터
|
|
12
|
+
# 있었지만 **부르는 사람이 없었다**(2026-07-27 실측: 프로덕션 호출자 0건, tests/ 와
|
|
13
|
+
# 자기 자신뿐). *"호출자가 없는 검출기는 산문이지 게이트가 아니다."*
|
|
14
|
+
# 진입점은 **peer 소유 코드**이고 이 어댑터가 만든 것이 아니다.
|
|
15
|
+
#
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
# VERDICT ENUM (선언과 정확히 일치해야 한다)
|
|
18
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
# 0 CLEAN 위반 0건
|
|
20
|
+
# 1 FINDINGS 위반 존재
|
|
21
|
+
# 2 ARGS 인자 오류 — 판정이 아니다
|
|
22
|
+
# 3 ENGINE_ERROR 스캔이 못 돌았다(python 부재 · 입력 불가 · 대상 0개) — 판정이 아니다
|
|
23
|
+
# 20 PEER_ABSENT **peer 하네스가 이 머신에 없다**
|
|
24
|
+
#
|
|
25
|
+
# 🟥 **`2` 는 peer 셸 헤더에 안 적혀 있다** — 그 헤더는 `0/1/3` 만 적는다. 그러나 하위
|
|
26
|
+
# python CLI(`src/static_review/web_rules.py`)가 `parser.error()` 를 쓰고 argparse 는
|
|
27
|
+
# **exit 2** 를 내며, 셸 래퍼는 `exit "$RC"` 로 **그대로 전파**한다(실측 2026-08-17).
|
|
28
|
+
# 즉 문서화된 계약보다 실제 계약이 한 값 넓다. 어댑터는 **실제 계약**을 선언한다 —
|
|
29
|
+
# 안 그러면 argparse 오류가 `*)` 로 떨어져 HARNESS_ERROR 가 되고, 「인자를 틀렸다」가
|
|
30
|
+
# 「계기가 깨졌다」로 렌더된다.
|
|
31
|
+
#
|
|
32
|
+
# 🟥 값 셋이 「안 쟀다」의 서로 다른 얼굴이다:
|
|
33
|
+
# ARGS(2) 인자가 틀렸다 (호출에 대한 진술)
|
|
34
|
+
# ENGINE_ERROR(3) 하네스는 있는데 못 쟀다 (계기에 대한 진술)
|
|
35
|
+
# PEER_ABSENT(20) 그 노드가 이 머신에 없다 (클러스터에 대한 진술)
|
|
36
|
+
# peer 가 자기 규율로 이미 적어 뒀다 — *"exit 3 을 0 으로 뭉개지 않는다. 못 잰 것을
|
|
37
|
+
# 깨끗하다고 보고하는 순간 게이트가 장식이 된다."* 어댑터는 그 위에 PEER_ABSENT 만 더한다.
|
|
38
|
+
#
|
|
39
|
+
# ⚠️ 하위 exit 을 **전파하지 않는다** — 아래 case 가 명시 재매핑이다. 같은 숫자가 이
|
|
40
|
+
# 클러스터 안에서 다른 사건을 뜻한다(`new_code_anchor_scan.sh` 의 `4=OUT_OF_SCOPE` ·
|
|
41
|
+
# `judge_rer.sh` 의 `4=BLOCKED` · `pull_cli.py` 의 `4=NO_CORPUS`).
|
|
42
|
+
#
|
|
43
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
# 🟥 `writes: write-local` — read-only 가 **아니다.** cold 실측이 근거다
|
|
45
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
# 진입점이 `python -m src.static_review.web_rules` 를 부르고, 그 import 폐포가
|
|
47
|
+
# `src/config/settings.py` 의 **import 시점 `ensure_dirs()`** 에 닿는다.
|
|
48
|
+
#
|
|
49
|
+
# warm(운영자 실 트리) 실측 : 트리 변화 **없음** ← 🟥 이것을 근거로 쓰면 안 된다
|
|
50
|
+
# cold(격리 클론) 실측 : 신규 엔트리 **25개**
|
|
51
|
+
# · `__pycache__` 디렉토리 5 + `.pyc` 파일 18
|
|
52
|
+
# · **빈 디렉토리 `./tmp` · `./uploads`** ← `mkdir(exist_ok=True)` 의 산물
|
|
53
|
+
#
|
|
54
|
+
# warm 이 깨끗해 보인 이유는 그 디렉토리들이 **이미 있어서** mkdir 이 무음 no-op 이었기
|
|
55
|
+
# 때문이다 — **warm cache 아티팩트지 read-only 의 증거가 아니다.**
|
|
56
|
+
# ★그리고 저 «빈 디렉토리» 는 2026-08-17 이전의 M6 이펙트 프로브가 **구조적으로 못 보던**
|
|
57
|
+
# 형태다(`_snapshot` 이 `-type f` 라 파일만 셌다). FH PR #422 로 그 축을 넣었고,
|
|
58
|
+
# 그래서 이 선언은 **이제 기계로 검증·반증된다.** 그 전이었으면 거짓 `read-only` 로
|
|
59
|
+
# 선언해도 VERIFIED 를 받았다.
|
|
60
|
+
#
|
|
61
|
+
# ⚠️ **명시 잔여 — M6 는 peer 트리를 안 본다.** 프로브의 «샌드박스 밖» 감시면은 임시
|
|
62
|
+
# 카나리아 하나와 `$HOME` 엔트리 목록뿐이다. 어댑터는 **정의상 peer 트리에서 돌므로**,
|
|
63
|
+
# 이 capability 의 가장 실질적인 쓰기 표면이 프로브의 감시 밖에 있다. 위 cold 수치는
|
|
64
|
+
# 프로브 판정이 아니라 **손 실측**이다. 이 갭은 이 파일이 만든 게 아니라 어댑터 클래스
|
|
65
|
+
# 전체의 성질이고, 여기 이름으로 남긴다.
|
|
66
|
+
#
|
|
67
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
# 캘리브레이션 쌍 — **FH 소유 픽스처**, 오프라인·결정적
|
|
69
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
# 양성: `fixtures/qasp_web_rules_known_positive.json` — `page.wait_for_timeout(3000)`
|
|
71
|
+
# → `web-wait-for-timeout` 발화 → FINDINGS
|
|
72
|
+
# 음성: `fixtures/qasp_web_rules_known_negative.json` — `get_by_role(...).click()`
|
|
73
|
+
# → 위반 0 → CLEAN
|
|
74
|
+
# ★왜 peer 픽스처를 안 쓰나: peer 의 `tests/` 는 그 레포 사정으로 움직인다. 남의 트리에
|
|
75
|
+
# 있는 픽스처에 의존하는 계기는 **별도 계기가 아니라 그 레포의 일부**다.
|
|
76
|
+
# ★왜 PR 번호 모드를 안 쓰나: `gh api` 가 필요해 **네트워크·인증에 의존**하고, 그러면
|
|
77
|
+
# 등록 바의 M4 가 「계기가 판별하는가」가 아니라 「오늘 gh 가 되는가」를 재게 된다.
|
|
78
|
+
# ★residency: peer 는 company 다. 이 두 픽스처는 **우리가 지은 합성 diff** 라 조직
|
|
79
|
+
# 식별자가 없다 — 캘리브레이션 출력에 조직 경로가 실리지 않는다.
|
|
80
|
+
#
|
|
81
|
+
# 사용법
|
|
82
|
+
# qasp_web_rules.sh --diff-json <path> # peer 레포에서 실행, 실제 diff 스캔
|
|
83
|
+
# qasp_web_rules.sh --known-positive # expect FINDINGS
|
|
84
|
+
# qasp_web_rules.sh --known-negative # expect CLEAN
|
|
85
|
+
set -o pipefail
|
|
86
|
+
|
|
87
|
+
PEER_NAME="qasp"
|
|
88
|
+
RC_CLEAN=0; RC_FINDINGS=1; RC_ARGS=2; RC_ENGINE=3; RC_HARNESS=10; RC_PEER_ABSENT=20
|
|
89
|
+
|
|
90
|
+
_harness_error() { printf 'qasp-web-rules: HARNESS_ERROR — %s\n' "$1" >&2; exit "$RC_HARNESS"; }
|
|
91
|
+
_args_error() { printf 'qasp-web-rules: ARGS — %s\n' "$1" >&2; exit "$RC_ARGS"; }
|
|
92
|
+
_peer_absent() { printf 'qasp-web-rules: PEER_ABSENT — %s\n' "$1" >&2; exit "$RC_PEER_ABSENT"; }
|
|
93
|
+
|
|
94
|
+
SELF_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
95
|
+
_LIB="$SELF_DIR/peer_resolve.sh"
|
|
96
|
+
[ -r "$_LIB" ] || _harness_error "peer 해석 라이브러리 부재: $_LIB"
|
|
97
|
+
# shellcheck source=/dev/null
|
|
98
|
+
. "$_LIB"
|
|
99
|
+
|
|
100
|
+
# ── ① 인자 먼저 검문한다 — peer 해석보다 **앞**이다 ─────────────────────────
|
|
101
|
+
# 🟥 순서가 뒤집혀 있었다(2026-08-17 CI 실측). 초판은 peer 를 먼저 풀었고, 그래서
|
|
102
|
+
# **같은 잘못된 호출이 머신마다 다른 값**을 냈다:
|
|
103
|
+
# peer 있는 머신(내 로컬) : `qasp_web_rules.sh` (모드 없음) → ARGS(2)
|
|
104
|
+
# peer 없는 머신(CI) : 같은 호출 → HARNESS_ERROR(10)
|
|
105
|
+
# 즉 검정이 «어떻게 불렀나» 가 아니라 **«이 머신에 뭐가 깔렸나»** 를 재고 있었다.
|
|
106
|
+
# 이 레포가 오늘 출하 레인에서 이미 데인 클래스와 같다.
|
|
107
|
+
#
|
|
108
|
+
# ★원리: 이 파일 헤더가 스스로 갈라 놓은 대로 **ARGS 는 «호출에 대한 진술»** 이고
|
|
109
|
+
# **PEER_ABSENT 는 «클러스터에 대한 진술»** 이다. 두 사실은 독립이고, 호출이 틀렸다는
|
|
110
|
+
# 것은 클러스터를 보지 않고도 안다. 오타를 낸 사람에게 «qasp 를 깔아라» 라고 말하면
|
|
111
|
+
# 진짜 문제에서 멀어진다.
|
|
112
|
+
# ⚠️ 자매 어댑터(`qasp_new_code_anchor.sh`)는 아직 **peer-first** 다 — 그쪽 enum 레인이
|
|
113
|
+
# `_lane_peer` 로 가려져 있어서 이 어긋남이 안 보였다. 여기서만 고쳤고, 그쪽 정렬은
|
|
114
|
+
# 별건으로 남긴다(고치면 그쪽 레인의 SKIP 가드도 같이 걷어야 한다).
|
|
115
|
+
#
|
|
116
|
+
# ★경계는 «호출 + FH 로컬 파일» 이다. peer 를 봐야만 알 수 있는 것만 ② 로 내린다.
|
|
117
|
+
# 픽스처도 여기다 — `$SELF_DIR/fixtures/` 는 **우리 소유**라 peer 유무와 무관하다.
|
|
118
|
+
MODE="${1:-}"
|
|
119
|
+
DIFF_JSON=""
|
|
120
|
+
case "$MODE" in
|
|
121
|
+
--known-positive) DIFF_JSON="$SELF_DIR/fixtures/qasp_web_rules_known_positive.json" ;;
|
|
122
|
+
--known-negative) DIFF_JSON="$SELF_DIR/fixtures/qasp_web_rules_known_negative.json" ;;
|
|
123
|
+
--diff-json)
|
|
124
|
+
DIFF_JSON="${2:-}"
|
|
125
|
+
[ -n "$DIFF_JSON" ] || _args_error "--diff-json 뒤에 경로가 필요하다"
|
|
126
|
+
# 읽을 수 없는 입력은 **호출 오류**다 — peer 가 있든 없든 같은 값이어야 한다.
|
|
127
|
+
[ -r "$DIFF_JSON" ] || _args_error "읽을 수 없는 diff-json: $DIFF_JSON" ;;
|
|
128
|
+
'') _args_error "모드 미지정 — 돌지 않은 실행을 판정으로 렌더하지 않는다" ;;
|
|
129
|
+
*) _args_error "알 수 없는 모드 '$MODE'" ;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
132
|
+
# 픽스처 부재는 **ARGS 가 아니라 HARNESS_ERROR** 다 — 캘리브레이션 쌍이 사라진 것은
|
|
133
|
+
# 「호출을 잘못했다」가 아니라 **「이 계기가 자기 판별력을 증명할 수 없다」**는 뜻이다.
|
|
134
|
+
case "$MODE" in
|
|
135
|
+
--known-positive|--known-negative)
|
|
136
|
+
[ -r "$DIFF_JSON" ] || _harness_error "캘리브레이션 픽스처 부재: $DIFF_JSON" ;;
|
|
137
|
+
esac
|
|
138
|
+
|
|
139
|
+
# ── ② peer 해석 ─────────────────────────────────────────────────────────────
|
|
140
|
+
PEER_ROOT="$(fh_peer_resolve "$PEER_NAME")"; _rrc=$?
|
|
141
|
+
case "$_rrc" in
|
|
142
|
+
0) ;;
|
|
143
|
+
1) _peer_absent "peer «$PEER_NAME» 가 이 머신에 없다 — 안 쟀다(통과가 아니다)" ;;
|
|
144
|
+
2) _harness_error "peer «$PEER_NAME» 해석이 모호하다 — 어느 트리를 쟀는지 말할 수 없으므로 판정하지 않는다" ;;
|
|
145
|
+
*) _harness_error "peer 루트 목록 자체가 없다 — 전제 파손" ;;
|
|
146
|
+
esac
|
|
147
|
+
|
|
148
|
+
ENTRY="$PEER_ROOT/scripts/web_rules_pr_scan.sh"
|
|
149
|
+
[ -r "$ENTRY" ] || _harness_error "peer 진입점 부재: $ENTRY (하네스는 있으나 계기가 없다)"
|
|
150
|
+
|
|
151
|
+
# peer 의 진입점은 `git rev-parse --show-toplevel` 로 자기 루트를 잡으므로 **cwd 가 peer 여야**
|
|
152
|
+
# 한다. 판정은 파이프를 통과시키지 않는다(PIPE-VERDICT).
|
|
153
|
+
# `PYTHON` 을 명시로 넘긴다 — peer 의 기본값이 `$REPO_ROOT/.venv/bin/python` 이라 이미 맞지만,
|
|
154
|
+
# 그 폴백(`command -v python3`)이 발동하면 **의존성 없는 인터프리터**로 떨어져 ENGINE_ERROR 가
|
|
155
|
+
# 나고, 그건 「이 머신에 뭐가 깔렸나」를 재는 것이 된다. 명시하면 그 갈림이 안 생긴다.
|
|
156
|
+
_PY="$PEER_ROOT/.venv/bin/python"
|
|
157
|
+
if [ -x "$_PY" ]; then
|
|
158
|
+
out=$(cd "$PEER_ROOT" && PYTHON="$_PY" bash "$ENTRY" --files "$DIFF_JSON" 2>&1); rc=$?
|
|
159
|
+
else
|
|
160
|
+
out=$(cd "$PEER_ROOT" && bash "$ENTRY" --files "$DIFF_JSON" 2>&1); rc=$?
|
|
161
|
+
fi
|
|
162
|
+
printf '%s\n' "$out"
|
|
163
|
+
|
|
164
|
+
# ── 명시 재매핑 (같은 숫자, 다른 사건 — 전파가 아니다) ───────────────────────
|
|
165
|
+
case "$rc" in
|
|
166
|
+
0) printf 'qasp-web-rules: CLEAN — 웹 로케이터/대기 규율 위반 0건\n'; exit "$RC_CLEAN" ;;
|
|
167
|
+
1) printf 'qasp-web-rules: FINDINGS — 위반이 있다\n'; exit "$RC_FINDINGS" ;;
|
|
168
|
+
2) printf 'qasp-web-rules: ARGS — peer 계기가 인자를 거부했다(판정이 아니다)\n' >&2; exit "$RC_ARGS" ;;
|
|
169
|
+
3) printf 'qasp-web-rules: ENGINE_ERROR — 스캔이 돌지 못했다. «위반 0» 이 아니다\n' >&2; exit "$RC_ENGINE" ;;
|
|
170
|
+
*) _harness_error "peer 진입점이 자기 계약 {0,1,2,3} 밖의 $rc 를 반환했다" ;;
|
|
171
|
+
esac
|
|
@@ -105,6 +105,33 @@ _snapshot() { # $1=dir → "sha path" 목록 (정렬)
|
|
|
105
105
|
| tr '\n' '\0' \
|
|
106
106
|
| xargs -0 -n 400 shasum -a 256 2>/dev/null \
|
|
107
107
|
| LC_ALL=C sort
|
|
108
|
+
# 🟥 **디렉토리 존재 축** (2026-08-17). 위 블록은 `-type f` 라 **파일만** 센다 — 빈
|
|
109
|
+
# 디렉토리는 관측면에 아예 없었고, `mkdir -p uploads outputs tmp` 만 하는 capability 가
|
|
110
|
+
# `writes: read-only` 선언으로 **✅ VERIFIED** 를 받았다(known-pair 3-arm 실측).
|
|
111
|
+
# git 은 빈 디렉토리를 추적하지 않으므로 격리 클론 샌드박스에도 없다 — 즉 이건 warm
|
|
112
|
+
# cache 가 아니라 **관측면의 구멍**이다(원정 §2 의 진단을 이 실측이 정정했다).
|
|
113
|
+
# ★존재만 센다, mtime 은 안 센다 — 안쪽 파일이 바뀔 때마다 디렉토리 mtime 이 움직여
|
|
114
|
+
# 읽기전용 capability 가 거짓 VIOLATION 을 받는다.
|
|
115
|
+
# ★`.git` 제외 경계는 위 파일 축과 **같다**. 갈라지면 한쪽만 고쳐지는 구조가 된다.
|
|
116
|
+
find "$1" -type d -not -path '*/.git/*' -not -path '*/.git' 2>/dev/null \
|
|
117
|
+
| LC_ALL=C sort | sed 's/^/dir /'
|
|
118
|
+
# 🟥 **비정규 노드 축** (2026-08-17, cross-family codex/gpt-5.5 지목 → 실측 확인).
|
|
119
|
+
# `-type f` 도 `-type d` 도 symlink·fifo·소켓·디바이스 노드를 안 센다. 실측:
|
|
120
|
+
# entry `ln -sf /etc/passwd leaked_link` + writes: read-only → ✅ VERIFIED
|
|
121
|
+
# entry `mkfifo backdoor_pipe` + writes: read-only → ✅ VERIFIED
|
|
122
|
+
# 위 디렉토리 축을 더한 **뒤에도** 둘 다 통과했다 — 같은 결함 클래스가 남아 있었다.
|
|
123
|
+
# ★symlink 는 **대상까지** 잰다. 존재만 세면 같은 경로를 다른 곳으로 다시 거는
|
|
124
|
+
# `ln -sf other link` 가 무변화로 보인다 — 그게 이 축에서 가장 위험한 형태다.
|
|
125
|
+
# ★`stat -f`/`-printf` 를 안 쓴다 — 전자는 BSD 전용, 후자는 GNU 전용이라 한쪽 CI 에서
|
|
126
|
+
# 조용히 죽는다([[feedback_wiring_surfaces_hidden_failures]] 의 실측 클래스).
|
|
127
|
+
# `-type` 술어와 `readlink` 만 쓰면 양쪽에서 돈다.
|
|
128
|
+
find "$1" -type l -not -path '*/.git/*' 2>/dev/null | LC_ALL=C sort \
|
|
129
|
+
| while IFS= read -r _l; do
|
|
130
|
+
printf 'link %s -> %s\n' "$_l" "$(readlink "$_l" 2>/dev/null)"
|
|
131
|
+
done
|
|
132
|
+
find "$1" -not -path '*/.git/*' -not -path '*/.git' \
|
|
133
|
+
! -type f ! -type d ! -type l 2>/dev/null \
|
|
134
|
+
| LC_ALL=C sort | sed 's/^/node /'
|
|
108
135
|
# ⚠️ 명명된 한계: 개행이 든 파일명은 `find` 출력에서 쪼개진다. 이 프로브가 도입될 때부터
|
|
109
136
|
# 있던 성질이고 이번에 안 고쳤다 — 고치려면 `find -print0` 로 전 구간을 널 구분해야 한다.
|
|
110
137
|
}
|
|
@@ -683,6 +710,83 @@ EOF
|
|
|
683
710
|
fails=$((fails+1)); echo "❌ L15b rc=$rc (0 기대) — 정당한 비영 verdict 를 막는다"
|
|
684
711
|
fi
|
|
685
712
|
|
|
713
|
+
# ── L16/L16b 빈 디렉토리 생성은 «쓰기» 다 ────────────────────────────────────
|
|
714
|
+
# 2026-08-17 known-pair 3-arm 실측: `_snapshot` 이 `-type f` 라 **파일만** 셌고,
|
|
715
|
+
# `mkdir -p uploads outputs tmp` 만 하는 capability 가 `writes: read-only` 선언으로
|
|
716
|
+
# ✅ VERIFIED 를 받았다. git 이 빈 디렉토리를 추적하지 않으니 격리 클론에도 없어서
|
|
717
|
+
# before/after 어느 쪽에도 안 나타난다 — warm cache 가 아니라 관측면의 구멍이다.
|
|
718
|
+
# 과녁이 실물인 이유: qasp 의 static-review 진입점이 정확히 이 모양이다
|
|
719
|
+
# (`settings.py` 가 import 시점에 `ensure_dirs()` 를 무조건 실행).
|
|
720
|
+
cat > "$d/emptydirs.cap" <<EOF
|
|
721
|
+
id: t:emptydirs
|
|
722
|
+
entry: bash -c 'mkdir -p uploads outputs tmp'
|
|
723
|
+
requires_cwd: $_rs
|
|
724
|
+
writes: read-only
|
|
725
|
+
EOF
|
|
726
|
+
out=$(probe_one "$d/emptydirs.cap" 2>&1); rc=$?
|
|
727
|
+
if [ "$rc" = "1" ]; then
|
|
728
|
+
pass=$((pass+1)); echo "✅ L16 빈 디렉토리 생성은 read-only 선언을 위반한다"
|
|
729
|
+
else
|
|
730
|
+
fails=$((fails+1)); echo "❌ L16 rc=$rc (1 기대) — 빈 디렉토리 생성이 «no-write» 로 렌더된다"
|
|
731
|
+
fi
|
|
732
|
+
# L16b 컨트롤(과차단 방지): 재는 것은 **상태**지 활동이 아니다. 만들었다가 지워
|
|
733
|
+
# 순증이 0 이면 위반이 아니다 — 여기서 빨개지면 mkdir 자체를 벌하는 것이고,
|
|
734
|
+
# 그러면 임시 작업공간을 쓰는 정당한 read-only capability 가 전부 막힌다.
|
|
735
|
+
cat > "$d/dir_net_zero.cap" <<EOF
|
|
736
|
+
id: t:dirnetzero
|
|
737
|
+
entry: bash -c 'mkdir -p scratch_tmp && rmdir scratch_tmp'
|
|
738
|
+
requires_cwd: $_rs
|
|
739
|
+
writes: read-only
|
|
740
|
+
EOF
|
|
741
|
+
out=$(probe_one "$d/dir_net_zero.cap" 2>&1); rc=$?
|
|
742
|
+
if [ "$rc" = "0" ]; then
|
|
743
|
+
pass=$((pass+1)); echo "✅ L16b 컨트롤 — 순증 0 인 디렉토리 왕복은 위반이 아니다(상태를 잰다)"
|
|
744
|
+
else
|
|
745
|
+
fails=$((fails+1)); echo "❌ L16b rc=$rc (0 기대) — 활동을 벌한다(상태가 아니라)"
|
|
746
|
+
fi
|
|
747
|
+
|
|
748
|
+
# ── L17/L17b/L18 비정규 노드도 «쓰기» 다 ────────────────────────────────────
|
|
749
|
+
# L16 을 넣은 **뒤에도** 통과하던 형태다(실측): `-type f`·`-type d` 어느 쪽도 안 센다.
|
|
750
|
+
cat > "$d/symlink.cap" <<EOF
|
|
751
|
+
id: t:symlink
|
|
752
|
+
entry: bash -c 'ln -sf /etc/passwd leaked_link'
|
|
753
|
+
requires_cwd: $_rs
|
|
754
|
+
writes: read-only
|
|
755
|
+
EOF
|
|
756
|
+
out=$(probe_one "$d/symlink.cap" 2>&1); rc=$?
|
|
757
|
+
if [ "$rc" = "1" ]; then
|
|
758
|
+
pass=$((pass+1)); echo "✅ L17 symlink 생성은 read-only 선언을 위반한다"
|
|
759
|
+
else
|
|
760
|
+
fails=$((fails+1)); echo "❌ L17 rc=$rc (1 기대) — symlink 가 «no-write» 로 렌더된다"
|
|
761
|
+
fi
|
|
762
|
+
# L17b — **대상 재지정**. 존재만 세면 이게 무변화로 보인다: 경로도 타입도 그대로다.
|
|
763
|
+
# 이 축에서 가장 조용한 형태라 별도 레인으로 둔다.
|
|
764
|
+
mkdir -p "$_rs" 2>/dev/null; ln -sf /dev/null "$_rs/preexisting_link" 2>/dev/null
|
|
765
|
+
cat > "$d/relink.cap" <<EOF
|
|
766
|
+
id: t:relink
|
|
767
|
+
entry: bash -c 'ln -sf /etc/passwd preexisting_link'
|
|
768
|
+
requires_cwd: $_rs
|
|
769
|
+
writes: read-only
|
|
770
|
+
EOF
|
|
771
|
+
out=$(probe_one "$d/relink.cap" 2>&1); rc=$?
|
|
772
|
+
if [ "$rc" = "1" ]; then
|
|
773
|
+
pass=$((pass+1)); echo "✅ L17b 기존 symlink 의 **대상 재지정**도 위반이다(존재만 세지 않는다)"
|
|
774
|
+
else
|
|
775
|
+
fails=$((fails+1)); echo "❌ L17b rc=$rc (1 기대) — 대상을 안 재고 존재만 센다"
|
|
776
|
+
fi
|
|
777
|
+
cat > "$d/fifo.cap" <<EOF
|
|
778
|
+
id: t:fifo
|
|
779
|
+
entry: bash -c 'mkfifo backdoor_pipe'
|
|
780
|
+
requires_cwd: $_rs
|
|
781
|
+
writes: read-only
|
|
782
|
+
EOF
|
|
783
|
+
out=$(probe_one "$d/fifo.cap" 2>&1); rc=$?
|
|
784
|
+
if [ "$rc" = "1" ]; then
|
|
785
|
+
pass=$((pass+1)); echo "✅ L18 fifo/비정규 노드 생성도 위반이다"
|
|
786
|
+
else
|
|
787
|
+
fails=$((fails+1)); echo "❌ L18 rc=$rc (1 기대) — 비정규 노드가 감시면 밖이다"
|
|
788
|
+
fi
|
|
789
|
+
|
|
686
790
|
rm -rf "$d"
|
|
687
791
|
echo "── capability_effect_probe lanes: $pass PASS / $fails FAIL ──"
|
|
688
792
|
[ "$fails" = "0" ]
|
|
@@ -373,6 +373,13 @@ _check_one() {
|
|
|
373
373
|
if [ -z "$CAP_cal_pos_expect" ] || [ -z "$CAP_cal_neg_expect" ]; then
|
|
374
374
|
_fail "M4" "캘리브레이션 쌍 미선언(양성·음성 expect 둘 다 필요) — 답을 아는 케이스를 못 가르는 계기는 재는 게 아니다"
|
|
375
375
|
OBS_WHY="캘리브레이션 쌍 미선언"
|
|
376
|
+
# 🟥 위 «쌍 미선언» 검사는 **선언층**이므로 --declaration-only 에서도 돈다. 아래부터가
|
|
377
|
+
# **실행**이고, 그것만 건너뛴다. 둘을 한 덩어리로 스킵하면 «쌍을 아예 선언 안 한»
|
|
378
|
+
# capfile 이 선언층 검사를 통과해버린다 — 2026-08-17 실측에서 relay 가 삼킨
|
|
379
|
+
# `qasp_clean.cap` 이 정확히 그 값으로 걸렸다(M4 쌍 미선언).
|
|
380
|
+
elif [ "${DECL_ONLY:-0}" -eq 1 ]; then
|
|
381
|
+
printf ' ⏭ M4 — --declaration-only: 쌍 선언은 확인했고 **arm 은 실행하지 않았다**(SKIPPED, PASS 아님)\n'
|
|
382
|
+
OBS_WHY="--declaration-only 로 arm 미실행"
|
|
376
383
|
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M4:-}" ]; then
|
|
377
384
|
printf ' ⏭ M4 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
|
|
378
385
|
OBS_WHY="앞선 축 실패로 M4 생략"
|
|
@@ -417,6 +424,11 @@ _check_one() {
|
|
|
417
424
|
# «못 쟀다» 를 출력에 남긴다. 미측정을 0 으로 렌더하지 않는다.
|
|
418
425
|
if [ -z "$CAP_writes" ]; then
|
|
419
426
|
: # writes 미선언은 M2 가 이미 처리한다
|
|
427
|
+
elif [ "${DECL_ONLY:-0}" -eq 1 ]; then
|
|
428
|
+
# 🟥 M6 는 통째로 실행 기반이다(effect probe 가 entry 를 실제로 돌린다). 선언층에
|
|
429
|
+
# 남길 것이 없으므로 전부 건너뛴다 — 그래서 이 모드의 출력이 REGISTRABLE 이 아니라
|
|
430
|
+
# DECLARATION_VALID 다. `writes:` 의 진위는 이 모드에서 **등록자 주장**으로 남는다.
|
|
431
|
+
printf ' ⏭ M6 — --declaration-only: 진입점을 실행하지 않았다. `writes:` 는 미검증 주장이다(SKIPPED, PASS 아님)\n'
|
|
420
432
|
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M6:-}" ]; then
|
|
421
433
|
# ★앞선 축이 실패했으면 **진입점을 실행하지 않는다**(2026-08-16 cross-family 지목).
|
|
422
434
|
# M6 는 `eval` 로 entry 를 돌리므로, «등록 거부될 capfile» 의 진입점을 굳이 실행하는
|
|
@@ -619,6 +631,30 @@ LIAR
|
|
|
619
631
|
_tg "관측범위: 미선언 args 를 미선언이라 적는다" 1 "$T/nocal.cap" \
|
|
620
632
|
"실행된 arm: 양성 «<미선언>» / 음성 «<미선언>»"
|
|
621
633
|
|
|
634
|
+
# ── D1–D4 --declaration-only (2026-08-17, relay 배선용) ─────────────────────
|
|
635
|
+
# 이 모드는 `relay_channel.sh` 가 **호출 시점**에 부르는 경로다. 전량 호출하면 M4/M6 이
|
|
636
|
+
# 매 relay 실행마다 노드를 다시 돌리므로 선언층만 본다 — 그 대가를 레인으로 고정한다.
|
|
637
|
+
local dout drc
|
|
638
|
+
# D1 선언층 결함은 이 모드에서도 잡힌다(잡히는 게 요점이다 — 안 잡히면 배선이 무의미)
|
|
639
|
+
dout=$(bash "$0" --declaration-only "$T/nocal.cap" 2>&1); drc=$?
|
|
640
|
+
if [ "$drc" -eq 1 ]; then pass=$((pass+1)); printf ' ✅ %-40s\n' "D1: 선언층 결함은 --declaration-only 도 잡는다"
|
|
641
|
+
else fail=$((fail+1)); printf ' ❌ %-40s — rc=%s (기대 1)\n' "D1: 선언층 결함" "$drc"; fi
|
|
642
|
+
# D2 ★컨트롤 — 정상 선언은 통과한다. D1 만 있으면 「전부 막는 모드」와 구별이 안 된다
|
|
643
|
+
dout=$(bash "$0" --declaration-only "$T/good.cap" 2>&1); drc=$?
|
|
644
|
+
if [ "$drc" -eq 0 ]; then pass=$((pass+1)); printf ' ✅ %-40s\n' "D2: 컨트롤 — 정상 선언은 통과(과차단 아님)"
|
|
645
|
+
else fail=$((fail+1)); printf ' ❌ %-40s — rc=%s (기대 0)\n' "D2: 컨트롤" "$drc"; fi
|
|
646
|
+
# D3 🟥 어휘 분리 — 부분 검사를 REGISTRABLE 로 찍으면 안 된다. 그게 이 모드의 최대 위험이다
|
|
647
|
+
case "$dout" in
|
|
648
|
+
*DECLARATION_VALID*) pass=$((pass+1)); printf ' ✅ %-40s\n' "D3: REGISTRABLE 이 아니라 DECLARATION_VALID" ;;
|
|
649
|
+
*) fail=$((fail+1)); printf ' ❌ %-40s — 부분 검사가 전체 통과 어휘로 찍힌다\n' "D3: 어휘 분리" ;;
|
|
650
|
+
esac
|
|
651
|
+
# D4 M4/M6 을 «안 돌렸다» 고 말한다. 침묵하면 읽는 사람이 PASS 로 읽는다(미측정≠0)
|
|
652
|
+
case "$dout" in
|
|
653
|
+
*"M4 — --declaration-only"*|*"M6 — --declaration-only"*)
|
|
654
|
+
pass=$((pass+1)); printf ' ✅ %-40s\n' "D4: 안 돌린 축을 안 돌렸다고 적는다" ;;
|
|
655
|
+
*) fail=$((fail+1)); printf ' ❌ %-40s — 생략을 침묵으로 처리한다\n' "D4: 미실행 표기" ;;
|
|
656
|
+
esac
|
|
657
|
+
|
|
622
658
|
printf '\n 통과 %d · 실패 %d\n' "$pass" "$fail"
|
|
623
659
|
rm -rf "$T"
|
|
624
660
|
[ "$fail" -eq 0 ] || return 1
|
|
@@ -626,14 +662,44 @@ LIAR
|
|
|
626
662
|
}
|
|
627
663
|
|
|
628
664
|
[ "${1:-}" = "--self-test" ] && { _self_test; exit $?; }
|
|
629
|
-
[ $# -ge 1 ] || _die "capfile 인자가 없다. 사용법: $0 <capfile> [<capfile>...]"
|
|
630
665
|
|
|
631
|
-
|
|
666
|
+
# ── --declaration-only (2026-08-17) ───────────────────────────────────────────
|
|
667
|
+
# 🟥 **왜 이 모드가 있나 — 그리고 왜 「가벼운 등록검사」가 아닌가.**
|
|
668
|
+
# `relay_channel.sh` 가 호출 시점에 이 검사기를 부르게 배선하면서 필요해졌다. 그냥 전량
|
|
669
|
+
# 호출하면 **매 relay 실행마다 M4(known-pair arm 을 실제로 돌린다)와 M6(effect probe 가
|
|
670
|
+
# entry 를 `eval` 로 실행한다)가 재실행된다** — 호출 한 번이 노드를 여러 번 더 돌리고,
|
|
671
|
+
# M6 는 그 자체가 위험 노출이라 이 파일이 이미 «등록 거부될 capfile 의 진입점을 굳이
|
|
672
|
+
# 실행하지 않는다» 는 가드를 갖고 있다. 그 판단을 호출 경로에도 그대로 적용한다.
|
|
673
|
+
#
|
|
674
|
+
# **대안을 안 고른 이유**: relay 안에 선언 검사를 새로 쓰는 것. 그러면 같은 스키마에 대해
|
|
675
|
+
# 계기가 셋이 되고, 이 레포는 그 divergent-normalizer 함정을 **이미 두 번 밟았다**
|
|
676
|
+
# (relay 헤더 §META_KEYS — 2026-08-11 · 2026-08-16, 둘 다 «등록되는데 부를 수 없는» 선언).
|
|
677
|
+
# 스키마의 단일 소스는 이 파일이어야 한다.
|
|
678
|
+
#
|
|
679
|
+
# 🟥 **이 모드는 REGISTRABLE 을 말하지 않는다.** 출력 어휘를 일부러 분리한다 —
|
|
680
|
+
# `DECLARATION_VALID` 는 «선언층이 유효하다» 이지 «등록 가능하다» 가 아니다. 둘을 같은
|
|
681
|
+
# 단어로 찍으면 부분 검사가 전체 통과로 읽힌다(`[[feedback_not_found_is_not_zero_family]]`).
|
|
682
|
+
DECL_ONLY=0
|
|
683
|
+
if [ "${1:-}" = "--declaration-only" ]; then DECL_ONLY=1; shift; fi
|
|
684
|
+
export DECL_ONLY
|
|
685
|
+
|
|
686
|
+
[ $# -ge 1 ] || _die "capfile 인자가 없다. 사용법: $0 [--declaration-only] <capfile> [<capfile>...]"
|
|
687
|
+
|
|
688
|
+
if [ "$DECL_ONLY" -eq 1 ]; then
|
|
689
|
+
printf 'capability_registry_check — **선언층만** (M1·M2·M3·M5 + 추가조항 · M4/M6 미실행)\n'
|
|
690
|
+
else
|
|
691
|
+
printf 'capability_registry_check — M1–M6 + 추가조항 (등록 시점)\n'
|
|
692
|
+
fi
|
|
632
693
|
for f in "$@"; do _check_one "$f"; done
|
|
633
694
|
|
|
634
695
|
printf '\n'
|
|
635
696
|
if [ "$FAILED" -eq 0 ]; then
|
|
636
|
-
|
|
697
|
+
if [ "$DECL_ONLY" -eq 1 ]; then
|
|
698
|
+
printf '✅ DECLARATION_VALID — 선언층 통과. 🟥 REGISTRABLE 이 아니다: M4(known-pair 실행)·M6(선언 진위)는 **안 돌렸다**\n'
|
|
699
|
+
else
|
|
700
|
+
printf '✅ REGISTRABLE — 전 capfile 이 M1–M6 + 추가조항 통과\n'
|
|
701
|
+
fi
|
|
702
|
+
exit "$RC_OK"
|
|
637
703
|
else
|
|
638
704
|
printf '❌ REJECTED — 등록 불가. 그 표면은 오늘 있던 자리(dispatch 엔트리)에 그대로 남는다 — 손실이 아니다\n'
|
|
639
705
|
exit "$RC_REJECT"
|
package/scripts/relay_channel.sh
CHANGED
|
@@ -21,10 +21,20 @@
|
|
|
21
21
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
22
|
# 이 계기가 증명하지 않는 것 (과잉주장 금지 — 명명된 잔여)
|
|
23
23
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
-
# · **등록
|
|
25
|
-
# `scripts/capability_registry_check.sh`
|
|
26
|
-
#
|
|
27
|
-
#
|
|
24
|
+
# · ✅ **등록 시점 선언층은 2026-08-17 부터 여기서 막는다(배선됨).** `do_run` 이
|
|
25
|
+
# `_registry_gate` 로 `scripts/capability_registry_check.sh --declaration-only` 를 부르고,
|
|
26
|
+
# REJECTED 면 조합을 열지 않는다(COMPOSITION_VIOLATION).
|
|
27
|
+
# 🟥 **남은 잔여는 좁아졌을 뿐 사라지지 않았다**: 그 모드는 M4(known-pair 실행)·M6(선언
|
|
28
|
+
# 진위)를 **안 돌린다**. 즉 여기서 닫히는 것은 «선언이 형식에 맞나»(M1·M2·M3·M5 +
|
|
29
|
+
# 추가조항)이고, «선언이 **진실인지**»는 여전히 검사하지 않는다 — `writes: read-only`
|
|
30
|
+
# 의 진위는 등록 시점 M6 의 몫이다. 왜 전량 호출을 안 했는지는 `_registry_gate` 헤더에.
|
|
31
|
+
#
|
|
32
|
+
# · (이력) 🟥 **이 자리에 이렇게 적혀 있었다 — 그리고 그 문장이 잔여를 은폐했다.**
|
|
33
|
+
# 원문: *"`scripts/capability_registry_check.sh`(M1–M5 + M4 쌍 실행)는 **여전히 없다**."*
|
|
34
|
+
# 2026-08-17 실측 시점에 그 파일은 **42KB 로 실재했고 정상 작동했다.** 낡은 서술이
|
|
35
|
+
# «없으니 안 부르는 게 당연하다» 를 정당화하는 형태로 남아 있었고, 그 사이 relay 는
|
|
36
|
+
# `❌ REJECTED` 판정을 받는 capfile 을 그냥 실행해 CLEAN 을 체인에 병합했다.
|
|
37
|
+
# **stale 한 잔여 서술은 잔여를 기록하는 게 아니라 은폐한다.**
|
|
28
38
|
# · **호출 순간 자체엔 기계 floor 가 없다.** "지금 capability 를 조합하려 한다" 를
|
|
29
39
|
# 관측하는 훅은 존재하지 않는다(트리거가 의도다). 이 스크립트는 *호출되면* 계산을
|
|
30
40
|
# 강제하지만, **호출되도록 강제하지는 못한다.** 스펙 §1 이 이미 그렇게 적어놨고
|
|
@@ -507,6 +517,73 @@ _invoke_node() {
|
|
|
507
517
|
return 0
|
|
508
518
|
}
|
|
509
519
|
|
|
520
|
+
# ── 등록 시점 게이트 ──────────────────────────────────────────────────────────
|
|
521
|
+
# 호출 시점(여기)에서 **등록 시점 검사기를 그대로 부른다.** 선언 검사를 여기 새로 쓰지
|
|
522
|
+
# 않는 것이 요점이다 — 같은 스키마에 계기가 둘이 되면 관대함이 갈리고, 이 파일은 그
|
|
523
|
+
# divergent-normalizer 함정을 **이미 두 번 밟았다**(§META_KEYS 헤더: 2026-08-11 · 08-16,
|
|
524
|
+
# 둘 다 «등록되는데 부를 수 없는» 선언을 만들었다). 스키마의 단일 소스는 검사기 쪽이다.
|
|
525
|
+
#
|
|
526
|
+
# 🟥 **`--declaration-only` 로 부르는 이유 — 그리고 그 대가를 명시한다.**
|
|
527
|
+
# 전량 호출하면 M4(known-pair arm 실행)와 M6(effect probe 가 entry 를 실행)가 **매 relay
|
|
528
|
+
# 호출마다 재실행된다.** 검사기 자신이 «등록 거부될 capfile 의 진입점을 굳이 실행하는 것은
|
|
529
|
+
# 그 자체가 위험 노출» 이라는 가드를 갖고 있고, 그 판단은 호출 경로에서도 같다.
|
|
530
|
+
# ⇒ **대가**: 이 게이트는 «선언이 형식에 맞나» 를 보지 «선언이 사실인가» 는 **안 본다.**
|
|
531
|
+
# `writes: read-only` 의 진위는 여기서 닫히지 않는다(그건 등록 시점 M6 의 몫이다).
|
|
532
|
+
# 이 파일 헤더가 원래 적어둔 잔여 — «선언이 진실인지는 검사하지 않는다» — 는
|
|
533
|
+
# **좁아졌을 뿐 사라지지 않았다.** 좁아진 폭이 정확히 M1·M2·M3·M5 + 추가조항이다.
|
|
534
|
+
#
|
|
535
|
+
# **degrade 방향 = fail-closed.** 검사기 부재·실행 실패·알 수 없는 rc 는 전부 차단이다.
|
|
536
|
+
# 이건 조합을 여는 게이트이고, 게이트의 도구가 없을 때 통과시키는 것은 게이트가 아니다
|
|
537
|
+
# (CLAUDE.md §Surface-Class Degrade Invariant). 과차단 위험은 인정하되, 여기서 열면
|
|
538
|
+
# 배선의 목적 자체가 사라진다 — 안 부르던 상태와 같아지기 때문이다.
|
|
539
|
+
_registry_gate() {
|
|
540
|
+
# 심볼릭 링크 해석. `${0%/*}` 는 **호출에 쓰인 이름**의 디렉터리를 주므로, 이 파일로의
|
|
541
|
+
# 심링크가 있으면 링크 옆(아무것도 없는 곳)을 뒤지고 «검사기 부재» 로 fail-closed 한다.
|
|
542
|
+
# 자기 경로 버그를 남의 파일 탓으로 돌리는 게이트는 그냥 실패하는 것보다 나쁘다 —
|
|
543
|
+
# 읽는 사람을 틀린 수리로 보낸다(검사기 M6 이 2026-08-16 에 실제로 밟은 형태).
|
|
544
|
+
local _src="${BASH_SOURCE[0]}" _dir
|
|
545
|
+
while [ -L "$_src" ]; do
|
|
546
|
+
_dir=$(cd -P "$(dirname "$_src")" && pwd)
|
|
547
|
+
_src=$(readlink "$_src")
|
|
548
|
+
case "$_src" in /*) ;; *) _src="$_dir/$_src" ;; esac
|
|
549
|
+
done
|
|
550
|
+
_dir=$(cd -P "$(dirname "$_src")" && pwd)
|
|
551
|
+
local checker="$_dir/capability_registry_check.sh"
|
|
552
|
+
|
|
553
|
+
if [ ! -r "$checker" ]; then
|
|
554
|
+
_violation "등록 검사기에 도달할 수 없다: $checker — fail-closed(조합을 열지 않는다)"
|
|
555
|
+
return 1
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
local f out rc bad=0
|
|
559
|
+
GATE_SHA=() # 게이트가 «본» 바이트의 해시. 실행 직전에 대조한다(TOCTOU).
|
|
560
|
+
for f in "${CAPS[@]}"; do
|
|
561
|
+
# 🟥 rc 는 **직접** 취한다. 파이프로 읽으면 필터의 rc 가 잡혀 REJECTED 가 0 으로 읽힌다
|
|
562
|
+
# — 이 레포가 이름 붙인 결함이고 D6(pipefail) 과 같은 뿌리다.
|
|
563
|
+
out=$(bash "$checker" --declaration-only "$f" 2>&1); rc=$?
|
|
564
|
+
case "$rc" in
|
|
565
|
+
# 🟥 **성공 출력을 버리지 않는다** (cross-family A급 수리). 초판은 `0) ;;` 로 통째로
|
|
566
|
+
# 폐기했고, 그 결과 `relay run` 출력에는 `FH_STATUS: SUCCESS` 만 남아 **호출자가
|
|
567
|
+
# «전체 검사가 통과했다»로 읽었다.** 실제로는 M4(known-pair 실행)·M6(선언 진위)를
|
|
568
|
+
# 안 돌린 선언층 통과다. 침묵은 «다 봤다»로 읽힌다 — 안 본 축을 이름으로 찍는다.
|
|
569
|
+
0) printf 'FH_REGISTRY_GATE: DECLARATION_VALID %s\n' "$(basename "$f")"
|
|
570
|
+
printf 'FH_REGISTRY_GATE_SKIPPED: M4 M6 (%s — 선언 진위는 등록 시점 검사의 몫)\n' "$(basename "$f")"
|
|
571
|
+
# 🟥 **검사한 바이트를 실행할 바이트에 결박한다** (cross-family A급 — TOCTOU).
|
|
572
|
+
# 게이트는 capfile «경로» 를 검사하고, 그 뒤 `_merge_caps`·`_invoke_node` 가 같은
|
|
573
|
+
# 경로를 **다시 읽는다.** 그 사이 파일이 바뀌면 «검사된 선언» 과 «실행된 선언» 이
|
|
574
|
+
# 다르고, 초판엔 둘을 묶는 것이 없었다. 여기서 해시를 남기고 실행 직전에 대조한다.
|
|
575
|
+
GATE_SHA+=("$(_sha < "$f")") ;;
|
|
576
|
+
1) _violation "등록 시점 검사 REJECTED — 부를 수 없는 선언이다: $f"
|
|
577
|
+
printf '%s\n' "$out" | sed -n 's/^ ❌ / ↳ /p' >&2
|
|
578
|
+
bad=1 ;;
|
|
579
|
+
*) _violation "등록 검사기가 비정상 종료(rc=$rc): $f — fail-closed"
|
|
580
|
+
printf '%s\n' "$out" | tail -3 >&2
|
|
581
|
+
bad=1 ;;
|
|
582
|
+
esac
|
|
583
|
+
done
|
|
584
|
+
[ "$bad" -eq 0 ]
|
|
585
|
+
}
|
|
586
|
+
|
|
510
587
|
# ── run ───────────────────────────────────────────────────────────────────────
|
|
511
588
|
do_run() {
|
|
512
589
|
local task="" recdir="" ack=0 n=0 upstream="" rc final=$RC_PASS _ci
|
|
@@ -572,6 +649,41 @@ do_run() {
|
|
|
572
649
|
local ncap=${#CAPS[@]}
|
|
573
650
|
[ "$ncap" -ge 2 ] || _die "relay 는 노드 2개 이상을 요구한다(받은 수: $ncap) — 1노드는 단순 호출이지 경유가 아니다"
|
|
574
651
|
|
|
652
|
+
# ── 등록 시점 게이트 배선 (2026-08-17) ──────────────────────────────────────
|
|
653
|
+
# 🟥 **왜 여기가 필요했나 — 이 파일이 게이트를 갖고도 안 불렀다.**
|
|
654
|
+
# 2026-08-17 실측: `capability_registry_check.sh qasp_clean.cap` → `❌ REJECTED`
|
|
655
|
+
# (M2+ enum 에 「안 돌았다」 없음 · M5 requires_cwd 미선언 · M4 캘리브레이션 쌍 미선언).
|
|
656
|
+
# **그런데 relay 는 그 cap 을 그냥 실행하고 CLEAN 을 체인에 병합했다** — grep 결과
|
|
657
|
+
# `capability_registry_check` 히트 3건이 **전부 주석**이고 실행문은 0줄이었다.
|
|
658
|
+
# 게이트는 42KB 로 실재하고 정상 작동하는데 **아무도 부르지 않는** 상태였다
|
|
659
|
+
# ([[feedback_built_but_not_wired]] — 「호출부 미배선이면 산문이 실행기」).
|
|
660
|
+
# 그 결과가 §2-b 의 «relay 3노드 CLEAN 동의»이고, 그 동의는 허상이었다.
|
|
661
|
+
# 🟥 **우회 채널이 있고, 왜 있는지 적는다 — 이걸 숨기면 그게 더 나쁘다.**
|
|
662
|
+
# 이 게이트를 켜자 relay 자신의 **런타임 심층 방어 4종**이 do_run 경로에서 도달 불가가 됐다
|
|
663
|
+
# (실측: L26b «126 이상» · L31b «중복 선언» · L36b 실행 불가 · N8 requires_cwd 부재).
|
|
664
|
+
# 구조적으로 배타적이다 — entry 가 실행 불가여야 126 이 나는데 그건 M1 이 먼저 잡고,
|
|
665
|
+
# requires_cwd 가 없어야 NOT_CALLABLE 인데 그건 M5 가 먼저 잡는다.
|
|
666
|
+
# 그 방어들은 **죽은 코드가 아니라 심층 방어**다: 게이트를 안 거치는 진입점, 게이트가
|
|
667
|
+
# fail-closed 로 죽은 뒤의 경로에서 여전히 마지막 벽이다. 시험할 수 없으면 썩는다
|
|
668
|
+
# ([[feedback_built_but_not_wired]] 의 거울상 — 배선했더니 아래층이 안 보이게 된 형태).
|
|
669
|
+
# ⇒ 끄는 길을 **하나만**, **시끄럽게** 둔다. 조용한 우회가 실사용으로 새는 것이지
|
|
670
|
+
# 존재 자체가 새는 게 아니다. 이 줄이 실사용 로그에 보이면 그게 결함 신호다.
|
|
671
|
+
if [ "${FH_RELAY_REGISTRY_GATE:-on}" = "off" ]; then
|
|
672
|
+
# 🟥 **우회는 실행을 막지 않지만 clearing PASS 는 구조적으로 막는다** (cross-family A급 수리).
|
|
673
|
+
# 초판은 경고만 stderr 로 찍었는데, 재현 결과 **REJECT capfile 이 실제로 실행되고
|
|
674
|
+
# `FH_RELAY_VERDICT: PASS` 까지 갔다.** 경고는 텍스트일 뿐 게이트가 아니다.
|
|
675
|
+
# ⇒ `DEGRADED_SEEN` 과 같은 형태로 **verdict 를 강등**한다. 심층 방어 레인들은 rc=2/4 를
|
|
676
|
+
# 기대하므로 안 깨지고, 실사용자가 이 스위치를 켜도 **게이트를 열지는 못한다.**
|
|
677
|
+
# («이중 조건 env» 대신 이걸 고른 이유: 조건을 늘려도 둘 다 켜면 그만이고, 그건 여전히
|
|
678
|
+
# PASS 를 낸다. 여기서 막아야 하는 것은 진입이 아니라 **통과**다.)
|
|
679
|
+
GATE_BYPASSED=1
|
|
680
|
+
printf '⚠️ 등록 게이트 우회 중 (FH_RELAY_REGISTRY_GATE=off) — 심층 방어 시험용 경로다.\n' >&2
|
|
681
|
+
printf '⚠️ 실사용에서 이 줄이 보이면 결함이다: 부를 수 없는 선언이 조합에 들어온다.\n' >&2
|
|
682
|
+
printf '⚠️ 이 런은 clearing PASS 를 낼 수 없다(강등 고정).\n' >&2
|
|
683
|
+
else
|
|
684
|
+
_registry_gate || { printf 'FH_STATUS: ERROR\nFH_RELAY_VERDICT: COMPOSITION_VIOLATION\n'; return "$RC_VIOLATION"; }
|
|
685
|
+
fi
|
|
686
|
+
|
|
575
687
|
_merge_caps || { printf 'FH_STATUS: ERROR\nFH_RELAY_VERDICT: COMPOSITION_VIOLATION\n'; return "$RC_VIOLATION"; }
|
|
576
688
|
_run_checks || { printf 'FH_STATUS: ERROR\nFH_RELAY_VERDICT: COMPOSITION_VIOLATION\n'; return "$RC_VIOLATION"; }
|
|
577
689
|
|
|
@@ -600,6 +712,21 @@ do_run() {
|
|
|
600
712
|
: > "$recdir/CHAIN.txt"
|
|
601
713
|
printf 'task_sha: %s\n' "$(printf '%s' "$task" | _sha)" >> "$recdir/CHAIN.txt"
|
|
602
714
|
|
|
715
|
+
# 🟥 **TOCTOU 재확인** — 게이트가 검사한 바이트와 지금 실행할 바이트가 같은지 본다.
|
|
716
|
+
# 게이트를 우회한 런(GATE_SHA 비어 있음)은 대조할 기준이 없으므로 건너뛴다 — 그 런은
|
|
717
|
+
# 이미 clearing PASS 를 못 내도록 강등돼 있다.
|
|
718
|
+
if [ "${#GATE_SHA[@]}" -gt 0 ]; then
|
|
719
|
+
local _i=0 _now
|
|
720
|
+
for f in "${CAPS[@]}"; do
|
|
721
|
+
_now=$(_sha < "$f")
|
|
722
|
+
if [ "$_now" != "${GATE_SHA[$_i]}" ]; then
|
|
723
|
+
printf 'FH_STATUS: ERROR\nFH_RELAY_VERDICT: HARNESS_ERROR\n'
|
|
724
|
+
_die "capfile 이 검사 후 변경됐다: $f — 검사한 선언과 실행할 선언이 다르다(TOCTOU)"
|
|
725
|
+
fi
|
|
726
|
+
_i=$((_i+1))
|
|
727
|
+
done
|
|
728
|
+
fi
|
|
729
|
+
|
|
603
730
|
for f in "${CAPS[@]}"; do
|
|
604
731
|
n=$((n+1))
|
|
605
732
|
_invoke_node "$f" "$task" "$upstream" "$recdir" "$n" "${CAPARGS[$((n-1))]:-}"
|
|
@@ -679,7 +806,14 @@ do_run() {
|
|
|
679
806
|
# D2 — 체인에 judge:model 이 있으면 PASS 는 **단독으로 게이트를 열 수 없다.**
|
|
680
807
|
# D4 연장 — advisory 로 넘긴 HARNESS_ERROR 도 clearing PASS 를 영구히 막는다.
|
|
681
808
|
# "안 재고 통과" 를 초록으로 렌더하지 않는다(미측정≠PASS).
|
|
682
|
-
if [ "${
|
|
809
|
+
if [ "${GATE_BYPASSED:-0}" -eq 1 ]; then
|
|
810
|
+
# 🟥 등록 게이트를 우회한 런은 **어떤 경우에도 clearing PASS 가 아니다.**
|
|
811
|
+
# 선언이 부를 수 있는 것인지 확인하지 않았으므로 «안 재고 통과» 이고, 이 파일의 D4 가
|
|
812
|
+
# 이미 그것을 PASS 로 렌더하지 않기로 정했다(미측정 ≠ PASS).
|
|
813
|
+
printf 'FH_RELAY_VERDICT: PENDING\n'
|
|
814
|
+
printf 'FH_RELAY_CLEARING: NON_CLEARING (등록 게이트 우회 — 선언 유효성을 안 쟀다. 미측정은 PASS 가 아니다)\n'
|
|
815
|
+
final=$RC_PENDING
|
|
816
|
+
elif [ "${DEGRADED_SEEN:-0}" -eq 1 ]; then
|
|
683
817
|
printf 'FH_RELAY_VERDICT: PENDING\n'
|
|
684
818
|
printf 'FH_RELAY_CLEARING: NON_CLEARING (HARNESS_ERROR 를 advisory 로 통과시켰다 — 미측정은 PASS 가 아니다)\n'
|
|
685
819
|
final=$RC_PENDING
|
|
@@ -702,7 +836,18 @@ do_merge_only() {
|
|
|
702
836
|
[ "${#CAPS[@]}" -gt 0 ] || _die "usage: merge --cap <capfile> [--cap ...]"
|
|
703
837
|
_merge_caps || return "$RC_VIOLATION"
|
|
704
838
|
_run_checks || return "$RC_VIOLATION"
|
|
705
|
-
|
|
839
|
+
# 🟥 **`merge` 는 등록 게이트를 부르지 않는다 — 그리고 그 사실을 출력이 말한다**
|
|
840
|
+
# (cross-family A급 수리). 재현: 같은 capfile 이 `capability_registry_check --declaration-only`
|
|
841
|
+
# 에서 rc=1 인데 `relay merge` 에서는 rc=0 `FH_STATUS: SUCCESS` 였다. 실행은 아니지만
|
|
842
|
+
# **preflight/조합 계산 표면으로 쓰이면 fail-open** 이다.
|
|
843
|
+
# ⇒ 게이트를 강제하지 **않는** 이유: `merge` 는 «병합만 계산» 하는 조회 경로라 등록 전
|
|
844
|
+
# 선언을 실험하는 정당한 용도가 있다. 막을 곳은 진입이 아니라 **오독**이다.
|
|
845
|
+
# ⇒ 그래서 `SUCCESS` 뒤에 **무엇을 안 봤는지**를 기계 필드로 찍는다. 침묵하면 호출자가
|
|
846
|
+
# «조합이 유효하다»로 읽는다 — 이 파일이 D4 에서 이미 정한 «미측정 ≠ PASS» 와 같은 규율.
|
|
847
|
+
printf 'FH_STATUS: SUCCESS\n'
|
|
848
|
+
printf 'FH_MERGE_SCOPE: SCHEMA_ONLY (병합 계산만 — 등록 시점 검사 미실행)\n'
|
|
849
|
+
printf 'FH_REGISTRY_GATE: NOT_RUN (이 경로는 capability_registry_check 를 부르지 않는다. `run` 에서만 검사한다)\n'
|
|
850
|
+
_print_merge
|
|
706
851
|
}
|
|
707
852
|
|
|
708
853
|
case "${1:-}" in
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -527,6 +527,9 @@ for _pair in \
|
|
|
527
527
|
"scripts/knowledge_seam_check.sh|scripts/test_knowledge_seam_lanes.sh" \
|
|
528
528
|
"templates/.git-hooks/pre-commit|scripts/test_marker_crossfamily_lanes.sh" \
|
|
529
529
|
"templates/.git-hooks/pre-commit|scripts/test_marker_floor_lanes.sh" \
|
|
530
|
+
".github/workflows/regression-guard.yml|scripts/test_regression_guard_ci_lanes.sh" \
|
|
531
|
+
"templates/.git-hooks/pre-commit|scripts/test_marker_standpoint_lanes.sh" \
|
|
532
|
+
"templates/.git-hooks/pre-commit|scripts/test_marker_thirdparty_lanes.sh" \
|
|
530
533
|
"scripts/residency_closure_scan.py|scripts/test_residency_closure_lanes.sh" \
|
|
531
534
|
"scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh" \
|
|
532
535
|
"scripts/field_canon_preload.sh|scripts/test_field_canon_lanes.sh" \
|
|
@@ -958,6 +961,21 @@ else
|
|
|
958
961
|
fail=1
|
|
959
962
|
fi
|
|
960
963
|
|
|
964
|
+
# target_freeze — 같은 형태(shipped subject + 자기 앵커). 🟥 이 블록이 없어서 `lane-runner`
|
|
965
|
+
# 검사가 «돌리는 게 없는 스위트는 산문이다» 로 정확히 걸었다. 지은 사람이 자기 레인을 안
|
|
966
|
+
# 배선한 것이고, 오늘 이 세션이 남의 코드에서 네 번 잡은 그 얼굴이다.
|
|
967
|
+
if [ ! -f scripts/target_freeze.sh ]; then
|
|
968
|
+
echo "FAIL scripts/target_freeze.sh is in package.json files[] but absent — a deleted subject, not a skip"
|
|
969
|
+
fail=1
|
|
970
|
+
elif [ -f scripts/test_target_freeze_lanes.sh ]; then
|
|
971
|
+
if ! bash scripts/test_target_freeze_lanes.sh; then
|
|
972
|
+
fail=1
|
|
973
|
+
fi
|
|
974
|
+
else
|
|
975
|
+
echo "FAIL test_target_freeze_lanes.sh: target_freeze.sh present but its anchor is missing"
|
|
976
|
+
fail=1
|
|
977
|
+
fi
|
|
978
|
+
|
|
961
979
|
# And the ablation calibrator, for the third instance of the same reason. Its whole job is telling
|
|
962
980
|
# apart states that look identical from outside — "the arm could not answer" vs "the runner is dead"
|
|
963
981
|
# vs "the runner read the answer off disk" — and round 2 of its own adversarial review found that
|