kisenon
Agent-Safe Change Control

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_conflictsignore, require_human, block

ignoreon_conflicts에서 유효하며, 클래스 액션으로는 결코 유효하지 않습니다.

기본 태세promote_mode에서 오고, 오버레이가 클래스별로 재정의합니다:

promote_mode모든 클래스의 기본값
selfauto_promote
humanrequire_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에 있습니다.

이것들이 어떻게 맞물리는가

  • LINTBLAST데이터를 생성합니다 — 하나는 정적, 하나는 측정된 것.
  • POLICY는 그 데이터를 결정으로 바꿉니다. 페일클로즈드이며, block이라고 말할 때 사람의 클릭으로 재정의할 수 없습니다.
  • LEASHSCOPE는 에이전트가 전혀 초과할 수 없는 하드 리밋입니다 — 포크에서 집행되고, 생성 시 고정됩니다.

이 다섯이 흔히 잘못 읽히는 방식은 Common pitfalls를 참조하세요.