FL_30 Fail-closed Gates (라이선스·성능·런타임·렌더·퍼블리시·완전성)
한 줄 요약
FableLayer의 Fail-closed Gates는 “증거 없으면 무조건 차단”을 기본값으로 삼는 셸 스크립트 검문소 묶음으로, 마크다운 명세만으로는 절대 통과할 수 없도록 라이선스·성능주장·런타임·렌더·퍼블리시·완전성을 각각 별도 exit 코드로 막는다. 왜 배우나: 하네스에서 “통과는 어렵게, 실패는 안전하게”라는 게이트 설계 원칙을 가장 작고 읽을 수 있는 형태로 보여주기 때문이다.
그림
flowchart TD
V["verify_fablelayer.sh<br/>마스터 게이트 (OR 집계)"] --> S{"필수 문서 존재?<br/>REQUIREMENTS/README/LICENSE 등"}
S -->|없음| F1["fail structure (RC=1)"]
S -->|있음| G
subgraph G["서브게이트 (각각 HARD)"]
L["license_gate.sh<br/>FL7 / exit 2"]
P["perf_claim_gate.sh<br/>FL9 / exit 2"]
B["bench_integrity_gate.sh<br/>FL6 / exit 2"]
C["completeness_gate.sh<br/>FL13 / exit 3"]
R["render_gate.sh<br/>FL18 / exit 2"]
RT["runtime_gate.sh<br/>FL20 / exit 2"]
end
G --> PUB{"mode = publish?"}
PUB -->|예| PG["publish_gate.sh<br/>FL15 / exit 2<br/>승인토큰 AND 전체green"]
PUB -->|아니오| AGG
PG --> AGG["RC 집계 (하나라도 비0 = FAIL)"]
G --> AGG
AGG -->|"RC=0"| OK["VERIFY PASS"]
AGG -->|"RC!=0"| FAIL["VERIFY FAIL:실패목록"]
PRE["preflight_gate.sh<br/>FL17 / exit 4<br/>(루트 단독점유 확인)"] -.선행.-> V
쉽게 풀기
공항 보안 검색대를 떠올리면 된다. FableLayer는 결과물(프롬프트 제품)을 세상에 내보내기 전에 여러 검문대를 통과시킨다.
- “Fail-closed(실패 시 닫힘)“는 검문대 직원이 애매하면 무조건 막는다는 뜻이다. 증거가 없으면 “아마 괜찮겠지”가 아니라 “통과 불가”가 기본값이다. 평소 소프트웨어는 에러 나면 그냥 넘어가는 경우가 많은데, 여기서는 반대다.
- 검문대는 7개 정도 있고 각각 다른 것을 검사한다. 짐 안에 베낀 남의 프롬프트가 있는지(license), “Opus랑 동급 95%“같은 근거 없는 자랑이 있는지(perf), 벤치 결과에 원자료와 한계 고백이 붙어 있는지(bench), 필요한 부품이 다 들어있는지(completeness), 설정 파일이 진짜 열리는지(render), 코드가 실제로 돌아가는지(runtime)를 본다.
- 가장 엄격한 검문대는 출국심사(publish)다. 여기는 “다른 모든 검문대가 초록불”이고 “사람이 직접 승인 도장(APPROVED)을 찍은” 두 조건을 동시에 만족해야만 외부 배포를 허용한다.
- 핵심 트릭 하나: runtime/completeness 게이트는 “마크다운 설명서만 써놓고 다 했다”고 우기는 걸 막는다. 실제 파이썬 파일이 컴파일되고 import되고 CLI가 실행돼야 통과한다. 설명서는 짐이 아니라 진짜 작동하는 물건만 통과시키는 셈이다.
단계로 보면: (1) preflight로 작업 폴더가 내 것인지 먼저 확인 → (2) verify가 필수 문서를 점검 → (3) 6개 서브게이트를 차례로 실행 → (4) publish 모드면 승인+전체초록 추가 확인 → (5) 하나라도 비0이면 전체 실패.
핵심 정리
각 게이트의 책임과 실패 코드를 한눈에 본다. exit 코드가 게이트마다 다른 이유는 마스터가 어떤 게이트가 막았는지 구분하기 위해서다.
| 게이트 | 막는 것(요지) | exit 코드 |
|---|---|---|
| license_gate (FL7) | 유출 프롬프트 복사 지시문·전문, LICENSE/NOTICE/ATTRIBUTION 누락, 비AGPL | 2 |
| perf_claim_gate (FL9) | bench 근거 없는 수치 성능주장, capability/procedure 구분 누락 | 2 |
| bench_integrity_gate (FL6) | RESULTS.md·원자료(json)·재현스크립트·한계 섹션 누락 | 2 |
| completeness_gate (FL13) | 모드별 필수 디렉터리/파일 누락, 알 수 없는 mode | 3 |
| render_gate (FL18) | 깨진 JSON 매니페스트, 실행 불가 CLI, README의 죽은 bench 링크 | 2 |
| runtime_gate (FL20) | syntax/import 오류, CLI 서브커맨드 실패, 단위테스트 실패 | 2 |
| publish_gate (FL15) | 승인토큰 부재 또는 전체 게이트 비초록 상태에서의 외부 배포 | 2 |
| preflight_gate (FL17) | 루트가 타 프로젝트/에이전트 점유 상태 | 4 |
[!note]- 마스터 게이트(verify_fablelayer.sh)의 집계 규칙
- 먼저
REQUIREMENTS.md README.md README.ko.md LICENSE NOTICE ATTRIBUTION.md ROADMAP.md FAILURE_LOG.md8종 필수 문서 존재를 확인하고, 없으면fail "structure"로 RC=1.- 서브게이트 실행 순서: license → perf → bench → completeness(—mode 전달) → render → runtime. 각 호출은
run_subgate로 감싸 비0이면FAILED목록에 이름을 추가하고 RC=1로 만든다(OR 집계).- 게이트 스크립트 파일 자체가 없으면
gate script absent로 역시 실패 처리(fail-closed).--mode publish일 때만 publish_gate를 배선한다. publish_gate는 내부에서 verify를--mode all로 호출하므로 무한루프가 없다.- FL8 fallback-zero:
$HOME/fable-forge/gates/fable_lint.sh가 있으면 core/styles/skills/agents/adapters를 린트하고, 그 게이트의 exit 2(HARD fallback 패턴)만 실패로 본다. 린트 스크립트가 없으면 WARN만 남기고 FL8은 미검증으로 둔다.
[!note]- selftest.sh — 게이트가 “막아야 할 걸 막는지”까지 검증 selftest는 fixtures/ 아래의 pass/fail 픽스처를 각 게이트에 먹여 기대 exit 코드와 일치하는지 본다. 예:
license_gate.sh license_fail 2,completeness_gate.sh complete_fail 3 --mode new,preflight_gate.sh preflight_fail 4,publish_gate.sh publish_fail 2. 즉 게이트는 PASS 픽스처는 0을, FAIL 픽스처는 정확히 그 게이트의 HARD 코드를 내야 셀프테스트를 통과한다. 하나라도 어긋나면SELFTEST FAIL.
실제 예시
성능주장 게이트의 핵심 로직. “수치 + 성능 키워드”가 동시에 있으면서 근거/정직성 마커가 없으면 차단한다.
# /home/seunghyeong/harness-fablelayer/gates/perf_claim_gate.sh
# unbacked perf claim lines: number (NN% or N점) + perf keyword,
# lacking bench ref / honesty marker / price-table context
bad=$(grep -nIE '([0-9]{2}%|[0-9]+ *점)' $FILES 2>/dev/null \
| grep -iE 'similar|동률|동등|능가|opus|fable|품질|quality|절감|saving|cheaper|score|점수|상회|tie' \
| grep -ivE 'bench/|\[bench|target|목표|미검증|추정|aspirational|claim-only|한계|limitation|noise|노이즈|예시|example|\$[0-9]|/1M|입력|출력|input \$|output \$|price|단가' )
if [ -n "$bad" ]; then
echo "PERF FAIL(2): unbacked performance claim(s):"
printf '%s\n' "$bad"
RC=2
fi
# capability vs procedure distinction must be present
if ! grep -liE 'capability' $FILES >/dev/null 2>&1; then
echo "PERF FAIL(2): README missing 'capability' (non-transfer) framing"; RC=2
fi
마크다운만으로는 통과 못 하게 만드는 런타임 게이트(GATE-04/05 해소). 실제 파이썬이 돌아야 한다.
# /home/seunghyeong/harness-fablelayer/gates/runtime_gate.sh
if [ -d fablelayer ]; then
if ! python3 -m compileall -q fablelayer >/dev/null 2>&1; then
echo "RUNTIME FAIL(2): syntax error in fablelayer/"; RC=2
fi
for f in fablelayer/*.py; do
base="$(basename "$f" .py)"; [ "$base" = "__init__" ] && continue
if ! python3 -c "import fablelayer.$base" >/dev/null 2>&1; then
echo "RUNTIME FAIL(2): import fablelayer.$base"; RC=2
fi
done
if [ -f fablelayer/cli.py ]; then
for sub in "--help" "status"; do
if ! python3 -m fablelayer.cli $sub >/dev/null 2>&1; then
echo "RUNTIME FAIL(2): cli $sub non-zero"; RC=2
fi
done
fi
fi
이중 조건 출국심사(publish). 승인토큰 AND 전체 게이트 초록을 모두 만족해야 한다.
# /home/seunghyeong/harness-fablelayer/gates/publish_gate.sh
approved=0
[ "$APPROVAL" = "--approved" ] && approved=1
if ls "$ROOT"/runs/*/APPROVED >/dev/null 2>&1; then approved=1; fi
if [ "$approved" -ne 1 ]; then
echo "PUBLISH FAIL(2): no approval token (FL15) — external publish/push/marketplace blocked"; RC=2
fi
# all gates must be green
if ! bash "$GATES_DIR/verify_fablelayer.sh" "$ROOT" --mode all >/dev/null 2>&1; then
echo "PUBLISH FAIL(2): verify_fablelayer not green — cannot publish"; RC=2
fi
요약 & 셀프체크
- 7개+1개 게이트는 모두
set -u로 시작하는 fail-closed 셸 스크립트이며, 게이트별로 exit 2/3/4를 달리 써서 마스터가 어떤 검문이 막았는지 구분한다. - runtime/completeness 게이트가 실제 파이썬 컴파일·import·CLI·테스트를 요구하므로 마크다운 명세만으로는 절대 통과할 수 없다.
- publish_gate만 유일하게 “사람 승인(APPROVED) + 전체 게이트 초록”이라는 이중 AND 조건을 걸어 외부 배포를 봉인한다.
셀프체크:
- license_gate, completeness_gate, preflight_gate의 실패 exit 코드는 각각 무엇이며 왜 서로 다르게 두었는가?
- perf_claim_gate가 “절감 30%“라는 문장을 통과시키려면 같은 줄에 어떤 종류의 단서가 있어야 하는가?
- publish_gate가 verify를
--mode all로 다시 부르는데도 무한루프가 생기지 않는 이유는?