지식위키

ouroboros · 가드레일: 샌드박스 클래스 · 권한 번역 · 플러그인 방화벽

ouroboros · 가드레일: 샌드박스 클래스 · 권한 번역 · 플러그인 방화벽

한 줄 요약

AI 에이전트가 내 컴퓨터를 얼마나 만질 수 있는지를 “결정 한 곳 + 번역 여러 곳 + 외부 출입 단일 정문”으로 통제하는 3중 안전장치다. 왜 배우나: AI에게 권한을 주되 폭주를 막는 설계는 모든 에이전트 도구의 생명선이고, ouroboros의 방식이 그 교과서적 모범이기 때문이다.

그림

flowchart TD
    A["세션 역할 확정<br/>인터뷰·코디·구현 등"] --> B["derive_sandbox_class<br/>엔진이 단 한 번 결정"]
    B --> C{"어느 백엔드인가?"}
    C -->|Claude| D[permission_mode 로 번역]
    C -->|Codex| E["--sandbox / --full-auto 로 번역"]
    C -->|Copilot| F[--allow-all-tools 로 번역]
    D & E & F --> G["build_child_env<br/>부모 마커 제거 + 깊이 +1"]
    G --> H[에이전트 CLI 실행]
    H --> I{"외부 플러그인 호출?"}
    I -->|예| J[invoke_plugin 단일 정문]
    J --> K["신뢰검사 → 1회 확인 → 해시 재계산 → 감사기록"]
    I -->|아니오| L[정상 작업 계속]

쉽게 풀기

이 가드레일은 한 문장으로 “누가 무엇을 정하고, 누가 그걸 옮겨 적기만 하는가”를 칼같이 나눈 설계다. 회사 건물의 보안 체계에 비유하면 이해가 빠르다.

  1. 출입 등급을 정하는 보안실 (엔진) 세션이 시작될 때 그 세션의 “역할”이 정해진다. 인터뷰·평가 역할이면 “구경만 가능”, 코디네이터면 “내 작업폴더만 손댐”, 구현 역할이면 “무제한”. 이 등급 판정은 건물 전체에서 딱 한 군데, 보안실에서만 내려진다. 코드로는 derive_sandbox_class() 함수 하나다. 등급의 이름표는 백엔드(Claude/Codex/Copilot)와 무관한 중립 단어 3개뿐이다: READ_ONLY, WORKSPACE_WRITE, UNRESTRICTED.

  2. 등급을 자기 카드리더 형식으로 바꾸는 층별 게이트 (어댑터) 각 층의 출입문(=각 AI 백엔드)은 카드리더 방식이 제각각이다. 보안실이 정한 “구경만 가능”이라는 등급을, Claude 게이트는 permission_mode=default로, Codex 게이트는 --sandbox read-only로, Copilot 게이트는 빈 도구목록으로 옮겨 적기만 한다. 게이트는 절대 “음, 이 사람은 사실 더 줘도 되겠네” 하고 스스로 다시 판단하지 않는다. 만약 새 등급이 생겼는데 게이트의 변환표에 없으면, 조용히 통과시키는 게 아니라 시끄럽게 오류를 내며 멈춘다(기본값이 “관대”가 아니라 “거절”).

  3. 부모 흔적을 지우고 깊이를 세는 출입증 발급 (자식 프로세스 격리) 에이전트가 또 다른 에이전트를 불러내는(spawn) 일이 있는데, 이때 부모의 런타임 표식을 환경변수에서 지운다. 자식이 부모인 척 다시 들어오는 걸 막기 위해서다. 또 호출이 한 단계 깊어질 때마다 깊이 카운터를 +1 하고, 기본 5단계를 넘으면 예외를 던진다. 끝없이 자기를 복제하는 “포크 폭탄”을 막는 천장이다.

  4. 외부 업체는 무조건 단일 정문 경비를 거침 (플러그인 방화벽) 외부 플러그인을 실행할 때는 invoke_plugin이라는 단 하나의 문(초크포인트)만 통과시킨다. 이 문에서 신뢰 검사 → 사용자 1회 확인 → 무결성 해시 재계산 → 감사 로그 기록을 강제한다. 핵심 규칙: 신뢰 검사에서 막히면 “실행됐다(plugin.invoked)“는 기록은 절대 남지 않는다. 차단은 차단으로만 기록된다.

