Metadata-Version: 2.4
Name: fusegate
Version: 0.1.1
Summary: Claude Code 세션 정책 엔진 — 에이전트 폭주(재귀 스폰·예산 소진·반복 루프)를 실행 전에 차단
Project-URL: Homepage, https://github.com/calintzy/fusegate
Project-URL: Repository, https://github.com/calintzy/fusegate
Project-URL: Issues, https://github.com/calintzy/fusegate/issues
Author-email: calintzy <byjrasid@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: agent,claude-code,guardrail,hooks,policy,runaway
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# fusegate

[![tests](https://github.com/calintzy/fusegate/actions/workflows/test.yml/badge.svg)](https://github.com/calintzy/fusegate/actions/workflows/test.yml)
[![PyPI](https://img.shields.io/pypi/v/fusegate)](https://pypi.org/project/fusegate/)

Claude Code 세션에 다는 정책 엔진. 재귀 스폰, 예산 소진, 반복 루프 같은 에이전트 폭주를
**실행되기 전에** 차단한다.

이름 그대로다: **퓨즈(fuse)를 단 관문(gate)**. 과부하가 나면 집 전체가 타기 전에 두꺼비집의
퓨즈가 먼저 끊어진다 — 그 퓨즈를 에이전트 실행 관문에 단다. 세션이 정책 상한을 넘는 순간,
해당 실행만 그 자리에서 끊긴다.

## 왜 만들었나

2026년 6월, Claude Code에서 서브에이전트가 재귀로 50단계까지 스스로를 복제하며 5분 만에 약
400만 토큰을 태운 사례가 보고됐다([anthropics/claude-code#68619](https://github.com/anthropics/claude-code/issues/68619),
아직 OPEN). 같은 시기 AI 에이전트가 프로덕션 DB를 삭제한 사건들도 잇따랐다 — Cursor 에이전트가
Railway API 호출로 DB와 백업을 수 초 만에 지운 건과, Replit 에이전트가 코드 프리즈 중 무승인
파괴를 일으켜 CEO가 공개 사과한 건은 **서로 다른 사건**이다(언론이 혼동해 보도한 전례가 있어
구분해 적는다 — AI Incident DB #1152, #1469).

피해자들의 결론은 한결같다: *프롬프트에 적은 규칙은 제안일 뿐이다. 강한 제약이 필요하면
시스템 수준의 경계가 필요하다.* 모니터링 도구는 많지만 전부 표시 전용이다 — fusegate는
표시하지 않고 **강제**한다.

## 무엇을 하나

`fusegate init` 한 번으로 프로젝트에 훅 4종이 걸리고, 이후 모든 툴 실행 직전(PreToolUse)에
정책을 평가한다.

| 규칙 | 내용 | 스코프 |
|---|---|---|
| `depth` | 재귀 스폰 깊이 상한 (기본 1 — 아래 실측 근거) | 세션 |
| `concurrent_session` | 세션 내 동시 활성 서브에이전트 수 상한 | 세션 |
| `concurrent_global` | 프로젝트 전역(모든 세션 합산) 동시 수 상한 | 전역 |
| `budget_session` | 세션 토큰 예산 — **추정 기반**(트랜스크립트 파싱) | 세션 |
| `budget_subagent` | 서브에이전트 단위 토큰 예산(추정 기반) | 에이전트 |
| `repeat` | 동일 툴 호출의 연속 반복 상한 | 세션 |

- **enforce / warn 모드** — 규칙별로 고를 수 있다. enforce는 차단하고, warn은 차단 없이 모델이
  관측 가능한 경고를 주입해 자가 교정을 유도한다.
- **예산 위반은 확장만 차단** — Agent 스폰만 막고 Bash·편집 등은 통과시켜, 진행 중 작업의
  마무리(커밋·정리)를 잃지 않게 한다.
- **차단 메시지가 재시도를 막는다** — 차단 시 모델에게 "재시도하거나 다른 방법으로 우회하지
  말 것, 정지하고 사용자에게 보고할 것"을 지시한다. 거부→재스폰 패턴(#68619의 악화 경로)을
  막는 실측된 문구다.
- **fail-open** — 엔진이 어떤 식으로 고장 나도(정책 문법 오류, DB 손상, 버그) 세션은 절대 막히지
  않는다. 통과시키고, 경고하고, 기록한다.
- **위반 텔레메트리** — 모든 판정이 `violations` 테이블과 `events.jsonl`에 남는다(차단, 경고,
  킬스위치 통과, 엔진 장애 통과를 구분).
- **관측** — `fusegate status`(터미널)와 `fusegate dashboard`(127.0.0.1 전용, 완전 읽기 전용
  로컬 대시보드)로 위반, 활성 에이전트, 예산 추정을 조회한다.
- **킬스위치** — `FUSEGATE_DISABLE=1` 하나뿐이다. 끄는 행위는 항상 명시적이고 기록된다.
  몰래 우회하는 경로는 만들지 않았다.

### 대시보드 화면

| 위반 목록 | 타임라인 | 예산 |
|---|---|---|
| ![위반 목록 뷰](probes/evidence/ISC-D3/dashboard.png) | ![타임라인 뷰](probes/evidence/ISC-D3/dashboard-timeline.png) | ![예산 뷰](probes/evidence/ISC-D3/dashboard-budget.png) |

## 네이티브 상한과 뭐가 다른가

Claude Code에는 이미 전역 env var 상한이 있다: `CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`,
`CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH`, `CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION`. 쓰고 있다면
계속 쓰면 된다 — fusegate는 이 변수들을 건드리지 않고, 둘 다 설정돼 있으면 보수적인 쪽이 먼저
걸린다.

fusegate의 가치는 상한의 유무가 아니라 **정책 계층**이다: 전역 고정 노브가 아닌 프로젝트별
정책 파일, 규칙별 enforce/warn 선택, 예산과 반복 같은 상한 밖 규칙, 그리고 무엇이 언제 왜
차단됐는지의 위반 텔레메트리 — 벤더가 버그를 고치고 상한을 늘려도 프로젝트마다 다른 "정책"은
남는다.

## 실측: 무엇이 이 폭주를 실제로 끊나

재귀 스폰 폭주(#68619 서사)를 가드 없음 / depth=2 / depth=1 세 조건으로 대조 재현했다.
**n=1 실측이며 모든 수치는 추정치다** — 재현 스크립트와 원본 증거는
[`measurements/`](measurements/RUNAWAY_2026-07-30.md)에 있다(안전 상한 내장).

| | 스폰 수 | 차단 | 중단 지점 | 토큰(추정)\* | 비용(추정, CLI 보고) |
|---|---|---|---|---|---|
| 가드 없음 | 4 | 0 | 자연 종료(안전 상한 내 관측) | 783,109 | $0.19 |
| depth=2 enforce | 5 | 0 | 자연 종료 — **차단 실패** | 1,040,701 | $0.25 |
| **depth=1 enforce** | **1** | **1** | **정책 차단** | 310,464 | $0.14 |

\* 토큰 수치는 stream-json usage 4개 카테고리 합산이라 **캐시 재사용(cache_read)이 포함된
누적 usage**다. 신규 처리량이나 절감률 계산의 근거로 읽지 말 것 — 비용 감각은 CLI 보고 USD가
더 정확하다(두 추정 소스의 차이는 measurements/ 문서 참조).

이 실측의 핵심 발견은 차단 성공이 아니라 **depth=2의 차단 실패**다. 부모-자식 페어링 필드가
훅 페이로드에 없어 깊이를 동시 활성 수로 근사하는데, 부모가 먼저 종료되는 순차 사슬은 이
근사에 걸리지 않는다. 재귀 재스폰을 실제로 막는 설정은 depth=1뿐이었고, **기본값이 1인
이유**다. 이 한계는 문서에 공개돼 있다(`docs/tracking/findings.md`).

## 설치와 사용

```bash
pip install fusegate         # PyPI: https://pypi.org/project/fusegate/
cd <보호할 프로젝트>
fusegate init                # .fusegate/ 생성 + .claude/settings.json 훅 병합(백업, 멱등)
```

정책은 `.fusegate/policy.toml` 하나로 조정한다(편집 즉시 적용):

```toml
[fusegate]
mode = "enforce"             # 전역 기본: "enforce" | "warn"

[rules.depth]
limit = 1                    # 규칙 테이블을 지우면 그 규칙은 비활성
[rules.budget_session]
limit_tokens = 2_000_000     # 추정 기반
# 모든 규칙에 mode = "warn" 오버라이드 가능
```

일시 해제: `FUSEGATE_DISABLE=1`. 위반 확인: `.fusegate/events.jsonl`.

## 정직하게 밝혀두는 한계

- **예산은 추정이다.** 트랜스크립트에 기록된 usage를 파싱한 누적치라 실제 청구와 다를 수 있고,
  아직 기록되지 않은 사용량만큼 뒤처질 수 있다. 과금 명세로 쓰지 말 것.
- **깊이는 근사다.** 훅 페이로드에 부모-자식을 잇는 필드가 없어 동시 활성 수로 근사한다.
  limit=1은 정확히 동작하지만("메인의 직속 스폰만 허용"), 2 이상은 순차 사슬형 폭주를 놓칠 수
  있다.
- **CLI 2.1.215 실측 스냅샷 기반이다.** 훅 계약(이벤트, 필드명, 차단 지점)은 버전업 시 재검증이
  필요하다.
- **fail-open이 철학이다.** 게이트가 죽으면 보호도 사라진다 — 대신 세션은 절대 막히지 않고,
  장애는 반드시 기록·경고로 드러난다.
