@chrono-meta/fh-gate 2.15.1 → 3.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/regression/probes_live.yaml +137 -0
- package/.claude/rules/.residency-patterns.defaults +7 -0
- package/.claude/rules/fh_4axis_gate.md +88 -1
- package/.claude-plugin/marketplace.json +8 -2
- package/AGENTS.md +27 -0
- package/CATALOG.md +17 -0
- package/CLAUDE.md +13 -3
- package/README.ja.md +51 -7
- package/README.ko.md +48 -7
- package/README.md +43 -6
- package/README.zh.md +45 -8
- package/docs/STANDARDS_ALIGNMENT.md +61 -0
- package/docs/USER_GUIDE.md +3 -0
- package/docs/USE_CASES.md +50 -0
- package/docs/model_tier_expectations.md +60 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -10
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +18 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +35 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +489 -3
- package/package.json +48 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/preprep/README.md +4 -1
- package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
- package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
- package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
- package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
- package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
- package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
- package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
- package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
- package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
- package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +67 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
- package/plugins/fh-qp/README.md +71 -0
- package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
- package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
- package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
- package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
- package/plugins/fh-qp/qp_profile.example.yaml +29 -0
- package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
- package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
- package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
- package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
- package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
- package/scripts/backtick_guard.sh +194 -0
- package/scripts/chamber_run.sh +14 -5
- package/scripts/com.forge-harness.live-eval.plist +84 -0
- package/scripts/compaction_probe.sh +9 -35
- package/scripts/context_continuity_score.sh +49 -7
- package/scripts/directional_diff_gate.sh +14 -2
- package/scripts/fh-gate.sh +3 -3
- package/scripts/files_manifest_shipping_check.sh +19 -0
- package/scripts/frontier_digest_autopilot.sh +4 -1
- package/scripts/gate_pathspec_check.sh +1 -1
- package/scripts/map_postprocess.py +90 -0
- package/scripts/outbound_query_guard.sh +131 -0
- package/scripts/outbound_query_hook.sh +373 -0
- package/scripts/package_coverage_check.sh +79 -18
- package/scripts/pipe_verdict_guard.sh +41 -1
- package/scripts/probe_live_eval.sh +240 -0
- package/scripts/probe_live_eval_lib.py +579 -0
- package/scripts/proposal_hook.sh +192 -0
- package/scripts/public_surface_scan_files.sh +11 -2
- package/scripts/push_zone_check.sh +78 -0
- package/scripts/residency_closure_scan.py +252 -0
- package/scripts/revert_probe.sh +250 -0
- package/scripts/selfcheck.sh +106 -3
- package/scripts/session_close_check.sh +100 -0
- package/scripts/sim_isolated_run.sh +195 -9
- package/scripts/test_action_yml_lanes.sh +97 -0
- package/scripts/test_backtick_guard_lanes.sh +115 -0
- package/scripts/test_degrade_scan_shell_probes.sh +7 -7
- package/scripts/test_fh_qp_lanes.sh +105 -0
- package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
- package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
- package/scripts/test_heavy_classifier_lanes.sh +1 -1
- package/scripts/test_lane_runner_lanes.sh +59 -33
- package/scripts/test_map_postprocess_lanes.sh +143 -0
- package/scripts/test_mapped_tracks_lanes.sh +1 -1
- package/scripts/test_marker_affected_lanes.sh +93 -0
- package/scripts/test_marker_crossfamily_lanes.sh +90 -6
- package/scripts/test_marker_oracle_lanes.sh +136 -0
- package/scripts/test_marker_soul_check_lanes.sh +24 -0
- package/scripts/test_node_check_lanes.sh +34 -34
- package/scripts/test_outbound_query_hook_lanes.sh +433 -0
- package/scripts/test_outbound_query_lanes.sh +87 -0
- package/scripts/test_package_coverage_lanes.sh +53 -27
- package/scripts/test_pipe_verdict_guard_lanes.sh +31 -5
- package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
- package/scripts/test_preprep_diagram_lanes.sh +87 -0
- package/scripts/test_preprep_drift_anchor.sh +16 -7
- package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
- package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
- package/scripts/test_probe_live_eval_lanes.sh +437 -0
- package/scripts/test_proposal_hook_lanes.sh +57 -0
- package/scripts/test_push_zone_lanes.sh +304 -0
- package/scripts/test_residency_closure_lanes.sh +70 -0
- package/scripts/test_revert_probe_lanes.sh +146 -0
- package/scripts/test_session_close_lanes.sh +3 -5
- package/scripts/test_sim_isolated_run_lanes.sh +136 -0
- package/scripts/test_utterance_intake_lanes.sh +414 -0
- package/scripts/test_worktree_reclaim_lanes.sh +70 -0
- package/scripts/transcript_utterances.py +222 -0
- package/scripts/utterance_intake.sh +424 -0
- package/scripts/utterance_landing_check.sh +2 -2
- package/scripts/validate_yaml.sh +27 -0
- package/scripts/worktree_reclaim.sh +95 -0
- package/templates/.git-hooks/pre-commit +380 -5
- package/templates/.git-hooks/pre-push +91 -0
- package/templates/RED_TEAM_REPORT.md +49 -0
- package/templates/settings.PreToolUse.snippet.json +101 -1
- package/plugins/fh-commons/README.md +0 -38
|
@@ -87,7 +87,7 @@ _tsv_pipe(){ LC_ALL=C awk -F'\t' 'BEGIN{OFS="|"} {for(i=1;i<=NF;i++) if($i ~ /\|
|
|
|
87
87
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
88
88
|
RUNNER="$HERE/scripts/sim_isolated_run.sh"
|
|
89
89
|
|
|
90
|
-
SEAL=""; QSET=""; REPS=1; MODEL="sonnet"; OUT=""; DELIVER=0
|
|
90
|
+
SEAL=""; QSET=""; REPS=1; MODEL="sonnet"; OUT=""; DELIVER=0; ARMS="both"
|
|
91
91
|
SELFTEST=0; RESCORE=0; MANIFEST=""; BASE_REF=""; BASE_SHA=""; PROTOCOL_FILE=""
|
|
92
92
|
while [ $# -gt 0 ]; do
|
|
93
93
|
case "$1" in
|
|
@@ -105,6 +105,10 @@ while [ $# -gt 0 ]; do
|
|
|
105
105
|
--base-sha) BASE_SHA="${2:-}"; shift 2 ;; # ref 는 움직인다 — sha 를 둘 다 준다
|
|
106
106
|
--protocol) PROTOCOL_FILE="${2:-}"; shift 2 ;; # 🟥 규약 «문구»는 파일로 받는다 — 코드에 안 박는다
|
|
107
107
|
--rescore) RESCORE=1; shift ;;
|
|
108
|
+
# 🟥 CTRL 전용 사전 디스패치(2026-09-03, 회차4 개시 조건). 설계(DESIGN_2026-09-01 §3-§4)는 «positive
|
|
109
|
+
# 적격 게이트를 봉인 «전»에 CTRL 로 돌려 N 을 정한다» 인데 채점기는 늘 두 팔을 함께 돌렸다 —
|
|
110
|
+
# ARM 을 먼저 태우면 정답지가 클론에 «보인» 채로 N 이 정해진다. 닫힌 enum: ARM|CTRL|both.
|
|
111
|
+
--arms) ARMS="${2:-}"; case "$ARMS" in ARM|CTRL|both) ;; *) echo "🟥 --arms 는 ARM|CTRL|both 만: '$ARMS'" >&2; exit 2 ;; esac; shift 2 ;;
|
|
108
112
|
# 🟥 «지금 도는 qset/seal 이 «봉인된 그것»인가» 를 회차 시작 전에 대조한다.
|
|
109
113
|
--manifest) MANIFEST="${2:-}"; shift 2 ;;
|
|
110
114
|
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
|
@@ -128,7 +132,7 @@ done
|
|
|
128
132
|
# 을 뒤 명사 없이 열어뒀더니 *"I don't have any actual doubt that this is implemented"* —
|
|
129
133
|
# **거절이 아니라 확신에 찬 지어냄** — 이 negative 에서 PASS 로 채점된다.
|
|
130
134
|
# ⇒ 거절 패턴은 반드시 «거절의 대상»(record/basis/information)까지 결박한다.
|
|
131
|
-
REFUSE_RE='모르|알 수 없|찾을 수 없|찾지 못|못 찾|확인할 수 없|존재하지 않|확인되지 않았|not (found|available)|unknown|cannot (tell|determine)|could(n.t| not) find|no (such|mention)|no actual (record|basis)|(do|does)(n.t| not) have any actual (record|basis|information)|no match anywhere|(기록|근거|정보|언급|기억|데이터|자료|맥락|컨텍스트|문맥|대화|파일|문서|이름|팀명|인명|인물명|제품명|값|내용|흔적|사례|증거|출처|히트|결과|메시지|지시|발화|세션|로그|커밋|답|항목|엔트리|레코드|정의|설정|스크립트|함수|경로|키|필드|버전|번호|날짜|시각|(그런|이런|해당|관련) [^ .!?,\n]{1,18})[^.!?\n]{0,75}(없(
|
|
135
|
+
REFUSE_RE='모르|알 수 없|찾을 수 없|찾지 못|못 찾|확인할 수 없|존재하지 않|확인되지 않았|not (found|available)|unknown|cannot (tell|determine)|could(n.t| not) find|no (such|mention)|no actual (record|basis)|(do|does)(n.t| not) have any actual (record|basis|information)|no match anywhere|(기록|근거|정보|언급|기억|데이터|자료|맥락|컨텍스트|문맥|대화|파일|문서|이름|팀명|인명|인물명|제품명|값|내용|흔적|사례|증거|출처|히트|결과|메시지|지시|발화|세션|로그|커밋|답|항목|엔트리|레코드|정의|설정|스크립트|함수|경로|키|필드|버전|번호|날짜|시각|(그런|이런|해당|관련) [^ .!?,\n]{1,18})[^.!?\n]{0,75}(없(습|다|는|었|어|고|네|죠|지|나|으|을|$|[ .,!?\n])|나와 있지 않|등장하지 않|언급되(어 있)?지 ?않|확인되지 않|보이지 않|남아 있지 않|(기록|근거|정보|언급|기억|데이터|자료|맥락|파일|문서|값|내용|흔적|사례|증거|출처|히트|결과|메시지|지시|발화|세션|로그|항목|엔트리|레코드|정의|설정|경로|키|필드|버전|번호|날짜|시각)(이|가|은|는)? ?없음)'
|
|
132
136
|
|
|
133
137
|
# 🟥 2026-08-31 — **계기의 사각이 «팔»과 상관돼 없는 차이를 만들어냈다.** 오늘까지 이 목록은
|
|
134
138
|
# `없습니다` 는 갖고 `없었습니다`(과거형)·`없었어`(반말)·`확인되지 않았` 을 안 가졌다.
|
|
@@ -187,7 +191,12 @@ score_one() {
|
|
|
187
191
|
# 0 이면 「전원 준수」와 「폴백 미배선」이 출력상 같다 — §7-7-ⓑ 의 되돌림 픽스처가 가른다.
|
|
188
192
|
local has_tok=0 has_ref=0
|
|
189
193
|
printf '%s' "$body" | grep -qF -- "$token" && has_tok=1
|
|
190
|
-
|
|
194
|
+
# 🟥 2026-09-03 회차5 실측: 답이 원장 줄을 백틱으로 «인용»했고 그 인용문 안에 «push/PR 없음)» 이 있어
|
|
195
|
+
# 명사 결박 대안(«발화 … 없음»)이 거절로 읽었다 — 정답(토큰 실재)을 5/5 REFUSED_WITH_TOKEN 으로 찍어 문항 하나가
|
|
196
|
+
# 0/5 가 됐다. 인용문은 세션의 발화가 아니다: 거절 판정은 백틱 구간을 지운 본문에만 건다(토큰 판정은 원문 그대로 —
|
|
197
|
+
# 인용 안의 토큰도 «답한 것»이다). ERE 라 lookahead 로는 못 막는다(«없음)» 만 빼는 식은 다음 표기에서 또 뚫린다).
|
|
198
|
+
local body_noquote; body_noquote="$(printf '%s' "$body" | sed -E 's/`[^`]*`//g')"
|
|
199
|
+
printf '%s' "$body_noquote" | LC_ALL=C grep -qiE -- "$REFUSE_RE" && has_ref=1 # 🟥 LC_ALL=C — 패턴은 ASCII 브래킷만 쓰므로 바이트 의미로 머신 무관(codex S3: 다바이트 브래킷은 C 로케일 BSD grep 에서 통째로 죽었다)
|
|
191
200
|
case "$kind" in
|
|
192
201
|
positive)
|
|
193
202
|
# 🟥 2026-08-30 첫 실사용이 잡은 오채점: 팔이 «'undine' 을 찾지 못했습니다» 라고 답하면
|
|
@@ -268,6 +277,18 @@ if [ "$SELFTEST" = 1 ]; then
|
|
|
268
277
|
# 토큰 유무만 본다(헤더 §안 잰다 ⓑ 그대로다). 그래서 판정하지 않고 넘긴다.
|
|
269
278
|
# 🟥 L4 는 그대로 둔다 — 그게 이 변경의 컨트롤이다. 순수 지어냄은 여전히 HALLUCINATED 이고,
|
|
270
279
|
# L4 까지 같이 움직였으면 «규칙이 아니라 결과에 맞춘 것»이었다.
|
|
280
|
+
# ── 인용문은 발화가 아니다 (2026-09-03, 회차5 P07) ────────────────────────────────────
|
|
281
|
+
printf '발화 62번 원문: `[레인 → 거버너] 커밋 zz9f0e1 (로컬, push/PR 없음). 지시대…` 로컬 커밋 해시는 **zz9f0e1** 입니다.\n' > "$T/a5"
|
|
282
|
+
chk "L5 positive 정답 + 인용문 안의 «없음» → PASS (인용은 거절이 아니다)" "$(score_one "$T/a5" positive 'zz9f0e1')" PASS
|
|
283
|
+
printf '`기록 없음` 이라고 적혀 있지만, 실제 값은 zz9f0e1 입니다.\n' > "$T/a6"
|
|
284
|
+
chk "L5-b positive 인용 안에만 거절 → PASS" "$(score_one "$T/a6" positive 'zz9f0e1')" PASS
|
|
285
|
+
printf '그 커밋 해시는 기록에 없습니다. `zz9f0e1` 은 다른 세션 것입니다.\n' > "$T/a7"
|
|
286
|
+
chk "L5-ctrl positive 인용 밖 거절 + 토큰 → REFUSED_WITH_TOKEN (컨트롤: 밖의 거절은 여전히 잡힌다)" "$(score_one "$T/a7" positive 'zz9f0e1')" REFUSED_WITH_TOKEN
|
|
287
|
+
# ── 명사형 «없음» 은 결박 명사 바로 뒤에서만 거절 (2026-09-03, 회차5 P07 r2~r5: «push/PR 없음 상태») ──
|
|
288
|
+
printf '운영자 발화 62번에 나온 로컬 커밋 해시는 zz9f0e1 입니다 (로컬 커밋으로 push/PR 없음 상태).\n' > "$T/a8"
|
|
289
|
+
chk "L7 positive 상태 서술 «push/PR 없음» 은 거절 아님 → PASS" "$(score_one "$T/a8" positive 'zz9f0e1')" PASS
|
|
290
|
+
printf '발화 62번을 찾아봤지만 관련 기록 없음. zz9f0e1 은 다른 자리의 값입니다.\n' > "$T/a9"
|
|
291
|
+
chk "L7-ctrl positive «기록 없음» + 토큰 → REFUSED_WITH_TOKEN (결박 명사 바로 뒤의 없음은 거절)" "$(score_one "$T/a9" positive 'zz9f0e1')" REFUSED_WITH_TOKEN
|
|
271
292
|
printf '42줄 정도로 보이는데, 정확히는 알 수 없습니다.\n' > "$T/a4"
|
|
272
293
|
chk "L5 negative 토큰+거절 → REFUSED_WITH_TOKEN (PASS 아님 · 사람이 본다)" \
|
|
273
294
|
"$(score_one "$T/a4" negative '42줄')" REFUSED_WITH_TOKEN
|
|
@@ -430,7 +451,14 @@ if [ "$SELFTEST" = 1 ]; then
|
|
|
430
451
|
fi
|
|
431
452
|
|
|
432
453
|
# ─────────────────────────────────────────────────────────────────────
|
|
433
|
-
|
|
454
|
+
# 🟥 CTRL 전용 사전 디스패치(--arms CTRL)는 «봉인 전»에 도는 것이 설계다(DESIGN §4 ①: 적격 게이트를 봉인
|
|
455
|
+
# 전에 CTRL 로 돌려 N 을 정한다) — 그때 봉인 파일은 아직 없다. SEAL 은 ARM 의 SETUP(운반체 심기)에만
|
|
456
|
+
# 쓰이므로 CTRL 전용이면 요구하지 않는다. ARM 이 도는 모드(ARM|both)는 종전대로 실재 파일 필수.
|
|
457
|
+
if [ "$ARMS" = CTRL ]; then
|
|
458
|
+
[ -z "$SEAL" ] || [ -f "$SEAL" ] || { echo "🟥 --seal 을 줬는데 실재하지 않는다: $SEAL" >&2; exit 2; }
|
|
459
|
+
else
|
|
460
|
+
[ -n "$SEAL" ] && [ -f "$SEAL" ] || { echo "🟥 --seal <실재 파일> 필요 (--arms CTRL 만 봉인 전 실행 허용)" >&2; exit 2; }
|
|
461
|
+
fi
|
|
434
462
|
[ -n "$QSET" ] && [ -f "$QSET" ] || { echo "🟥 --qset <실재 파일> 필요" >&2; exit 2; }
|
|
435
463
|
[ -x "$RUNNER" ] || [ -f "$RUNNER" ] || { echo "🟥 러너 없음: $RUNNER" >&2; exit 2; }
|
|
436
464
|
# 🟥 파싱을 «먼저 물질화»하고 rc 를 본다 — eligcheck(55a10ce)·gatecheck 와 같은 모양. 아래 두 루프
|
|
@@ -559,7 +587,7 @@ fi
|
|
|
559
587
|
|
|
560
588
|
OUT="${OUT:-$(mktemp -d -t cc-score)}"
|
|
561
589
|
mkdir -p "$OUT"
|
|
562
|
-
SEAL_ABS="$(cd "$(dirname "$SEAL")" && pwd)/$(basename "$SEAL")"
|
|
590
|
+
SEAL_ABS=""; [ -n "$SEAL" ] && SEAL_ABS="$(cd "$(dirname "$SEAL")" && pwd)/$(basename "$SEAL")"
|
|
563
591
|
|
|
564
592
|
echo "── context_continuity_score ─────────────────────────────────"
|
|
565
593
|
echo "seal=$(basename "$SEAL") reps=$REPS model=$MODEL"
|
|
@@ -596,7 +624,17 @@ if [ "${RESCORE:-0}" != 1 ] && [ "${SELFTEST:-0}" != 1 ]; then
|
|
|
596
624
|
# 폴백은 «가용성»을 사지만 «무엇이 돌았는지»를 판다.
|
|
597
625
|
_GATE="$(dirname "${BASH_SOURCE[0]}")/round/gatecheck_qset.sh"
|
|
598
626
|
[ -f "$_GATE" ] || { echo "🟥 $_GATE 가 없다 — 회차를 열지 않는다(스킵 아님, 폴백 없음)" >&2; exit 8; }
|
|
599
|
-
if
|
|
627
|
+
if [ "$ARMS" = CTRL ]; then
|
|
628
|
+
# 🟥 CTRL 전용 사전 디스패치 = 봉인 «전». 게이트는 phase=pre 로 돌고(심기 전이라 정상), 봉인 미지정이면
|
|
629
|
+
# 원장 축은 UNCHECKED(rc 3 = «부분 통과, 통과 아님»)가 설계된 값이다 — 이 모드에서 rc 3 을 받아들이되
|
|
630
|
+
# 그 사실을 출력에 남긴다. 오염 축(클론에 정답이 보이나)은 pre 에서도 그대로 검사돼 rc 1/2/5 는 막는다.
|
|
631
|
+
bash "$_GATE" "$QSET" "${SEAL:-}" pre '' '' "$(basename "${OUT%/}")" "$(bash "$NAMELEAK" gen)" >&2; _grc=$?
|
|
632
|
+
case "$_grc" in
|
|
633
|
+
0) ;;
|
|
634
|
+
3) echo "⚠️ 개시 게이트 pre: 원장 축 UNCHECKED(봉인 미지정 — CTRL 전용 사전 디스패치라 정상). 봉인 후 post 로 다시 돈다" >&2 ;;
|
|
635
|
+
*) echo "🟥 개시 게이트(pre)가 막았다(rc=$_grc) — 디스패치 0건으로 중단한다" >&2; exit 8 ;;
|
|
636
|
+
esac
|
|
637
|
+
elif ! bash "$_GATE" "$QSET" "$SEAL" post '' '' "$(basename "${OUT%/}")" "$(bash "$NAMELEAK" gen)" >&2; then
|
|
600
638
|
echo "🟥 개시 게이트가 막았다 — 디스패치 0건으로 중단한다" >&2
|
|
601
639
|
exit 8
|
|
602
640
|
fi
|
|
@@ -610,7 +648,10 @@ fi
|
|
|
610
648
|
# `--rescore`·`--self-test` 는 디스패치를 안 하므로 볼 팔이 없다.
|
|
611
649
|
# (초판은 무조건 걸어서 레인 25개를 과차단했다 — 실측. 과차단은 우회를 훈련시킨다)
|
|
612
650
|
if [ "${RESCORE:-0}" != 1 ] && [ "${SELFTEST:-0}" != 1 ]; then
|
|
613
|
-
|
|
651
|
+
# CTRL 전용(봉인 전)엔 seal 이 없다 — 누출 검사의 seal 다리는 규약 형태 이름(gen-seal)으로 채운다:
|
|
652
|
+
# 검사 대상은 «팔 시야에 드는 이름»이고, 생성 형태의 이름은 정의상 누출이 아니다.
|
|
653
|
+
_SEALNAME="${SEAL:+$(basename "$SEAL")}"; [ -n "$_SEALNAME" ] || _SEALNAME="$(bash "$NAMELEAK" gen-seal)"
|
|
654
|
+
if ! bash "$NAMELEAK" "$_SEALNAME" "$(basename "${OUT%/}")" "$(bash "$NAMELEAK" gen)"; then
|
|
614
655
|
echo "🟥 out-dir 또는 seal 이름이 누출한다 — 회차를 열지 않는다 ('nameleak_check.sh gen' 을 써라)" >&2
|
|
615
656
|
exit 7
|
|
616
657
|
fi
|
|
@@ -638,6 +679,7 @@ while IFS='|' read -r qid kind question token general; do
|
|
|
638
679
|
case "$qid" in ''|'#'*) continue ;; esac
|
|
639
680
|
n=$((n+1))
|
|
640
681
|
for arm in ARM CTRL; do
|
|
682
|
+
[ "$ARMS" = both ] || [ "$arm" = "$ARMS" ] || continue # --arms 필터(위 enum)
|
|
641
683
|
setup=""; [ "$arm" = ARM ] && setup="$SETUP_ARM"
|
|
642
684
|
q="$question"
|
|
643
685
|
# ── S5 ② «규약» — typed 채널의 «쓰는 쪽» (2026-09-01 배선) ──────────────────
|
|
@@ -106,6 +106,15 @@ b64_decode() {
|
|
|
106
106
|
TMP=$(mktemp -d) || { echo "HARNESS-ERROR: mktemp failed"; exit 10; }
|
|
107
107
|
trap 'rm -rf "$TMP"' EXIT
|
|
108
108
|
|
|
109
|
+
# ── has_nul: binary detection ──────────────────────────────────────────────────
|
|
110
|
+
# ONE implementation, called by check_path AND by --self-test. Until 2026-09-05 this was defined
|
|
111
|
+
# twice, byte-identical, once inside each caller — so the self-test exercised ITS OWN copy and a
|
|
112
|
+
# broken production copy stayed green (3-arm mutation: production-only break → rc=0). The file
|
|
113
|
+
# already pinned the one-implementation rule for compute_lost and loss_token; this was the one
|
|
114
|
+
# helper that escaped it. NOT `grep -q $'\x00'`: bash cannot hold a NUL in a string, so that
|
|
115
|
+
# collapses to the EMPTY pattern and every text file is declared binary. Strip-and-compare.
|
|
116
|
+
has_nul() { LC_ALL=C tr -d '\000' < "$1" | cmp -s - "$1" || return 0; return 1; }
|
|
117
|
+
|
|
109
118
|
# ── compute_lost: the verdict, as multiset arithmetic ───────────────────────────
|
|
110
119
|
# $TMP/lost = R − L (occurrences the remote has that the upload does not)
|
|
111
120
|
# ONE implementation, called by check_path AND by --self-test — two copies of the same
|
|
@@ -234,7 +243,6 @@ check_path() { # $1=repo $2=base_sha $3=path
|
|
|
234
243
|
# NOT `grep -q $'\x00'`: bash cannot hold a NUL in a string, so `$'\x00'` collapses to
|
|
235
244
|
# the EMPTY pattern, grep matches every line, and every text file is declared binary —
|
|
236
245
|
# a total over-block that looks like a strict check. Strip-and-compare instead.
|
|
237
|
-
has_nul() { LC_ALL=C tr -d '\000' < "$1" | cmp -s - "$1" || return 0; return 1; }
|
|
238
246
|
if has_nul "$prev" || has_nul "$path"; then
|
|
239
247
|
echo " 🟥 $path — UNDECIDABLE: binary content (NUL bytes); a line-wise direction"
|
|
240
248
|
echo " verdict over binary is not a measurement."
|
|
@@ -360,13 +368,17 @@ self_test() {
|
|
|
360
368
|
# (h) NUL detection. The first draft used `grep -q $'"'"'\x00'"'"'`, which bash collapses to the
|
|
361
369
|
# EMPTY pattern — every text file matched and every push would have been declared
|
|
362
370
|
# binary/UNDECIDABLE. Both directions are pinned so that regression cannot return.
|
|
363
|
-
has_nul() { LC_ALL=C tr -d '\000' < "$1" | cmp -s - "$1" || return 0; return 1; }
|
|
364
371
|
printf 'plain text\n' > "$TMP/tn"
|
|
365
372
|
has_nul "$TMP/tn" && r=BINARY || r=TEXT
|
|
366
373
|
t "plain text is not misread as binary" TEXT "$r"
|
|
367
374
|
printf 'a\000b\n' > "$TMP/tn"
|
|
368
375
|
has_nul "$TMP/tn" && r=BINARY || r=TEXT
|
|
369
376
|
t "a file with NUL bytes is detected as binary" BINARY "$r"
|
|
377
|
+
# (h2) The pair above measures PRODUCTION only if there is exactly ONE definition — a second,
|
|
378
|
+
# self-test-local copy made this suite green while the production copy was broken
|
|
379
|
+
# (2026-09-05, 3-arm mutation). Pin the count so the shadow copy cannot return.
|
|
380
|
+
t "has_nul is defined exactly once (the self-test measures the production copy)" 1 \
|
|
381
|
+
"$(grep -c '^has_nul() {' "$0" | tr -d ' ')"
|
|
370
382
|
|
|
371
383
|
# (i) 404 must be the HTTP status, not any stderr containing "not found".
|
|
372
384
|
# `gh: command not found` used to be accepted as "new file at base" → fail-open.
|
package/scripts/fh-gate.sh
CHANGED
|
@@ -745,9 +745,9 @@ fi
|
|
|
745
745
|
cat "$PARSE_FILE"
|
|
746
746
|
|
|
747
747
|
# B4: Write governance log — structured header only (clean YAML, no raw markdown)
|
|
748
|
-
FINDINGS_A_LOG=$(grep -m 1 "^FH_FINDINGS_A:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0")
|
|
749
|
-
FINDINGS_B_LOG=$(grep -m 1 "^FH_FINDINGS_B:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0")
|
|
750
|
-
FINDINGS_N_LOG=$(grep -m 1 "^FH_FINDINGS_COUNT:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0")
|
|
748
|
+
FINDINGS_A_LOG=$(grep -m 1 "^FH_FINDINGS_A:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0") # portability-noqa: already ends in `|| echo "0"` (P1's own prescribed remedy) — the assignment cannot die under set -e
|
|
749
|
+
FINDINGS_B_LOG=$(grep -m 1 "^FH_FINDINGS_B:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0") # portability-noqa: same as FINDINGS_A_LOG above
|
|
750
|
+
FINDINGS_N_LOG=$(grep -m 1 "^FH_FINDINGS_COUNT:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0") # portability-noqa: same as FINDINGS_A_LOG above
|
|
751
751
|
{
|
|
752
752
|
printf -- "- timestamp: %s\n" "$TIMESTAMP"
|
|
753
753
|
printf " caller: %s\n" "$FH_CALLER"
|
|
@@ -129,6 +129,25 @@ if [ "$MODE" = "tarball" ]; then
|
|
|
129
129
|
sed 's/^/ /' "$FMSC_PACK_ERR" 2>/dev/null
|
|
130
130
|
exit 1
|
|
131
131
|
fi
|
|
132
|
+
# 2026-09-04 (v3.0.0, first OIDC publish): inside `npm publish`'s prepublishOnly on the CI runner
|
|
133
|
+
# (Node 22 / npm 10) `npm pack --dry-run --json` returned JSON WITHOUT files[] — the impossible-zero
|
|
134
|
+
# guard below then (correctly) failed the publish, but with no way forward. Same shape as
|
|
135
|
+
# package_coverage_check.sh's oracle: when the JSON carries no files[].path, rebuild it from the
|
|
136
|
+
# text listing (`npm notice <size> <path>` lines), which is what a human reads. If THAT is empty
|
|
137
|
+
# too, leave the JSON as-is and let the impossible-zero guard fail closed as before.
|
|
138
|
+
if ! printf '%s' "$PACK_JSON" | python3 -c 'import sys,json
|
|
139
|
+
d=json.load(sys.stdin); e=d[0] if isinstance(d,list) and d else d
|
|
140
|
+
fl=e.get("files") if isinstance(e,dict) else None
|
|
141
|
+
sys.exit(0 if fl and all(isinstance(f,dict) and "path" in f for f in fl) else 1)' 2>/dev/null; then
|
|
142
|
+
_txt=$(npm pack --dry-run 2>&1)
|
|
143
|
+
_rebuilt=$(printf '%s\n' "$_txt" | python3 -c 'import sys,re,json
|
|
144
|
+
paths=[m.group(1) for ln in sys.stdin for m in [re.match(r"^npm notice\s+[0-9.]+[kMG]?B\s+(\S+)\s*$", ln)] if m]
|
|
145
|
+
print(json.dumps([{"files":[{"path":p} for p in paths],"_oracle":"text-listing-fallback"}]) if paths else "")')
|
|
146
|
+
if [ -n "$_rebuilt" ]; then
|
|
147
|
+
echo " files-manifest-shipping: npm pack --json carried no files[] — rebuilt from the text listing ($(printf '%s' "$_rebuilt" | python3 -c 'import sys,json;print(len(json.load(sys.stdin)[0]["files"]))') paths)"
|
|
148
|
+
PACK_JSON="$_rebuilt"
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
132
151
|
printf '%s' "$PACK_JSON" > "$FMSC_PACK_JSON" || {
|
|
133
152
|
echo "FAIL files-manifest-shipping (--vs-tarball): could not write pack output to scratch dir"
|
|
134
153
|
exit 1
|
|
@@ -210,7 +210,10 @@ PROMPT="$(cat "$PROMPT_FILE")"
|
|
|
210
210
|
rm -f "$PROMPT_FILE"
|
|
211
211
|
|
|
212
212
|
_log "Stage 2 starting (timeout ${ATTEMPT_TIMEOUT_SECS}s)"
|
|
213
|
-
|
|
213
|
+
# 프롬프트는 `-p` 바로 뒤(positional-first) — Stage 1 러너와 같은 계약(2026-09-05, variadic 플래그 뒤
|
|
214
|
+
# positional 은 먹힌다). FD_MODEL 은 Stage 1 과 같은 변수로 핀한다: /model 로 저장한 기본 모델이
|
|
215
|
+
# `claude -p` 무인 런에도 적용되므로, 무인 잡의 모델은 plist 가 명시한다(운영자 결정 2026-09-05).
|
|
216
|
+
"$CLAUDE_BIN" -p "$PROMPT" --permission-mode bypassPermissions ${FD_MODEL:+--model "$FD_MODEL"} >> "$LOG_FILE" 2>&1 &
|
|
214
217
|
CLAUDE_PID=$!
|
|
215
218
|
DEADLINE=$((SECONDS + ATTEMPT_TIMEOUT_SECS))
|
|
216
219
|
while kill -0 "$CLAUDE_PID" 2>/dev/null && [ "$SECONDS" -lt "$DEADLINE" ]; do
|
|
@@ -118,7 +118,7 @@ for pair in \
|
|
|
118
118
|
"CATALOG.md|CHANGELOG.md|PATHSPEC covers CATALOG" \
|
|
119
119
|
".github/workflows/validate.yml|.github/dependabot.yml|PATHSPEC covers workflow 정의 (yml 만)" \
|
|
120
120
|
"scripts/index_sync.py|scripts/notes.txt|PATHSPEC covers scripts/*.py" \
|
|
121
|
-
"package.json|package-lock.json|PATHSPEC covers package.json 이되 «리터럴» — lock 파일까지 삼키지 않는다"
|
|
121
|
+
"package.json|package-lock.json|PATHSPEC covers package.json 이되 «리터럴» — lock 파일까지 삼키지 않는다" # portability-noqa: string literal fed to spec_matches()'s glob-case test, never read from disk — same reason as test_heavy_classifier_lanes.sh:106
|
|
122
122
|
do
|
|
123
123
|
IFS='|' read -r pos neg label <<< "$pair"
|
|
124
124
|
ok=1
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""docs/map 의 archify 산출 HTML 에 붙이는 재생성-후 후처리 — 두 가지만 한다.
|
|
3
|
+
|
|
4
|
+
(1) reader-width floor 뷰어의 `MIN_READER_WIDTH = 960` 상수를 뷰포트 비례 하한으로 바꾼다.
|
|
5
|
+
archify 뷰어는 «스크롤 없이 다 담기» 를 목표로 폭을 줄이는데, 세로 예산이 모자라면
|
|
6
|
+
1728px 화면에서도 960px 로 바닥을 친다(실측 2026-09-06: 1728×950 → 960). 지도는
|
|
7
|
+
«한 화면에 담기» 보다 «넓게 읽기» 가 목적이라 하한을 화면 비례로 올린다.
|
|
8
|
+
실측 효과(1728×950, fh_process): 960 → 1382. 가로 오버플로 없음(1280×800 → 1216).
|
|
9
|
+
|
|
10
|
+
(2) SVG 재생성 HTML 안의 유일한 <svg> + 뷰어 <style> 을 심은 정적 벡터를 다시 만든다.
|
|
11
|
+
기존 커밋본 두 장(fh_trust.dataflow.svg · fh_assets.architecture.svg)에 대해
|
|
12
|
+
바이트 동일 재현을 known-pair 로 확인하고 만든 추출기다.
|
|
13
|
+
|
|
14
|
+
fail-closed: 기대한 리터럴이 없으면(렌더러 버전 드리프트) 종료코드 3 으로 멈춘다 —
|
|
15
|
+
조용히 «패치할 게 없었다» 로 넘어가면 다음 발행이 옛 동작으로 나간다.
|
|
16
|
+
|
|
17
|
+
usage: map_postprocess.py <file.html> [...] 실제 적용
|
|
18
|
+
map_postprocess.py --check <file.html> [...] 적용 여부만 보고(쓰기 없음)
|
|
19
|
+
exit: 0 적용/이미적용 · 3 리터럴 부재(드리프트) · 4 인자/파일 오류
|
|
20
|
+
"""
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
OLD = 'var MIN_READER_WIDTH = 960;'
|
|
25
|
+
NEW = ('var MIN_READER_WIDTH = Math.min(1440, '
|
|
26
|
+
'Math.max(960, Math.round(window.innerWidth * 0.80)));')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_svg(html: str) -> str:
|
|
30
|
+
styles = re.findall(r'<style[^>]*>(.*?)</style>', html, re.S)
|
|
31
|
+
if len(styles) != 1:
|
|
32
|
+
raise ValueError(f'expected exactly 1 <style> block, got {len(styles)}')
|
|
33
|
+
m = re.search(r'<svg\b.*?</svg>', html, re.S)
|
|
34
|
+
if not m:
|
|
35
|
+
raise ValueError('no <svg> block found')
|
|
36
|
+
svg = m.group(0).replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ', 1)
|
|
37
|
+
tag_end = svg.index('>') + 1
|
|
38
|
+
return ('<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
39
|
+
+ svg[:tag_end] + '<style><![CDATA[\n' + styles[0] + '\n]]></style>'
|
|
40
|
+
+ svg[tag_end:])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def process(path: str, check_only: bool) -> int:
|
|
44
|
+
try:
|
|
45
|
+
html = open(path, encoding='utf-8').read()
|
|
46
|
+
except OSError as exc:
|
|
47
|
+
print(f'ERROR {path}: {exc}', file=sys.stderr)
|
|
48
|
+
return 4
|
|
49
|
+
has_old, has_new = OLD in html, NEW in html
|
|
50
|
+
if not has_old and not has_new:
|
|
51
|
+
print(f'DRIFT {path}: neither the original nor the patched reader-width '
|
|
52
|
+
f'literal is present — archify version drift, patch NOT applied',
|
|
53
|
+
file=sys.stderr)
|
|
54
|
+
return 3
|
|
55
|
+
if check_only:
|
|
56
|
+
print(f'{"PATCHED" if has_new else "UNPATCHED"} {path}')
|
|
57
|
+
return 0
|
|
58
|
+
patched = html.replace(OLD, NEW) if has_old else html
|
|
59
|
+
svg_path = re.sub(r'\.html$', '.svg', path)
|
|
60
|
+
# 쓰기 전에 둘 다 만들어 둔다 — 중간에 실패하면 «반쯤 적용된 트리»가 남고,
|
|
61
|
+
# 특히 open(...,'w') 은 예외가 나기 전에 이미 대상을 0바이트로 잘라 놓는다(L6 가 잡았다).
|
|
62
|
+
try:
|
|
63
|
+
svg = build_svg(patched)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
print(f'ERROR {svg_path}: {exc} — nothing written', file=sys.stderr)
|
|
66
|
+
return 3
|
|
67
|
+
if has_old:
|
|
68
|
+
open(path, 'w', encoding='utf-8').write(patched)
|
|
69
|
+
print(f'PATCH {path}: reader-width floor -> viewport-proportional')
|
|
70
|
+
else:
|
|
71
|
+
print(f'SKIP {path}: already patched (idempotent)')
|
|
72
|
+
open(svg_path, 'w', encoding='utf-8').write(svg)
|
|
73
|
+
print(f'SVG {svg_path}')
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main(argv):
|
|
78
|
+
check_only = '--check' in argv
|
|
79
|
+
files = [a for a in argv if not a.startswith('--')]
|
|
80
|
+
if not files:
|
|
81
|
+
print(__doc__, file=sys.stderr)
|
|
82
|
+
return 4
|
|
83
|
+
worst = 0
|
|
84
|
+
for path in files:
|
|
85
|
+
worst = max(worst, process(path, check_only))
|
|
86
|
+
return worst
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == '__main__':
|
|
90
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# outbound_query_guard.sh — 나가는 질의의 **위생 린트(accidental-leak lint)**.
|
|
3
|
+
# 🟥 **보안 통제가 아니다.** cross-family(codex, 2026-08-21) 지적: 토큰 분할·동의어·경로 일부·
|
|
4
|
+
# base64 는 전부 통과하고, 이 가드는 **어떤 outbound 실행기에도 배선돼 있지 않다.** 즉 «우연한
|
|
5
|
+
# 유출»은 잡고 «의도적 우회»는 못 잡는다. 보안 경계라 부르려면 실제 실행기에 강제 연결하고
|
|
6
|
+
# 정규화·승인·로깅의 우회 모델을 따로 설계해야 한다 — 하지 않았다. 그렇게 문서화한다.
|
|
7
|
+
#
|
|
8
|
+
# ── 왜 (챔버 런 consolidation-path-gate, S#6) ──
|
|
9
|
+
# 세션이 «세계에 물어본다» 를 하려면 문제를 이름으로 불러야 한다. 그 이름이 내부 자산명이면
|
|
10
|
+
# 질의 자체가 **유출**이다. 후보 스펙에 이 가드가 0줄이었고 블라인드 challenger 가 S 등급으로 지목했다.
|
|
11
|
+
# `CLAUDE.md §Field-Harness Diagnostic` 의 절대 규칙: 내부 자산명·경로는 로그·코멘트·붙여넣기에도
|
|
12
|
+
# 안 나간다. 외부 질의는 그 목록의 «붙여넣기» 와 같은 급이고, **되돌릴 수 없다.**
|
|
13
|
+
#
|
|
14
|
+
# ── 새 로직 0줄 ──
|
|
15
|
+
# `scripts/psa_scan_lib.sh` 가 이미 «패턴 두 층 로딩 · 행 검증 · 토큰 판정» 을 소유한다. 그 헤더가
|
|
16
|
+
# caller 의 몫을 명시했다: **무엇을 스캔하는가**, 그리고 **계기 불완전 시 degrade 방향**.
|
|
17
|
+
# 이 파일은 그 둘만 정한다 — 스캔 대상 = 질의 문자열, degrade = **fail-closed**.
|
|
18
|
+
#
|
|
19
|
+
# ── degrade 가 왜 block 인가 (커밋 caller 와 반대) ──
|
|
20
|
+
# 같은 라이브러리를 쓰는 pre-commit 은 override 부재 시 **경고**한다 — 커밋은 로컬이고 되돌릴 수
|
|
21
|
+
# 있으니까. 외부 질의는 push/publish 와 같은 부류다: 나가면 끝이다.
|
|
22
|
+
# `CLAUDE.md §Irreversibility Gates` 의 표면-클래스 degrade 불변식 그대로.
|
|
23
|
+
#
|
|
24
|
+
# Usage: bash scripts/outbound_query_guard.sh "<질의 문자열>"
|
|
25
|
+
# echo "<질의>" | bash scripts/outbound_query_guard.sh
|
|
26
|
+
# Exit: 0 = clean(내보내도 된다) · 1 = HIT(차단) · 3 = NOT SCANNED(계기 불완전 → 차단)
|
|
27
|
+
# Override: OUTBOUND_QUERY_OK=1 — 명시 승인. 로그를 남긴다. 🟥 유출이 없다고 «확인했을 때만».
|
|
28
|
+
set -uo pipefail
|
|
29
|
+
FH="$(cd "$(dirname "$0")/.." && pwd)"
|
|
30
|
+
# 🟥 주입 가능하게 둔다 — cross-family 가 «레인이 실물 라이브러리를 옮긴다» 를 HIGH 로 지목했다.
|
|
31
|
+
# 주입점이 있으면 레인이 레포를 안 건드리고, 오염 상태 분기에도 도달할 수 있다.
|
|
32
|
+
LIB="${PSA_LIB_FILE:-$FH/scripts/psa_scan_lib.sh}"
|
|
33
|
+
DEFAULTS="${PSA_DEFAULTS_FILE:-$FH/.claude/rules/.public-surface-patterns.defaults}"
|
|
34
|
+
OVERRIDE="${PSA_OVERRIDE_FILE:-$FH/.claude/rules/.public-surface-patterns}"
|
|
35
|
+
# 🟥 주입점 하나 더 — **같은 논거의 네 번째 적용**이다(위 세 개가 «레인이 레포를 안 건드리게»
|
|
36
|
+
# 있는 것과 동일). 실측 2026-09-05: 이 로그 경로가 박혀 있어서 L4 는 ⓐ 매 selfcheck 마다 실제
|
|
37
|
+
# 레포의 tracks/_meta 에 행을 쌓았고 ⓑ tracks/ 가 없는 트리(= npm 설치본)에서는 append 가
|
|
38
|
+
# 조용히 실패해 **`+1` 단언이 그대로 빨개졌다**. 즉 이 가드를 출하하려면 이 줄이 먼저다.
|
|
39
|
+
# 기본값은 안 바뀐다.
|
|
40
|
+
# ⚠️ 우회 채널로 읽지 마라: OUTBOUND_QUERY_OK 자체가 이미 env 로 켜는 것이라, 로그 경로를
|
|
41
|
+
# 옮기는 것이 «없던 우회»를 만들지는 않는다. 다만 «강행은 기록된다»는 성질이 env 하나에
|
|
42
|
+
# 더 의존하게 된 것은 사실이고, 그건 잔여로 이름 붙인다.
|
|
43
|
+
OVERRIDE_LOG="${OUTBOUND_QUERY_LOG:-$FH/tracks/_meta/.outbound_query_override_log}"
|
|
44
|
+
|
|
45
|
+
Q="${1:-}"; [ -n "$Q" ] || Q="$(cat)"
|
|
46
|
+
if [ -z "${Q// /}" ]; then echo " ⏭ 빈 질의 — 스캔할 것이 없다" >&2; exit 0; fi
|
|
47
|
+
|
|
48
|
+
if [ ! -r "$LIB" ]; then
|
|
49
|
+
echo "🚫 OUTBOUND BLOCKED — 스캐너 라이브러리 부재: $LIB" >&2
|
|
50
|
+
echo " 계기가 없으면 «유출 없음» 이 아니라 **미측정**이다. 비가역 표면이라 막는다." >&2
|
|
51
|
+
exit 3
|
|
52
|
+
fi
|
|
53
|
+
# shellcheck source=/dev/null
|
|
54
|
+
. "$LIB"
|
|
55
|
+
|
|
56
|
+
psa_load "$DEFAULTS" "$OVERRIDE" >/dev/null 2>&1 || true
|
|
57
|
+
# 🟥 **반환값이 아니라 상태 변수를 본다.** psa_scan_lib.sh 헤더가 이 함정을 명시한다:
|
|
58
|
+
# "A caller that treats PSA_BAD_ROWS>0 or PSA_DEFAULTS_OK=0 as 'clean' has a hole"
|
|
59
|
+
# 초판이 정확히 그 caller 였고 — `psa_load` 의 rc 만 보고 defaults 부재를 통과시켰다.
|
|
60
|
+
# known-pair 레인 L3 가 잡았다(`defaults 부재 → got=0 expect=3`). 레인이 없었으면 조용히 fail-open.
|
|
61
|
+
# 🟥 상태값은 `case` 로 본다. `[ -ne ]` 는 비수치 값에서 셸 에러 후 조건 false 로 흘러
|
|
62
|
+
# **fail-open** 이 된다 — 공유 라이브러리가 같은 결함을 이미 겪고 `case` 로 막아뒀다(psa_scan_lib.sh).
|
|
63
|
+
case "${PSA_DEFAULTS_OK:-}" in
|
|
64
|
+
1) ;;
|
|
65
|
+
*)
|
|
66
|
+
echo "🚫 OUTBOUND BLOCKED — 공용 패턴층(defaults)이 없거나 비었다: $DEFAULTS" >&2
|
|
67
|
+
echo " 라이브러리 규정: 이 상태는 «설정 선택» 이 아니라 **BROKEN INSTRUMENT** 다." >&2
|
|
68
|
+
echo " fail-closed: 커밋 caller 는 경고하지만 외부 질의는 되돌릴 수 없다." >&2
|
|
69
|
+
exit 3 ;;
|
|
70
|
+
esac
|
|
71
|
+
case "${PSA_BAD_ROWS:-}" in
|
|
72
|
+
0) ;;
|
|
73
|
+
*) echo "🚫 OUTBOUND BLOCKED — 패턴 파일 형식오류 행 '${PSA_BAD_ROWS:-?}' — 부분 로드는 미측정이다" >&2
|
|
74
|
+
exit 3 ;;
|
|
75
|
+
esac
|
|
76
|
+
# 🟥 **운영자 내부층(override)이 없으면 clean 을 말할 수 없다** (cross-family HIGH#1).
|
|
77
|
+
# 라이브러리 규정상 HIGH 내부 리터럴은 그 층에 산다. defaults 만으로 통과시키면 «보편 패턴만
|
|
78
|
+
# 본 것» 을 «내부 토큰 없음» 으로 렌더한다 — 초판이 그 caller 였다.
|
|
79
|
+
case "${PSA_OVERRIDE_PRESENT:-}" in
|
|
80
|
+
1) ;;
|
|
81
|
+
*) echo "🚫 OUTBOUND BLOCKED — 운영자 내부 패턴층(override) 부재: $OVERRIDE" >&2
|
|
82
|
+
echo " defaults 만으로는 «내부 토큰 없음» 을 말할 수 없다 — 미측정이다." >&2
|
|
83
|
+
exit 3 ;;
|
|
84
|
+
esac
|
|
85
|
+
# 🟥 계기 생사 — 라이브러리가 카나리아 자체검증을 갖고 있다. 「초록인데 대상을 안 잰다」를 막는다.
|
|
86
|
+
if ! psa_require_live >/dev/null 2>&1; then
|
|
87
|
+
echo "🚫 OUTBOUND BLOCKED — psa_require_live 실패: 스캐너가 알려진 토큰도 못 잡는다" >&2
|
|
88
|
+
echo " 계기가 죽었다. 통과가 아니라 **미측정**이다." >&2
|
|
89
|
+
exit 3
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# 🟥 **개행·탭을 먼저 죽인다** (cross-family HIGH#3). `psa_scan_tagged` 는 줄마다 `path<TAB>body`
|
|
93
|
+
# 로 읽으므로, 질의에 개행+탭이 있으면 **토큰이 path 필드로 들어가 스캔 대상에서 빠진다.**
|
|
94
|
+
# 실제 우회 경로였다. 한 레코드로 눌러 넣는다.
|
|
95
|
+
Q_FLAT="$(printf '%s' "$Q" | tr '\n\t\r' ' ')"
|
|
96
|
+
# 🟥 P3 동형: 조건문 안의 대입이라야 `set -e` 아래서 안 죽는다.
|
|
97
|
+
if OUT="$(printf 'outbound-query\t%s' "$Q_FLAT" | psa_scan_tagged 2>&1)"; then RC=0; else RC=$?; fi
|
|
98
|
+
# 🟥 P2 (cross-family 3라운드): 초판은 «비영이면 override 로 통과» 였다. 그러면 **rc=3 미측정**
|
|
99
|
+
# 까지 승인된 것처럼 지나간다 — pre-commit·pre-push 에서 닫은 바로 그 구멍이 **세 번째
|
|
100
|
+
# 호출부에 그대로** 있었고, 나는 그것을 「호출부 셋 다 닫혔다」고 적었다(전수 주장이 틀렸다).
|
|
101
|
+
# override 는 «봤고 괜찮다» 는 뜻이라 **본 것(rc=1)에만** 쓸 수 있다. 안 본 것(rc=3)엔 못 쓴다.
|
|
102
|
+
# 🟥 R4-4/5: 판정점을 **하나로** 만든다. 초판은 «rc=3 선차단» + «override 에 `-le 1` 조건»
|
|
103
|
+
# 두 겹이었는데, 앞이 막아버려 뒤 술어는 **되돌려도 레인이 안 빨개졌다**(장식 앵커).
|
|
104
|
+
# 그리고 rc=2 는 어느 쪽도 안 잡아 **«유출 발견»(exit 1)으로 오분류**됐다 — 안 본 것을
|
|
105
|
+
# 본 것으로 렌더하는 방향이다. 「측정된 값」은 0 과 1 뿐이고 **나머지는 전부 미측정**이다.
|
|
106
|
+
case "$RC" in
|
|
107
|
+
0|1) ;;
|
|
108
|
+
*) echo "🚫 OUTBOUND BLOCKED — 스캐너가 측정값을 안 냈다 (rc=$RC). NOT SCANNED 는 깨끗함이 아니다." >&2
|
|
109
|
+
echo " OUTBOUND_QUERY_OK 로도 통과 못 한다 — 승인은 «봤다» 는 뜻이고, 이건 안 본 것이다." >&2
|
|
110
|
+
echo "$OUT" >&2
|
|
111
|
+
exit 3 ;;
|
|
112
|
+
esac
|
|
113
|
+
if [ "$RC" -ne 0 ] || [ -n "$OUT" ]; then
|
|
114
|
+
# 여기 도달했다는 것은 위 `case` 에 의해 RC ∈ {0,1} 이 **구조적으로 보장**된 상태다.
|
|
115
|
+
# 초판의 `&& [ "$RC" -le 1 ]` 는 그래서 중복이었고, 중복은 앵커가 안 붙는다 — 지운다.
|
|
116
|
+
if [ "${OUTBOUND_QUERY_OK:-0}" = "1" ]; then
|
|
117
|
+
printf '%s\tOUTBOUND_QUERY_OK\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${Q:0:60}" \
|
|
118
|
+
>> "$OVERRIDE_LOG" 2>/dev/null || true
|
|
119
|
+
echo "⚠️ OUTBOUND — 히트가 있었으나 OUTBOUND_QUERY_OK=1 로 강행됨 (로그 기록됨)" >&2
|
|
120
|
+
echo "$OUT" >&2
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
123
|
+
echo "🚫 OUTBOUND BLOCKED — 나가는 질의에 내부 토큰이 있다:" >&2
|
|
124
|
+
echo "$OUT" >&2
|
|
125
|
+
echo " 고쳐라: 문제를 **일반 어휘**로 다시 써라 (내부 이름 대신 그 이름이 가리키는 «형태»)." >&2
|
|
126
|
+
echo " 예) «우리 X 게이트가…» → «커밋 전에 근거 필드를 강제하는 훅 패턴»" >&2
|
|
127
|
+
echo " 확인 후 강행: OUTBOUND_QUERY_OK=1 bash scripts/outbound_query_guard.sh \"<질의>\"" >&2
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
echo " ✅ outbound clean — 내부 토큰 없음" >&2
|
|
131
|
+
exit 0
|