핵심 정리

안전장치담당한마디로
샌드박스 등급 결정엔진(보안실)역할→등급, 한 곳에서만
권한 번역어댑터(층별 게이트)등급→CLI 깃발, 옮겨 적기만
자식 격리런타임부모 마커 제거 + 깊이 천장
플러그인 방화벽단일 초크포인트신뢰·확인·해시·감사 강제

[!note] 백엔드 중립 어휘 — SandboxClass 3종 (src/ouroboros/sandbox.py)

  • READ_ONLY (read_only): 호스트 상태 조회만, 변경 불가 — 인터뷰·평가 역할
  • WORKSPACE_WRITE (workspace_write): 작업폴더 파일만 변경, 외부 호스트 차단 — 코디네이터 역할
  • UNRESTRICTED (unrestricted): 승인·샌드박스 게이트 우회 — 모든 어댑터가 warning 의무

[!note] 엔진 정책 매핑 (orchestrator/policy.py)

  • PolicyContext.session_role (필수): 유일하게 결정을 구동하는 필드
  • PolicyContext.execution_phase (필수): 감사 이벤트에 단계 귀속용
  • PolicyContext.runtime_backend (선택): 감사·리플레이용, 현재 결정엔 미사용(전방 호환 훅)
  • _ROLE_SANDBOX_CLASS: INTERVIEW/EVALUATION→READ_ONLY, COORDINATOR→WORKSPACE_WRITE, IMPLEMENTATION→UNRESTRICTED

[!note] 백엔드 번역 표 (평탄 룩업, 정책 판단 0)

SandboxClassClaude permission_modeCodex CLICopilot CLI
READ_ONLYdefault--sandbox read-only--available-tools=
WORKSPACE_WRITEacceptEdits--full-auto--allow-all-tools
UNRESTRICTEDbypassPermissions--dangerously-bypass-approvals-and-sandbox--allow-all

[!note] 자식 프로세스 env 격리 (runtime/child_env.py)

  • OUROBOROS_DEPTH_ENV_KEY = _OUROBOROS_DEPTH: 중첩 spawn마다 +1, 초과 시 예외
  • DEFAULT_MAX_OUROBOROS_DEPTH = 5: 재귀 천장(fork bomb 방지)
  • DEFAULT_OUROBOROS_STRIP_KEYS = ("OUROBOROS_AGENT_RUNTIME","OUROBOROS_LLM_BACKEND"): 모든 백엔드가 제거(자식의 부모 런타임 재진입 차단)

[!note] 플러그인 방화벽 감사 이벤트 (plugin/firewall.py, schema 0.1 / hooks 0.3)

event_type발생 시점result.status
plugin.failed (blocked)신뢰 미충족·disable·digest 변경·확인 거부blocked / trust_subject_changed
plugin.invoked엔트리포인트 subprocess 직전success
plugin.permission_usedrequired:true 권한마다 1개씩success
plugin.completed종료코드 0success
plugin.failed (failed)비정상 종료·타임아웃·실행불가failed

신뢰 검사 차단 시 plugin.invoked절대 발생하지 않는다(잠긴 Q1 계약).

[!note] Lockfile 신뢰 주체 (plugin/lockfile.py)

  • name / version (필수): 플러그인 식별
  • source_type (선택, legacy 빈문자): local_path / plugin_home / first_party
  • source_identity (선택): 정규화된 repo URL 또는 절대 경로
  • artifact_digest (선택): 설치 산출물 canonical tree hash, sha256:<hex>

