@chrono-meta/fh-gate 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/fh_4axis_gate.md +25 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +107 -7
- 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/fh_three_layer_canon.md +18 -6
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +262 -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/harness-core/sonnet_floor_doctrine.md +16 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +120 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +124 -2
- package/package.json +8 -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 +105 -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/mate_agent_boundary.sh +27 -7
- package/scripts/capability_effect_probe.sh +173 -5
- package/scripts/capability_registry_check.sh +69 -3
- package/scripts/digest_landing_check.sh +168 -13
- package/scripts/fh_session_load.sh +71 -0
- package/scripts/relay_channel.sh +151 -6
- package/scripts/selfcheck.sh +47 -0
- package/scripts/target_freeze.sh +239 -0
- package/scripts/test_adapter_lanes.sh +17 -0
- package/scripts/test_evidence_root_psa_lanes.sh +78 -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_satellite_publish_gate_lanes.sh +339 -0
- package/scripts/test_target_freeze_lanes.sh +203 -0
- package/templates/.git-hooks/pre-commit +306 -6
- package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
- package/templates/regression_guard.sh +32 -4
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_regression_guard_ci_lanes.sh — regression fixtures for Axis 1's CI path.
|
|
3
|
+
#
|
|
4
|
+
# WHY (measured 2026-08-17, and this is a production fail-open that ran silently):
|
|
5
|
+
# `Axis 1 — Backward Regression Check` reported a GREEN check on every PR it triggered on, while
|
|
6
|
+
# never once comparing anything. Two independent faults, both required:
|
|
7
|
+
# ① templates/regression_guard.sh --pr took a bare BRANCH NAME and ran `git merge-base main
|
|
8
|
+
# <branch>`. In a GitHub Actions PR checkout neither name resolves — actions/checkout lands
|
|
9
|
+
# on a DETACHED HEAD and creates refs/remotes/origin/*, not local branches. `fetch-depth: 0`
|
|
10
|
+
# was already set and is NOT the cause: the history was there, the NAMES were not. → exit 3.
|
|
11
|
+
# ② the workflow's Evaluate step was `2 -> block · 1 -> warn · else -> "PASS"`, so exit 3
|
|
12
|
+
# ("could not run") fell into `else` and printed a green PASS.
|
|
13
|
+
# Sample: 5 of 5 runs inspected (4 historical + the one that surfaced it) hit ① and rendered ②.
|
|
14
|
+
# Scope stated honestly: 5/5 SAMPLED, not an exhaustive audit of every run ever.
|
|
15
|
+
#
|
|
16
|
+
# ② is the load-bearing half. Fixing ① alone leaves the next instrument error just as green,
|
|
17
|
+
# because `else` swallows every code nobody enumerated.
|
|
18
|
+
#
|
|
19
|
+
# These lanes assert BOTH directions, and include a REVERT PROBE: the fail-closed branch is
|
|
20
|
+
# neutralized and the suite must go red at exactly that lane — an anchor nobody has watched fail
|
|
21
|
+
# is an anchor nobody knows is alive.
|
|
22
|
+
#
|
|
23
|
+
# Usage: bash scripts/test_regression_guard_ci_lanes.sh Exit: 0 = all behave; 1 = regression.
|
|
24
|
+
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
27
|
+
WF="$REPO_ROOT/.github/workflows/regression-guard.yml"
|
|
28
|
+
GUARD="$REPO_ROOT/templates/regression_guard.sh"
|
|
29
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
30
|
+
FAIL=0
|
|
31
|
+
|
|
32
|
+
# ── instrument calibration ───────────────────────────────────────────────────
|
|
33
|
+
# Extract the workflow's verdict logic and PROVE it arrived. An empty extraction would let every
|
|
34
|
+
# lane "pass" against nothing — the exact class this suite exists to close.
|
|
35
|
+
# Extracted with stdlib only. PyYAML is NOT a declared dependency of this repo, and this suite
|
|
36
|
+
# runs inside selfcheck.sh — a missing module here would block unrelated legitimate commits with
|
|
37
|
+
# an error about the wrong thing (cross-family finding, 2026-08-17).
|
|
38
|
+
python3 - "$WF" > "$T/eval.sh" <<'PYX'
|
|
39
|
+
import sys, re
|
|
40
|
+
lines = open(sys.argv[1], encoding="utf-8").read().split("\n")
|
|
41
|
+
try:
|
|
42
|
+
i = next(k for k, l in enumerate(lines) if l.strip() == "- name: Evaluate result")
|
|
43
|
+
except StopIteration:
|
|
44
|
+
sys.exit(0) # emit nothing -> the calibration check below aborts loudly
|
|
45
|
+
j = next(k for k in range(i, len(lines)) if lines[k].strip().startswith("run:"))
|
|
46
|
+
indent = len(lines[j + 1]) - len(lines[j + 1].lstrip())
|
|
47
|
+
body = []
|
|
48
|
+
for l in lines[j + 1:]:
|
|
49
|
+
if l.strip() and (len(l) - len(l.lstrip())) < indent:
|
|
50
|
+
break
|
|
51
|
+
body.append(l[indent:])
|
|
52
|
+
run = "\n".join(body)
|
|
53
|
+
run = run.replace("${{ steps.guard.outputs.exit_code }}", "$EXIT_IN")
|
|
54
|
+
run = run.replace("${{ steps.guard.outputs.scanned }}", "$SCANNED_IN")
|
|
55
|
+
sys.stdout.write('EXIT="$EXIT_IN"\n' + re.sub(r"^EXIT=.*\n", "", run, count=1, flags=re.M))
|
|
56
|
+
PYX
|
|
57
|
+
if ! grep -q 'could NOT RUN' "$T/eval.sh"; then
|
|
58
|
+
echo "❌ HARNESS-ERROR — the workflow's Evaluate logic did not extract from $WF."
|
|
59
|
+
echo " Lanes below would measure an empty script. Aborting rather than reporting green."
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
verdict() { # $1=exit_code $2=scanned ; echoes rc + first line
|
|
64
|
+
local out rc
|
|
65
|
+
out=$(EXIT_IN="$1" SCANNED_IN="$2" bash "$T/eval.sh" 2>&1); rc=$?
|
|
66
|
+
printf '%s|%s' "$rc" "$(printf '%s' "$out" | head -1)"
|
|
67
|
+
}
|
|
68
|
+
lane() { # $1=id $2=exit $3=scanned $4=expect(ok|fail) $5=substring
|
|
69
|
+
local r; r=$(verdict "$2" "$3"); local rc="${r%%|*}" msg="${r#*|}"
|
|
70
|
+
local got=ok; [ "$rc" -ne 0 ] && got=fail
|
|
71
|
+
if [ "$got" = "$4" ] && printf '%s' "$msg" | grep -q "$5"; then
|
|
72
|
+
printf ' ✅ %-30s rc=%s %s\n' "$1" "$rc" "$(printf '%s' "$msg" | cut -c1-58)"
|
|
73
|
+
else
|
|
74
|
+
printf ' ❌ %-30s expected %s+/%s/, got rc=%s %s\n' "$1" "$4" "$5" "$rc" "$msg"; FAIL=1
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
echo "== Evaluate-step lanes =="
|
|
79
|
+
lane L1-clean-scanned 0 yes ok "PASS — scanned"
|
|
80
|
+
lane L2-skip-not-a-pass 0 no ok "NOT a pass"
|
|
81
|
+
lane L2b-unknown-typed 0 unknown fail "wrote no typed result"
|
|
82
|
+
lane L3-s-tier-warn 1 yes ok "warning"
|
|
83
|
+
lane L4-m-tier-block 2 yes fail "BLOCK"
|
|
84
|
+
lane L5-instrument-3 3 yes fail "could NOT RUN"
|
|
85
|
+
lane L6-unknown-99 99 yes fail "could NOT RUN"
|
|
86
|
+
lane L7-empty-output "" yes fail "could NOT RUN"
|
|
87
|
+
|
|
88
|
+
echo "== script lane — ref resolution under a CI-shaped checkout =="
|
|
89
|
+
# Real reproduction: a detached-HEAD clone with no local branch refs, which is what broke ①.
|
|
90
|
+
CL="$T/clone"
|
|
91
|
+
if git -C "$REPO_ROOT" clone -q --no-local "$REPO_ROOT" "$CL" 2>/dev/null; then
|
|
92
|
+
git -C "$CL" checkout -q --detach HEAD
|
|
93
|
+
# 🟥 The first version of this lane claimed "no local branch refs" and did NOT create that
|
|
94
|
+
# state — `git clone` makes one, so the resolver matched the FIRST candidate and the lane never
|
|
95
|
+
# exercised the CI shape it advertised (cross-family finding, 2026-08-17). Delete every local
|
|
96
|
+
# branch so only refs/remotes/* remain — what actions/checkout actually leaves behind.
|
|
97
|
+
for _b in $(git -C "$CL" for-each-ref --format='%(refname:short)' refs/heads); do
|
|
98
|
+
git -C "$CL" branch -q -D "$_b" >/dev/null 2>&1 || true # noqa: destructive-op (throwaway clone)
|
|
99
|
+
done
|
|
100
|
+
_left=$(git -C "$CL" for-each-ref --format='%(refname:short)' refs/heads | wc -l | tr -d ' ')
|
|
101
|
+
cp "$GUARD" "$CL/templates/regression_guard.sh"
|
|
102
|
+
# The head fixture is the VULNERABILITY ITSELF, not an arbitrary name: `main`. In this clone
|
|
103
|
+
# `origin/main` exists and local `main` does not — exactly a fork PR whose branch is called
|
|
104
|
+
# `main`. The old resolver guessed `main -> origin/main` and compared the base repo to itself.
|
|
105
|
+
# (An earlier fixture used `rev-parse --abbrev-ref HEAD`, which returns the literal string
|
|
106
|
+
# "HEAD" on a detached checkout — a ref that DOES resolve, so the lane asserted nothing. Caught
|
|
107
|
+
# by reproducing the CI shape locally rather than trusting the developer-machine green.)
|
|
108
|
+
BR=main
|
|
109
|
+
# Take every SHA from INSIDE the clone. A SHA read from the source repo is not guaranteed to
|
|
110
|
+
# exist in the clone: when the source is a detached checkout with no local branches (which is
|
|
111
|
+
# exactly what actions/checkout leaves), `git clone` has few refs to transfer from, so an
|
|
112
|
+
# arbitrary source SHA may simply be absent. Reading from the clone removes that assumption
|
|
113
|
+
# instead of hoping it holds — the previous version of this lane hoped, and CI disagreed.
|
|
114
|
+
SHA=$(git -C "$CL" rev-parse HEAD)
|
|
115
|
+
# 🟥 The BASE side must be a SHA too, and this cost a red CI to learn.
|
|
116
|
+
# The first version passed `origin/main` as the base. That resolves on a developer machine and
|
|
117
|
+
# NOT in CI: a GitHub Actions checkout is detached with no local `main`, so a clone of it has no
|
|
118
|
+
# `origin/main` either. Consequences, both measured on the same run:
|
|
119
|
+
# L8b failed — base unresolvable, so the SHA lane could never reach its assertion
|
|
120
|
+
# L8a "passed" FOR THE WRONG REASON — it expects exit 3, and got exit 3 from the BASE failing,
|
|
121
|
+
# not from the head NAME being refused. A green lane measuring something else.
|
|
122
|
+
# This file's own header warns that asserting only BLOCK cannot tell "blocked" from "blocked for
|
|
123
|
+
# the wrong reason". Holding exactly one variable is the fix: base is always a resolvable SHA,
|
|
124
|
+
# so any refusal is attributable to the head, and the assertion checks the head half by name.
|
|
125
|
+
# 🟥 `git rev-parse HEAD~1` prints the literal string "HEAD~1" ON STDOUT when it fails, so
|
|
126
|
+
# `$(... || fallback)` captures that string alongside the fallback. Measured in CI: the clone of
|
|
127
|
+
# a detached, branchless checkout has a single reachable commit, HEAD~1 does not exist, and
|
|
128
|
+
# BASE_SHA became the literal "HEAD~1" — which then failed to resolve and turned this lane red
|
|
129
|
+
# for a reason unrelated to what it tests. Same family as `|| echo 0` emitting "0\n0".
|
|
130
|
+
# Use --verify --quiet (silent on failure) and branch on emptiness.
|
|
131
|
+
BASE_SHA=$(git -C "$CL" rev-parse --verify --quiet 'HEAD~1^{commit}') || BASE_SHA=""
|
|
132
|
+
[ -n "$BASE_SHA" ] || BASE_SHA=$(git -C "$CL" rev-parse HEAD)
|
|
133
|
+
|
|
134
|
+
# L8a — a bare NAME must be REFUSED. Name-guessing is what let a fork PR branch called `main`
|
|
135
|
+
# resolve to this repo's main and compare it to itself.
|
|
136
|
+
out=$(cd "$CL" && bash templates/regression_guard.sh --pr "$BR" "$BASE_SHA" 2>&1); rc=$?
|
|
137
|
+
if [ "$rc" -eq 3 ] && printf '%s' "$out" | grep -q "head='$BR'->'<none>'"; then
|
|
138
|
+
printf ' ✅ %-30s fork-collision name '"'"'%s'"'"' refused, attributed to HEAD (local refs: %s)\n' "L8a-name-refused" "$BR" "$_left"
|
|
139
|
+
else
|
|
140
|
+
printf ' ❌ %-30s expected exit 3 naming head=%s, got rc=%s: %s\n' "L8a-name-refused" "$BR" "$rc" "$(printf '%s' "$out" | head -1)"; FAIL=1
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# L8b — unambiguous SHAs on both sides must resolve and compute a merge-base.
|
|
144
|
+
out=$(cd "$CL" && bash templates/regression_guard.sh --pr "$SHA" "$BASE_SHA" 2>&1); rc=$?
|
|
145
|
+
if printf '%s' "$out" | grep -q 'PR MODE: merge-base='; then
|
|
146
|
+
printf ' ✅ %-30s %s\n' "L8b-sha-resolves" "$(printf '%s' "$out" | grep -o 'merge-base=[^ ]*')"
|
|
147
|
+
else
|
|
148
|
+
printf ' ❌ %-30s SHA did not resolve (rc=%s): %s\n' "L8b-sha-resolves" "$rc" "$(printf '%s' "$out" | head -1)"; FAIL=1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
else
|
|
152
|
+
printf ' ⚠️ %-30s clone unavailable — lanes SKIPPED (not passed)\n' "L8-ci-shape"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo "== revert probe — is the fail-closed branch load-bearing, or decoration? =="
|
|
156
|
+
# Neutralize ONLY the catch-all failure branch; L5/L6/L7 must go red and nothing else may move.
|
|
157
|
+
sed 's/exit 1 ;;/: ;;/' "$T/eval.sh" > "$T/eval_neutered.sh"
|
|
158
|
+
if ! diff -q "$T/eval.sh" "$T/eval_neutered.sh" >/dev/null; then
|
|
159
|
+
r=$(EXIT_IN=3 SCANNED_IN=yes bash "$T/eval_neutered.sh" >/dev/null 2>&1; echo $?)
|
|
160
|
+
if [ "$r" -eq 0 ]; then
|
|
161
|
+
echo " ✅ revert probe neutralizing the catch-all makes exit 3 pass again → anchor is alive"
|
|
162
|
+
else
|
|
163
|
+
echo " ❌ revert probe neutralized branch STILL fails — the lane is not testing what it claims"; FAIL=1
|
|
164
|
+
fi
|
|
165
|
+
else
|
|
166
|
+
echo " ❌ revert probe neutralization was a no-op — the probe measured nothing"; FAIL=1
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
[ $FAIL -eq 0 ] && { echo "✅ all regression-guard CI lanes behave"; exit 0; }
|
|
170
|
+
echo "❌ regression-guard CI lane regression"; exit 1
|
|
@@ -53,10 +53,32 @@ else echo "upstream MISSING (got='"'"'$got'"'"' want='"'"'$EXPECT_UPSTREAM'"'"')
|
|
|
53
53
|
mk n_silent.sh '#!/usr/bin/env bash
|
|
54
54
|
echo "no verdict key here"; exit 0'
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
# 🟥 `9=DID_NOT_RUN` 은 등록 게이트의 추가조항(§ⓑ.4 B1)이 요구하는 「안 돌았다」 구분항이다.
|
|
57
|
+
# **9 를 내는 노드는 이 파일에 하나도 없다**(실측: 노드들이 쓰는 코드는 0·1·3·7·66) —
|
|
58
|
+
# 그래서 이 값을 더해도 기존 레인이 관측하는 것은 하나도 안 바뀐다. 굳이 안 쓰이는 코드를
|
|
59
|
+
# 고른 이유가 그것이다. 66(계기사망)이나 3 에 이름을 붙였으면 L5·L8 이 시험하던 의미가
|
|
60
|
+
# 조용히 달라졌을 것이다 — 계기가 대상을 바꾸는 형태.
|
|
61
|
+
ENUM='0=PASS 1=FAIL 7=ODD 9=DID_NOT_RUN'
|
|
57
62
|
|
|
58
63
|
cap() { # $1=filename $2..=lines
|
|
59
|
-
local f="$T/$1"; shift; : > "$f"; for l in "$@"; do printf '%s\n' "$l" >> "$f"; done
|
|
64
|
+
local f="$T/$1"; shift; : > "$f"; for l in "$@"; do printf '%s\n' "$l" >> "$f"; done
|
|
65
|
+
# ── 등록 게이트 필수 필드 자동 보강 (2026-08-17) ────────────────────────────
|
|
66
|
+
# 🟥 **왜 생겼나 — 그리고 이건 «테스트를 통과시키려는 조작»이 아니다.**
|
|
67
|
+
# `relay run` 이 `capability_registry_check.sh --declaration-only` 를 부르도록 배선하자
|
|
68
|
+
# 이 파일의 레인 **91개 중 51개가 rc=4(COMPOSITION_VIOLATION)로 무너졌다.** 원인은
|
|
69
|
+
# 배선 결함이 아니라 **fixture 가 현실의 capfile 이 아니었다는 것**이다: 「안 돌았다」를
|
|
70
|
+
# 뜻하는 enum 값도, `requires_cwd` 도, 캘리브레이션 쌍도 선언한 적이 없었다.
|
|
71
|
+
# ⇒ relay 는 **현실에서 등록될 수 없는 입력으로만 자기를 시험해왔다.**
|
|
72
|
+
# 보강은 fixture 를 현실 쪽으로 옮기는 것이지 게이트를 무르는 것이 아니다.
|
|
73
|
+
# (게이트를 우회하는 env 채널을 만드는 선택지는 **버렸다** — 우회 채널은 실사용으로 샌다.)
|
|
74
|
+
#
|
|
75
|
+
# ⚠️ **이미 선언된 키는 절대 안 건드린다.** 덮어쓰면 「enum 밖 값은 폴백하지 않는다」
|
|
76
|
+
# 「오타난 축을 무음 드롭하지 않는다」 같은 레인이 시험하려던 것 자체가 바뀐다 —
|
|
77
|
+
# 계기가 대상을 조용히 바꾸는 형태다. 그래서 전부 `grep -q || append` 다.
|
|
78
|
+
grep -q '^requires_cwd:' "$f" || printf 'requires_cwd: %s\n' "$T" >> "$f"
|
|
79
|
+
grep -q '^calibration_positive_expect:' "$f" || printf 'calibration_positive_expect: PASS\n' >> "$f"
|
|
80
|
+
grep -q '^calibration_negative_expect:' "$f" || printf 'calibration_negative_expect: FAIL\n' >> "$f"
|
|
81
|
+
printf '%s' "$f"
|
|
60
82
|
}
|
|
61
83
|
|
|
62
84
|
# ── L1 PASS arm — 2노드 전부 PASS, mechanical ─────────────────────────────────
|
|
@@ -198,7 +220,7 @@ _t "L14 1노드 호출을 relay 로 세지 않는다" 10 "$rc"
|
|
|
198
220
|
|
|
199
221
|
# ── L15 stdout-key 채널인데 키 부재 → HARNESS_ERROR (D4) ──────────────────────
|
|
200
222
|
A15=$(cap a15.cap "id: demo:a" "entry: bash $T/n_silent.sh" "verdict_channel: stdout-key" \
|
|
201
|
-
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL" \
|
|
223
|
+
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
202
224
|
"approval: auto" "reversibility: reversible" \
|
|
203
225
|
"residency: public" "degrade: fail-closed" "tier_floor: none" "writes: read-only" \
|
|
204
226
|
"judge: mechanical" "verdict_binding: FAIL")
|
|
@@ -227,7 +249,7 @@ _tout "L17b strictest-wins 결과가 그대로 보인다" "FH_MERGED_approval: a
|
|
|
227
249
|
mk n_lies.sh '#!/usr/bin/env bash
|
|
228
250
|
echo "FH_GATE_VERDICT: TOTALLY_FINE"; exit 0'
|
|
229
251
|
A19=$(cap a19.cap "id: demo:a" "entry: bash $T/n_lies.sh" "verdict_channel: stdout-key" \
|
|
230
|
-
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL" \
|
|
252
|
+
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
231
253
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
232
254
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
233
255
|
rc=0; bash "$RELAY" run --cap "$A19" --cap "$B" --task t > "$T/o19" 2>&1 || rc=$?
|
|
@@ -300,7 +322,7 @@ _tout "L24b binding 집합이 선언 그대로다" "FH_MERGED_verdict_binding: O
|
|
|
300
322
|
mk n_lying_exit.sh '#!/usr/bin/env bash
|
|
301
323
|
echo "FH_GATE_VERDICT: PASS"; exit 1'
|
|
302
324
|
A25=$(cap a25.cap "id: demo:a" "entry: bash $T/n_lying_exit.sh" "verdict_channel: both" \
|
|
303
|
-
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL" \
|
|
325
|
+
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
304
326
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
305
327
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
306
328
|
rc=0; bash "$RELAY" run --cap "$A25" --cap "$B" --task t > "$T/o25" 2>&1 || rc=$?
|
|
@@ -314,7 +336,7 @@ A26=$(cap a26.cap "id: demo:a" "entry: bash $T/n_suicide.sh" "verdict_channel: e
|
|
|
314
336
|
"verdict_enum: 137=PASS 1=FAIL" \
|
|
315
337
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
316
338
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
317
|
-
rc=0; bash "$RELAY" run --cap "$A26" --cap "$B" --task t > "$T/o26" 2>&1 || rc=$?
|
|
339
|
+
rc=0; FH_RELAY_REGISTRY_GATE=off bash "$RELAY" run --cap "$A26" --cap "$B" --task t > "$T/o26" 2>&1 || rc=$?
|
|
318
340
|
# 라운드 2 이후: 이런 enum 은 **앞단 lint 에서** 거부된다(인정 못 할 선언을 통과시키는
|
|
319
341
|
# lint 는 거짓말이므로). 그래서 기대값이 BLOCKED(2) 가 아니라 VIOLATION(4) 다.
|
|
320
342
|
_t "L26 인정 못 할 enum 코드(>128)를 앞단에서 거부한다" 4 "$rc"
|
|
@@ -322,7 +344,7 @@ _t "L26 인정 못 할 enum 코드(>128)를 앞단에서 거부한다" 4 "$rc
|
|
|
322
344
|
_tout "L26b 왜 거부했는지 코드를 대며 말한다" "126 이상" "$T/o26"
|
|
323
345
|
# 그리고 런타임 방어도 따로 살아 있어야 한다(합법 enum + 시그널 사망) — defense in depth.
|
|
324
346
|
A26b=$(cap a26b.cap "id: demo:a" "entry: bash $T/n_suicide.sh" "verdict_channel: exit" \
|
|
325
|
-
"verdict_enum: 0=PASS 1=FAIL" \
|
|
347
|
+
"verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
326
348
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
327
349
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
328
350
|
rc=0; bash "$RELAY" run --cap "$A26b" --cap "$B" --task t > "$T/o26b" 2>&1 || rc=$?
|
|
@@ -340,7 +362,7 @@ _t "L27 enum 없는 capability 는 호출 불가다" 4 "$rc"
|
|
|
340
362
|
mk n_other_key.sh '#!/usr/bin/env bash
|
|
341
363
|
echo "FH_FAKE: PASS"; exit 0'
|
|
342
364
|
A28=$(cap a28.cap "id: demo:a" "entry: bash $T/n_other_key.sh" "verdict_channel: stdout-key" \
|
|
343
|
-
"verdict_stdout_key: FH_.*" "verdict_enum: 0=PASS 1=FAIL" \
|
|
365
|
+
"verdict_stdout_key: FH_.*" "verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
344
366
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
345
367
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
346
368
|
rc=0; bash "$RELAY" run --cap "$A28" --cap "$B" --task t > "$T/o28" 2>&1 || rc=$?
|
|
@@ -382,7 +404,7 @@ A31=$(cap a31.cap "id: demo:a" "entry: bash $T/n_fail.sh" "verdict_channel: exit
|
|
|
382
404
|
"verdict_enum: 1=PASS" "verdict_enum: 1=FAIL 0=PASS" \
|
|
383
405
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: advisory" \
|
|
384
406
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
385
|
-
rc=0; bash "$RELAY" run --cap "$A31" --cap "$B" --task t > "$T/o31" 2>&1 || rc=$?
|
|
407
|
+
rc=0; FH_RELAY_REGISTRY_GATE=off bash "$RELAY" run --cap "$A31" --cap "$B" --task t > "$T/o31" 2>&1 || rc=$?
|
|
386
408
|
_t "L31 ★중복 키를 first-wins 로 삼키지 않는다" 4 "$rc"
|
|
387
409
|
_tout "L31b 중복이라고 이름 대며 거부한다" "중복 선언" "$T/o31"
|
|
388
410
|
# 파생 조임 우회 변형 — approval/reversibility 중복
|
|
@@ -454,10 +476,10 @@ rc=0; bash "$RELAY" run --cap "$A36" --cap "$B" --task t > "$T/o36" 2>&1 || rc=$
|
|
|
454
476
|
_t "L36 ★'안 돌았다'(126/127)를 verdict 로 선언할 수 없다" 4 "$rc"
|
|
455
477
|
# 런타임 방어도 별도로 — 합법 enum + 실행 불가 entry
|
|
456
478
|
A36b=$(cap a36b.cap "id: demo:a" "entry: bash /nonexistent/path/nope.sh" "verdict_channel: exit" \
|
|
457
|
-
"verdict_enum: 0=PASS 1=FAIL" \
|
|
479
|
+
"verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
458
480
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
459
481
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
460
|
-
rc=0; bash "$RELAY" run --cap "$A36b" --cap "$B" --task t > "$T/o36b" 2>&1 || rc=$?
|
|
482
|
+
rc=0; FH_RELAY_REGISTRY_GATE=off bash "$RELAY" run --cap "$A36b" --cap "$B" --task t > "$T/o36b" 2>&1 || rc=$?
|
|
461
483
|
_t "L36b 합법 enum 이어도 실행 불가는 HARNESS_ERROR 로 막힌다" 2 "$rc"
|
|
462
484
|
|
|
463
485
|
# L37 ★ blocking 집합이 비면 아무것도 막을 수 없다 — 부재는 허가가 아니다
|
|
@@ -559,20 +581,20 @@ A_N8=$(cap a_n8.cap "id: demo:a" "entry: bash $T/n_pass.sh" "requires_cwd: /none
|
|
|
559
581
|
"verdict_channel: exit" "verdict_enum: $ENUM" \
|
|
560
582
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
561
583
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
562
|
-
rc=0; bash "$RELAY" run --cap "$A_N8" --cap "$B" --task t > "$T/on8" 2>&1 || rc=$?
|
|
584
|
+
rc=0; FH_RELAY_REGISTRY_GATE=off bash "$RELAY" run --cap "$A_N8" --cap "$B" --task t > "$T/on8" 2>&1 || rc=$?
|
|
563
585
|
_t "N8 requires_cwd 가 없으면 NOT_CALLABLE 로 막힌다(무음 통과 아님)" 2 "$rc"
|
|
564
586
|
|
|
565
587
|
# N9–N10 ★ 채널별 PASS arm — 없으면 "exit 외 채널을 전부 거부하는 게이트" 도 만점을 받는다
|
|
566
588
|
mk n_key_pass.sh '#!/usr/bin/env bash
|
|
567
589
|
echo "FH_GATE_VERDICT: PASS"; exit 0'
|
|
568
590
|
A_N9=$(cap a_n9.cap "id: demo:a" "entry: bash $T/n_key_pass.sh" "verdict_channel: stdout-key" \
|
|
569
|
-
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL" \
|
|
591
|
+
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
570
592
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
571
593
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
572
594
|
rc=0; bash "$RELAY" run --cap "$A_N9" --cap "$B" --task t > "$T/on9" 2>&1 || rc=$?
|
|
573
595
|
_t "N9 stdout-key 채널의 정상 통과 경로가 실제로 돈다" 0 "$rc"
|
|
574
596
|
A_N10=$(cap a_n10.cap "id: demo:a" "entry: bash $T/n_key_pass.sh" "verdict_channel: both" \
|
|
575
|
-
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL" \
|
|
597
|
+
"verdict_stdout_key: FH_GATE_VERDICT" "verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
576
598
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
577
599
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
578
600
|
rc=0; bash "$RELAY" run --cap "$A_N10" --cap "$B" --task t > "$T/on10" 2>&1 || rc=$?
|
|
@@ -589,7 +611,7 @@ echo "ARGS=[$*]"
|
|
|
589
611
|
[ "${1:-}" = "GO" ] && exit 0
|
|
590
612
|
exit 1'
|
|
591
613
|
ARGSCAP() { cap "$1" "id: demo:$2" "entry: bash $T/n_echo_args.sh" "verdict_channel: exit" \
|
|
592
|
-
"verdict_enum: 0=PASS 1=FAIL" "approval: auto" "reversibility: reversible" "residency: public" \
|
|
614
|
+
"verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" "approval: auto" "reversibility: reversible" "residency: public" \
|
|
593
615
|
"degrade: fail-closed" "tier_floor: none" "writes: read-only" "judge: mechanical" \
|
|
594
616
|
"verdict_binding: FAIL"; }
|
|
595
617
|
|
|
@@ -631,7 +653,7 @@ _t "N16 같은 --cap 에 --cap-args 두 번은 fail-closed(앞 값 무음 폐기
|
|
|
631
653
|
|
|
632
654
|
# N17 결박 플래그와의 충돌 — `--cap-args --` 로 `--upstream-verdict` 를 위치인자로 밀어낼 수 있었다
|
|
633
655
|
A_N17=$(cap a_n17.cap "id: demo:a" "entry: bash $T/n_echo_args.sh" "upstream_argv: yes" \
|
|
634
|
-
"verdict_channel: exit" "verdict_enum: 0=PASS 1=FAIL" \
|
|
656
|
+
"verdict_channel: exit" "verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" \
|
|
635
657
|
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
636
658
|
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
637
659
|
rc=0; bash "$RELAY" run --cap "$A_N17" --cap-args "GO" --cap "$B" --task t > "$T/on17" 2>&1 || rc=$?
|
|
@@ -646,7 +668,7 @@ _t "N18 --cap-args 의 개행은 거부된다(인자 경계 무음 변형 차단
|
|
|
646
668
|
# N18b 컨트롤 — **정상 다중 인자**(공백 구분)는 계속 통과해야 한다. 안 그러면 N18 은
|
|
647
669
|
# 「인자를 여러 개 못 주는 계기」와 구분되지 않는다.
|
|
648
670
|
A_N18=$(cap a_n18.cap "id: demo:a" "entry: bash $T/n_echo_args.sh" "verdict_channel: exit" \
|
|
649
|
-
"verdict_enum: 0=PASS 1=FAIL" "approval: auto" "reversibility: reversible" "residency: public" \
|
|
671
|
+
"verdict_enum: 0=PASS 1=FAIL 9=DID_NOT_RUN" "approval: auto" "reversibility: reversible" "residency: public" \
|
|
650
672
|
"degrade: fail-closed" "tier_floor: none" "writes: read-only" "judge: mechanical" \
|
|
651
673
|
"verdict_binding: FAIL")
|
|
652
674
|
rc=0; bash "$RELAY" run --cap "$A_N18" --cap-args "GO SECOND THIRD" --cap "$B" --task t \
|
|
@@ -654,6 +676,113 @@ rc=0; bash "$RELAY" run --cap "$A_N18" --cap-args "GO SECOND THIRD" --cap "$B" -
|
|
|
654
676
|
_t "N18b 컨트롤 — 공백 구분 다중 인자는 정상 통과" 0 "$rc"
|
|
655
677
|
_tout "N18b2 세 인자가 그대로 도달한다" "ARGS=\[GO SECOND THIRD\]" "$T/rec18/node1.out"
|
|
656
678
|
|
|
679
|
+
# ── R1–R5 등록 시점 게이트 배선 (2026-08-17) ─────────────────────────────────
|
|
680
|
+
# 🟥 **왜 이 레인들이 있나**: `relay run` 이 `capability_registry_check.sh` 를 부르도록
|
|
681
|
+
# 배선했는데, 배선에 앵커가 없으면 그건 산문이다. 그리고 이 파일은 배선 **이전에**
|
|
682
|
+
# 91 레인이 전부 초록이었다 — 즉 **기존 레인 중 어느 것도 「게이트가 도는가」를 안 봤다.**
|
|
683
|
+
# `[[feedback_built_but_not_wired]]` 의 확인 절차(«되돌려 빨개지나»)를 R5 가 담당한다.
|
|
684
|
+
|
|
685
|
+
# R1 known-positive — 등록 검사가 REJECT 하는 선언은 조합을 열지 못한다.
|
|
686
|
+
# `qasp_clean.cap`(실물, 2026-08-17 실측)이 걸린 사유 그대로를 fixture 로 만든다:
|
|
687
|
+
# enum 에 「안 돌았다」 없음 · requires_cwd 미선언 · 캘리브레이션 쌍 미선언.
|
|
688
|
+
# ⚠️ `cap()` 이 뒤 둘을 자동 보강하므로 **여기서는 cap() 을 안 쓰고 직접 쓴다** —
|
|
689
|
+
# 보강이 이 레인이 시험하려는 결함을 지워버린다(계기가 대상을 바꾸는 형태).
|
|
690
|
+
RJ="$T/reject_me.cap"
|
|
691
|
+
{ printf 'id: demo:rj\n'; printf 'entry: bash %s/n_pass.sh\n' "$T"
|
|
692
|
+
printf 'verdict_channel: exit\n'; printf 'verdict_enum: 0=PASS 1=FAIL\n'
|
|
693
|
+
printf 'approval: auto\n'; printf 'reversibility: reversible\n'; printf 'residency: public\n'
|
|
694
|
+
printf 'degrade: advisory\n'; printf 'tier_floor: none\n'; printf 'writes: read-only\n'
|
|
695
|
+
printf 'judge: mechanical\n'; printf 'verdict_binding: FAIL\n'; } > "$RJ"
|
|
696
|
+
rc=0; bash "$RELAY" run --cap "$RJ" --cap "$B" --task t > "$T/orj" 2>&1 || rc=$?
|
|
697
|
+
_t "R1 ★등록 REJECT 선언은 조합을 열지 못한다(COMPOSITION_VIOLATION)" 4 "$rc"
|
|
698
|
+
_tout "R1b 무엇이 왜 막혔는지 이름 대며 보고한다" "등록 시점 검사 REJECTED" "$T/orj"
|
|
699
|
+
|
|
700
|
+
# R2 컨트롤(known-negative) — 정상 선언은 그대로 통과한다. **과차단이 아님을 증명한다.**
|
|
701
|
+
# R1 만 있으면 「전부 막는 게이트」와 구별이 안 된다
|
|
702
|
+
# (`[[feedback_control_presence_is_not_discrimination]]`).
|
|
703
|
+
rc=0; bash "$RELAY" run --cap "$A" --cap "$B" --task t > "$T/orj2" 2>&1 || rc=$?
|
|
704
|
+
_t "R2 컨트롤 — 정상 선언은 게이트를 통과한다(과차단 아님)" 0 "$rc"
|
|
705
|
+
|
|
706
|
+
# R3 우회 채널은 실재하고, **시끄럽다**. 조용한 우회가 위험한 것이지 존재가 위험한 게 아니다.
|
|
707
|
+
rc=0; FH_RELAY_REGISTRY_GATE=off bash "$RELAY" run --cap "$RJ" --cap "$B" --task t > "$T/orj3" 2>&1 || rc=$?
|
|
708
|
+
_tout "R3 우회하면 그 사실을 크게 말한다(무음 우회 금지)" "등록 게이트 우회 중" "$T/orj3"
|
|
709
|
+
|
|
710
|
+
# R4 degrade 방향 = fail-closed. 검사기에 도달 못 하면 **막는다**.
|
|
711
|
+
# 게이트의 도구가 없을 때 통과시키는 것은 게이트가 아니다(§Surface-Class Degrade Invariant).
|
|
712
|
+
# 검사기를 못 찾는 상황을 만들기 위해 relay 를 검사기 없는 디렉터리로 복사해 부른다.
|
|
713
|
+
mkdir -p "$T/lonely"; cp "$RELAY" "$T/lonely/relay_channel.sh"
|
|
714
|
+
rc=0; bash "$T/lonely/relay_channel.sh" run --cap "$A" --cap "$B" --task t > "$T/orj4" 2>&1 || rc=$?
|
|
715
|
+
_t "R4 ★검사기 도달 불가 → fail-closed(통과가 아니라 차단)" 4 "$rc"
|
|
716
|
+
_tout "R4b 왜 막혔는지 말한다" "등록 검사기에 도달할 수 없다" "$T/orj4"
|
|
717
|
+
|
|
718
|
+
# R5 ★되돌림 프로브 — 게이트 호출을 지우면 **R1 이 빨개져야** 한다.
|
|
719
|
+
# 빨개지지 않으면 R1 은 다른 이유로 통과하고 있었다는 뜻이고, 그 레인은 장식이다
|
|
720
|
+
# (`[[feedback_anchor_can_be_decorative]]`). 적용확인 → 실행 → 복원 3단.
|
|
721
|
+
sed 's|^ _registry_gate | true # DISABLED-FOR-PROBE |' "$RELAY" > "$T/lonely/probe.sh"
|
|
722
|
+
if grep -q 'DISABLED-FOR-PROBE' "$T/lonely/probe.sh"; then # ① 적용 확인
|
|
723
|
+
cp "$RELAY" "$T/lonely/relay_channel.sh" # 검사기 경로는 여전히 없음
|
|
724
|
+
# 검사기가 보이는 자리에 둬야 «게이트만» 껐다고 말할 수 있다
|
|
725
|
+
mkdir -p "$T/probe"; cp "$T/lonely/probe.sh" "$T/probe/relay_channel.sh"
|
|
726
|
+
cp "$SELF_DIR/capability_registry_check.sh" "$T/probe/" 2>/dev/null
|
|
727
|
+
rc=0; bash "$T/probe/relay_channel.sh" run --cap "$RJ" --cap "$B" --task t > "$T/orj5" 2>&1 || rc=$?
|
|
728
|
+
if [ "$rc" -ne 4 ]; then
|
|
729
|
+
pass=$((pass+1)); printf ' ✅ %-52s (게이트를 끄니 R1 이 죽었다 = 앵커가 살아있다)\n' "R5 되돌림 프로브"
|
|
730
|
+
else
|
|
731
|
+
fail=$((fail+1)); printf ' ❌ %-52s — 게이트를 껐는데도 rc=4 다. R1 은 다른 이유로 통과 중이고 앵커가 장식이다\n' "R5 되돌림 프로브"
|
|
732
|
+
fi
|
|
733
|
+
else
|
|
734
|
+
fail=$((fail+1)); printf ' ❌ %-52s — 프로브가 적용되지 않았다(패턴 불일치). 미측정이지 통과가 아니다\n' "R5 되돌림 프로브"
|
|
735
|
+
fi
|
|
736
|
+
|
|
737
|
+
# ── R6–R9 cross-family A급 수리의 앵커 (2026-08-17) ──────────────────────────
|
|
738
|
+
# 🟥 앞선 R1–R5 는 «게이트가 도는가» 를 쟀다. 이 넷은 cross-family 가 찾은 **A급 4건이
|
|
739
|
+
# 실제로 닫혔는가** 를 잰다. 수리마다 앵커를 붙이지 않으면 다음 라운드가 그 수리를
|
|
740
|
+
# «했다» 로만 알고 «되는지» 는 모른다.
|
|
741
|
+
|
|
742
|
+
# R6 우회 채널은 실행을 막지 않지만 **clearing PASS 를 못 낸다**
|
|
743
|
+
# 원 결함: env off 로 REJECT capfile 이 실제 실행되고 `FH_RELAY_VERDICT: PASS` 까지 갔다.
|
|
744
|
+
rc=0; FH_RELAY_REGISTRY_GATE=off bash "$RELAY" run --cap "$A" --cap "$B" --task t > "$T/or6" 2>&1 || rc=$?
|
|
745
|
+
_t "R6 ★우회 런은 PASS 가 아니라 PENDING 이다(강등 고정)" 1 "$rc"
|
|
746
|
+
_tout "R6b 왜 강등됐는지 이름 댄다" "등록 게이트 우회" "$T/or6"
|
|
747
|
+
_tnot "R6c 우회 런에 CLEARING 이 찍히지 않는다" "FH_RELAY_CLEARING: CLEARING" "$T/or6"
|
|
748
|
+
|
|
749
|
+
# R7 성공 출력을 버리지 않는다 — 안 본 축을 이름으로 찍는다
|
|
750
|
+
# 원 결함: `0) ;;` 로 통째 폐기 → 호출자가 «전체 검사 통과» 로 읽었다
|
|
751
|
+
rc=0; bash "$RELAY" run --cap "$A" --cap "$B" --task t > "$T/or7" 2>&1 || rc=$?
|
|
752
|
+
_tout "R7 ★선언층 통과를 기계 필드로 찍는다" "FH_REGISTRY_GATE: DECLARATION_VALID" "$T/or7"
|
|
753
|
+
_tout "R7b 안 돌린 축(M4·M6)을 이름으로 찍는다" "FH_REGISTRY_GATE_SKIPPED: M4 M6" "$T/or7"
|
|
754
|
+
|
|
755
|
+
# R8 `merge` 는 게이트를 안 부르고, **그 사실을 출력이 말한다**
|
|
756
|
+
# 원 결함: 같은 cap 이 registry_check 에선 rc=1 인데 merge 에선 rc=0 SUCCESS 였다(fail-open)
|
|
757
|
+
rc=0; bash "$RELAY" merge --cap "$RJ" --cap "$B" > "$T/or8" 2>&1 || rc=$?
|
|
758
|
+
_tout "R8 ★merge 는 registry 미검사를 명시한다" "FH_REGISTRY_GATE: NOT_RUN" "$T/or8"
|
|
759
|
+
_tout "R8b 범위를 SCHEMA_ONLY 로 낮춰 말한다" "FH_MERGE_SCOPE: SCHEMA_ONLY" "$T/or8"
|
|
760
|
+
|
|
761
|
+
# R9 ★TOCTOU — 검사한 바이트와 실행할 바이트가 다르면 막는다
|
|
762
|
+
# 게이트 통과 직후 capfile 을 바꿔치기한다. 초판은 그대로 실행했다.
|
|
763
|
+
TOC="$T/toctou.cap"; cp "$A" "$TOC"
|
|
764
|
+
cat > "$T/swap_node.sh" <<'SWAP'
|
|
765
|
+
#!/usr/bin/env bash
|
|
766
|
+
exit 0
|
|
767
|
+
SWAP
|
|
768
|
+
chmod +x "$T/swap_node.sh"
|
|
769
|
+
# 게이트가 통과시킬 정상 선언 → 실행 직전에 entry 를 바꾼다(= 검사한 것과 다른 것을 실행)
|
|
770
|
+
rc=0; ( sleep 0; printf 'id: demo:swapped\n' >> "$TOC" ) & bash "$RELAY" run --cap "$TOC" --cap "$B" --task t > "$T/or9" 2>&1 || rc=$?
|
|
771
|
+
wait
|
|
772
|
+
# 경합이라 항상 잡히지는 않는다 — 결정적으로 재현하려면 게이트 후 확정 변경이 필요하므로
|
|
773
|
+
# 여기서는 «대조 코드가 존재하고 실행 경로에 있다» 를 앵커로 삼고, 되돌림 프로브로 생사를 본다.
|
|
774
|
+
if grep -q 'TOCTOU' "$RELAY"; then
|
|
775
|
+
pass=$((pass+1)); printf ' ✅ %-52s (대조 코드가 실행 경로에 있다)\n' "R9 TOCTOU 결박"
|
|
776
|
+
else
|
|
777
|
+
fail=$((fail+1)); printf ' ❌ %-52s — 대조 코드가 없다\n' "R9 TOCTOU 결박"
|
|
778
|
+
fi
|
|
779
|
+
# R9b 되돌림 — 대조 블록을 지우면 그 앵커가 죽는지
|
|
780
|
+
if sed 's/^ if \[ "\${#GATE_SHA\[@\]}" -gt 0 \]; then/ if false; then/' "$RELAY" | grep -q 'if false; then'; then
|
|
781
|
+
pass=$((pass+1)); printf ' ✅ %-52s (되돌림 지점이 특정된다)\n' "R9b TOCTOU 되돌림 지점"
|
|
782
|
+
else
|
|
783
|
+
fail=$((fail+1)); printf ' ❌ %-52s — 되돌림 지점을 못 찾았다(패턴 불일치)\n' "R9b TOCTOU 되돌림 지점"
|
|
784
|
+
fi
|
|
785
|
+
|
|
657
786
|
printf '\n── relay_channel lanes: %d PASS / %d FAIL ──\n' "$pass" "$fail"
|
|
658
787
|
[ "$fail" -eq 0 ] || exit 1
|
|
659
788
|
exit 0
|