Guardrails
예산, 정책 등급, 범위가 제한된 권한, 위험 린트, 폭발 반경 검사 — 에이전트가 당신을 해치기 전에 무엇이 그것을 멈추는가.
다섯 개의 가드레일이 AI 에이전트 샌드박스를 둘러쌉니다. 둘은 에이전트가 물리적으로 초과할 수 없는 하드 리밋이고(LEASH, SCOPE), 하나는 데이터를 판정으로 바꾸며(POLICY), 둘은 그것이 저울질할 데이터를 생성합니다(LINT, BLAST). 어느 것도 신뢰 경로에 LLM을 두지 않습니다.
LEASH — 샌드박스별 예산
차단하는 것: 폭주하는 에이전트 — 경계 없이 문장, 컴퓨트, 벽시계 시간을 태우는 에이전트.
샌드박스를 생성하거나 실행할 때 예산을 설정하세요:
keon sandbox create \
--budget-statements 500 \
--budget-compute-seconds 120 \
--budget-wall-seconds 900
# or the same three flags on: keon sandbox run …위반 시 샌드박스는 자체 종료합니다: 상태가 discarded가 되고
budget_breached가 어떤 한도가 터졌는지 지목합니다 — statements,
compute_seconds, 또는 wall_clock. CLI는 오류 코드
sandbox_budget_breached로 종료합니다. 어떤 예산이든 **80%**에서 경고가
발생합니다.
와이어 상에서 예산은 budgets 객체로, 예산당 하나의 항목입니다:
"budgets": {
"statements": { "limit": 500, "used": 218, "warned": false },
"compute_seconds": { "limit": 120, "used": 41, "warned": false },
"wall_clock": { "used": 63 }
}limit이 없으면 무제한을 의미합니다 — 잘못 읽을 센티넬 값이 없습니다.
기본값과 상한 구성(소유자/관리자):
# Per-project defaults double as the create-time cap.
curl -X PATCH .../v1/projects/{id} \
-d '{"sandbox_budget_defaults": {"statements": 500, "compute_seconds": 120}}'기본값이 곧 상한입니다: 생성 시점에 프로젝트 기본값보다 큰 값을 요청하면
422 sandbox_budget_exceeds_cap을 반환합니다. 프로젝트별 기본값은 구성
가능합니다 — 여기 인용할 고정 숫자는 없습니다.
두 가지 불변식:
- 생성 시 고정. 예산은 샌드박스가 생성될 때 고정됩니다. 이후 프로젝트 기본값을 변경해도 라이브 샌드박스를 다시 옭아매지 않습니다.
- 에이전트 키는 조이기만 가능. 에이전트 기능 키는 생성 시 더 조인 예산을 요청할 수 있지만, 프로젝트 기본값을 설정할 수는 결코 없습니다.
POLICY — 프로모트 정책 등급
차단하는 것: 프로젝트가 허용하는 것보다 위험한 변경의 프로모트 — 에이전트 (또는 사람)가 통과시키려 시도하는 것조차.
POLICY는 위험 클래스를 액션에 매핑하는 프로젝트별 오버레이로, 기본
promote_mode 위에 겹쳐집니다.
구성(CLI):
keon projects promote-policy <projectId> # show effective policy
keon projects promote-policy <projectId> \
--set destructive=block \
--set unbounded=require_human \
--on-conflicts require_human # merge-write
keon projects promote-policy <projectId> --clear # drop the overlay--set <class=action>은 반복 가능하며 기존 오버레이에 병합됩니다.
구성(API): GET / PUT / DELETE /v1/projects/{projectId}/promote-policy:
{ "version": 1,
"rules": { "destructive": "block", "unbounded": "require_human" },
"on_conflicts": "require_human" }| 필드 | 유효한 값 |
|---|---|
| 위험 클래스 | additive, mutating, destructive, unbounded, rewrite |
| 액션 | auto_promote, require_human, block |
on_conflicts | ignore, require_human, block |
ignore는 on_conflicts에서만 유효하며, 클래스 액션으로는 결코 유효하지
않습니다.
기본 태세는 promote_mode에서 오고, 오버레이가 클래스별로 재정의합니다:
promote_mode | 모든 클래스의 기본값 |
|---|---|
self | auto_promote |
human | require_human |
각 유효 규칙은 어디서 왔는지 보고합니다 — source: overlay, promote_mode,
또는 default.
차단될 때 에이전트가 보는 것: 차단된 프로모트는 결정 세부 정보가 첨부된
409 promote_blocked_by_policy를 반환합니다.
핵심 규칙: 사람의 Approve는 정책 block을 결코 재정의할 수 없습니다.
정책은 승인 시점에 재평가되므로, block은 클릭에 맞서 유지됩니다. 정책 검증은
페일클로즈드/엄격입니다 — 파싱할 수 없는 정책은 거부하며, 결코 열림으로
저하되지 않습니다.
SCOPE — 범위가 제한된 권한(레인)
차단하는 것: 선언한 스키마와 테이블 — 에이전트의 레인 — 밖의 모든 읽기나 쓰기.
구성(생성 시점만):
keon sandbox create \
--schemas app,analytics \
--tables app.users,app.orders
# omit both flags → unscoped (full-branch authority)--schemas 없는 --tables는 클라이언트 측에서 invalid_lane로 거부됩니다.
포크 안에서 에이전트는 레인 밖으로 물리적으로 쓸 수 없습니다 — 레인 밖 쓰기는
포크 내에서 SQLSTATE 42501(권한 부족) 또는 3F000(잘못된 스키마 이름)으로
실패합니다. 레인 밖 읽기도 거부됩니다. 그리고 프로모트는 모든 레인 밖 변경을
409 sandbox_out_of_lane로 거부합니다.
와이어 상에서 샌드박스는 scope 객체를 담습니다(없음 = 범위 제한 없음):
"scope": { "v": 1, "schemas": ["app", "analytics"], "tables": ["app.users", "app.orders"] }keon sandbox diff는 각각 이유 — out_of_lane_schema, out_of_lane_relation,
또는 unresolvable_targets — 를 가진 violations[]를 나열하는 lane 객체를
얻습니다.
두 가지 한계:
- 생성 후 불변. 레인을 넓히려면 버리고 재생성해야 합니다 — 제자리 확장은 없습니다.
- 직접 대상만. 레인은 트리거나
SECURITY DEFINER함수를 통해 도달하는 전이적 부작용을 관장하지 않습니다.
LINT — 정적 위험 분류
생성하는 것: 재생 집합의 모든 문장에 대한 정적 위험 클래스 — 실행 없음, LLM 없음, 행 수 없음. 순전히 텍스트 기반입니다.
새 명령은 없습니다. LINT는 이미 읽고 있는 세 곳에서 표면화됩니다:
keon sandbox diff안에서risk_report로,keon sandbox log에서 액션별로,- 샌드박스 와이어의
max_risk_class로.
클래스, 낮은 심각도에서 높은 심각도 순:
additive < mutating < rewrite < unbounded < destructive빈 재생 집합은 none으로 분류됩니다.
리포트 형태:
{ "statements": [
{ "seq": 4, "risk_class": "unbounded",
"reasons": ["where_trivially_true: DELETE guarded by WHERE 1=1 matches every row"] }
],
"rollup": { "max_class": "unbounded", "counts": { "additive": 2, "unbounded": 1 } } }각 이유는 <snake_case_code>: <sentence>입니다. 예를 들어, WHERE true 또는
WHERE 1=1은 문장을 이유 where_trivially_true와 함께 unbounded로
분류합니다.
기억할 두 가지:
- LINT는 데이터이지 결정이 아닙니다. 그것은 분류하고, POLICY가 집행합니다. LINT는 결코 스스로 무언가를 차단하지 않습니다.
- 위로 실패합니다. 불확실할 때 LINT는 가장 그럴듯한 최악의 클래스를
할당합니다. 탐지가 텍스트 기반이므로 실제 행 수는 입력이 아닙니다 —
WHERE없는DELETE는 테이블에 한 행이 있든 10억 행이 있든destructive로 읽힙니다.
BLAST — 폭발 반경 드라이런
생성하는 것: 프로모트가 실제로 무엇을 할지에 대한 측정된 증거 — 실제 락 수준, 실제 락 보유 시간, 실제 행 수, 그리고 포크 이후의 모든 발산.
keon sandbox dry-run <id>샌드박스의 정확한 문장 집합을 부모의 두 개의 일회성 포크에 대해 재생하고,
실제 비용을 측정하며, 발산을 탐지한 다음, 둘 다 파괴합니다. main에는 결코
손대지 않으며, 에이전트는 포크 자격 증명을 결코 받지 않습니다. BLAST는
권고입니다 — 결코 프로모트를 차단하지 않으며, 충돌은 데이터로
표면화됩니다.
전체 필드 참조, 충돌 의미론, 엔드포인트 목록은 Sandboxes & promote → Blast-radius dry-run에 있습니다.
이것들이 어떻게 맞물리는가
- LINT와 BLAST는 데이터를 생성합니다 — 하나는 정적, 하나는 측정된 것.
- POLICY는 그 데이터를 결정으로 바꿉니다. 페일클로즈드이며,
block이라고 말할 때 사람의 클릭으로 재정의할 수 없습니다. - LEASH와 SCOPE는 에이전트가 전혀 초과할 수 없는 하드 리밋입니다 — 포크에서 집행되고, 생성 시 고정됩니다.
이 다섯이 흔히 잘못 읽히는 방식은 Common pitfalls를 참조하세요.