신뢰 주체 = (version, source.type, source_identity, artifact_digest) 튜플. 한 컬럼이라도 바뀌면 권한 부여가 무효화된다(코드 치환 방어).

모델은 이 어휘를 직접 보지 않는다

기억할 큰 그림 하나: 이 가드레일은 모델이 읽는 **프롬프트가 아니라, 모델을 감싸는 실행 봉투(envelope)**다. 모델 호출 직전에 호출 환경(권한 모드·CLI 깃발·env)을 정해 에이전트 프로세스를 띄우고, 모델이 도구를 쓰려 할 때 백엔드 런타임이 그 봉투 안에서만 허용/차단한다.

  • 언제: 세션 시작(역할 확정) 시 1회 샌드박스 결정 → 백엔드 어댑터 기동 시 번역 → 자식 CLI spawn 시 env 격리 → 플러그인 호출마다 방화벽 통과.
  • 어떻게: 결정은 derive_sandbox_class(context)로 단일 enum 산출. 번역은 각 *_permissions.py가 enum→CLI 형식. 주입 위치는 프롬프트 본문이 아니라 SDK permission_mode 인자 / CLI argv 깃발 / 자식 프로세스 환경변수.
  • 핵심 교훈: “정책=무엇을 허용하나”는 엔진이, “표현=각 모델 CLI에 어떻게 말하나”는 어댑터가 담당하는 엔진-어댑터 경계. 모델의 도구 표면(어떤 도구가 보이고 실행 가능한지, evaluate_capability_policy)과 부작용 한도(샌드박스)를 런타임이 이 봉투로 결정한다.

실제 예시

엔진은 역할을 받아 중립 등급 enum 하나만 내놓는다. “결정은 여기, 단 한 곳”이라는 원칙이 코드로 그대로 드러난다.

# src/ouroboros/orchestrator/policy.py
# 세션 역할 → 백엔드 중립 샌드박스 클래스. 엔진 소유, 단 한 곳의 결정.
_ROLE_SANDBOX_CLASS: dict[PolicySessionRole, SandboxClass] = {
    PolicySessionRole.INTERVIEW: SandboxClass.READ_ONLY,
    PolicySessionRole.EVALUATION: SandboxClass.READ_ONLY,
    PolicySessionRole.COORDINATOR: SandboxClass.WORKSPACE_WRITE,
    PolicySessionRole.IMPLEMENTATION: SandboxClass.UNRESTRICTED,
}

def derive_sandbox_class(context: PolicyContext) -> SandboxClass:
    """엔진의 권위적 답: '이 세션은 어느 샌드박스 레벨인가?'
    어댑터는 이 enum을 룩업 표로 번역만 할 뿐, 자유형 권한 문자열로
    결정을 재계산해서는 안 된다."""
    return _ROLE_SANDBOX_CLASS[context.session_role]

어댑터는 그 enum을 자기 CLI 형식으로 옮겨 적기만 한다. 표에 없으면 조용히 봐주지 않고 시끄럽게 실패하고, 무제한 등급이면 반드시 경고를 남긴다.

# src/ouroboros/claude_permissions.py
# 엔진 SandboxClass → Claude SDK permission_mode (번역 전용, 판단 없음).
_SANDBOX_TO_CLAUDE_MODE: dict[SandboxClass, ClaudePermissionMode] = {
    SandboxClass.READ_ONLY: "default",
    SandboxClass.WORKSPACE_WRITE: "acceptEdits",
    SandboxClass.UNRESTRICTED: "bypassPermissions",
}

def claude_permission_mode_for_sandbox(sandbox: SandboxClass) -> ClaudePermissionMode:
    mode = _SANDBOX_TO_CLAUDE_MODE.get(sandbox)
    if mode is None:  # enum이 늘었는데 표를 안 고치면 조용히 관대해지는 대신 시끄럽게 실패
        raise KeyError(f"No Claude SDK permission_mode registered for {sandbox!r}")
    if sandbox is SandboxClass.UNRESTRICTED:
        log.warning("permissions.bypass_activated", sandbox=sandbox.value)
    return mode

직접 만들 때는 같은 골격을 그대로 베끼면 된다 — 중립 어휘 → 단일 결정 → 평탄 번역.

# my_guardrails.py — 결정/번역 분리 최소 구현
from enum import StrEnum

class SandboxClass(StrEnum):          # ① 백엔드 중립 어휘 (3개 고정)
    READ_ONLY = "read_only"
    WORKSPACE_WRITE = "workspace_write"
    UNRESTRICTED = "unrestricted"

_ROLE_TO_SANDBOX = {                   # ② 엔진: 단 한 곳에서 결정
    "interview": SandboxClass.READ_ONLY,
    "coordinator": SandboxClass.WORKSPACE_WRITE,
    "implementation": SandboxClass.UNRESTRICTED,
}
def derive(role: str) -> SandboxClass:
    return _ROLE_TO_SANDBOX[role]      # 자유형 문자열로 재판단 금지

_TO_MYBACKEND = {                      # ③ 어댑터: 평탄 룩업 번역만
    SandboxClass.READ_ONLY: ["--readonly"],
    SandboxClass.WORKSPACE_WRITE: ["--write-cwd"],
    SandboxClass.UNRESTRICTED: ["--yolo"],   # 여기서 반드시 warning 로그
}
def translate(sb: SandboxClass) -> list[str]:
    if sb not in _TO_MYBACKEND:        # enum 성장 시 조용한 관대함 대신 시끄러운 실패
        raise KeyError(sb)
    return _TO_MYBACKEND[sb]

직접 구현 체크리스트:

  • 샌드박스 어휘는 상향 의존 0인 최상위 모듈에 둔다(엔진·어댑터 양쪽이 순환 없이 import).
  • 결정 함수는 하나, 역할→클래스 매핑 dict는 한 곳.
  • 각 백엔드 번역은 평탄 dict 룩업, 미등록 시 KeyError로 즉시 실패(기본 관대 금지).
  • UNRESTRICTED 실현 시 모든 어댑터가 warning 발화.
  • 자식 spawn: 부모 런타임 마커 strip + _DEPTH 카운터로 재귀 천장(기본 5) 강제.
  • 외부 플러그인: 단일 invoke_plugin 초크포인트. 신뢰검사 차단 시 invoked 미발화, 매 호출 digest 재계산, argv 비밀값 [redacted], stdout/stderr는 sha256만 원장에.

요약 & 셀프체크

3줄 요약:

  1. 권한 등급은 엔진이 역할을 보고 단 한 곳(derive_sandbox_class)에서 중립 단어 3개 중 하나로 결정한다.
  2. 각 백엔드 어댑터는 그 등급을 자기 CLI 깃발로 옮겨 적기만 하고, 미등록 등급은 조용히 봐주는 대신 시끄럽게 실패한다.
  3. 외부 플러그인은 단일 정문 invoke_plugin에서 신뢰·확인·해시·감사를 강제로 거치며, 차단되면 “실행됨” 기록이 남지 않는다.

스스로 답해보기:

  • 인터뷰 역할 세션이 실수로 호스트 파일을 지우려 하면 어느 단계에서, 어떤 등급 때문에 막히나?
  • 어댑터 변환표에 새 등급이 빠져 있을 때 “조용히 관대해지는” 대신 “시끄럽게 실패”하게 만든 이유는?
  • 플러그인이 신뢰 검사에서 차단됐는데 감사 로그에 plugin.invoked가 보인다면, 무엇이 잘못된 것인가?

근거 파일

  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/sandbox.py
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/orchestrator/policy.py
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/claude_permissions.py
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/codex_permissions.py
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/copilot_permissions.py
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/codex/cli_policy.py
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/runtime/child_env.py
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/plugin/firewall.py
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/plugin/lockfile.py

연결

OB_개요 · _분석축_루브릭