DeepSeek Harness · 승인과 샌드박스

승인과 권한: 노브 두 개, 드롭다운 하나

샌드박스 모드와 승인 정책은 독립 노브 둘이고, 프리셋은 자주 쓰는 조합일 뿐입니다. 핵심 소스:packages/interaction/user-approvalpackages/interaction/permission-presets

강의 목표이 레슨을 마치면 세 가지를 말할 수 있어요: plan mode·auto-accept·YOLO 같은 제품 개념이 DSH 바닥에서는 왜 sandbox/modeapproval/policy 두 직교 변수로만 쪼개지는지; 프리셋 드롭다운이 왜 노브 조합에 이름만 붙이는지, 전환 시 로그에 어떤 이벤트가 붙는지; 그리고 승인이 왜 매 단계에서 fail-closed인지(답을 못 받으면 거부로 친다).
인터랙티브 데모 · 이중 노브 콘솔

돌릴 수 있는 권한 콘솔입니다. 왼쪽 노브 둘이 각각 한 가지, 오른쪽 드롭다운이 프리셋. 조작을 고르고 「재생」을 눌러 도구 호출이 관문을 넘는 과정을 본 뒤, 노브를 돌리고 프리셋을 바꿔 다시 실행해 같은 조작의 운명이 어떻게 달라지는지 보세요.

노브 1 · 샌드박스 모드

sandbox/mode (파일 효과) read-only workspace-write danger

노브 2 · 승인 정책

approval/policy (사람에게 물을지) ask never

프리셋 드롭다운

permission preset (노브 바로가기)
세션 로그에 추가된 이벤트
모델이 도구 호출 시작write ./src/app.ts
샌드박스 층노브 1로 파일 효과 판정
승인 층막힌 뒤 승격 요청, 노브 2가 물을지 결정
결말실행 대기
승인 팝업: Agent가 이번 호출만 샌드박스를 danger-full-access로 잠시 넓히려고 합니다. 허용할까요? (이 조작만 인가)
「재생」으로 현재 조합을 실행하거나, 여기로 스크롤하면 한 번 자동 재생됩니다.
수업용 시뮬레이션입니다. 노브 값·프리셋 표·승격 승인 흐름은 docs/subsystems/permission-presets.zh.md, docs/subsystems/approval.zh.md, packages/sandbox/sandbox/src/escalation.ts에 대응합니다. 승인 팝업이 떠 있는 동안 진짜로 버튼을 눌러 사용자를 대신할 수 있어요.
먼저 노브 둘의 역할을 가르기

결론부터: DSH는 “권한”이라는 큰 말을 서로 안 참견하는 작은 질문 둘로 쪼갭니다. 노브 1 sandbox/mode는 이 명령의 파일 효과가 어디까지 허용되는지 — read-only·workspace-write·danger-full-access — 파일시스템만, 네트워크·프로세스 가시성은 어휘에 없습니다(docs/subsystems/sandbox.zh.md 앞부분 정의). 노브 2 approval/policy는 사람 결정이 필요할 때 물을지 — ask / never.

로그에서도 독립 이벤트 둘입니다: sandbox/modeapproval/policy. 실행·프롬프트·리플레이는 이 두 노브 이벤트의 접힌 결과만 읽습니다. 직교의 실제 의미: 하나를 돌려도 다른 쪽은 꿈쩍 안 합니다.

노브 1 · sandbox/mode
  • read-only: 백엔드는 쓰기를 거부해야 하고, 셸에 필요한 /dev/null 같은 수신기만 둡니다.
  • workspace-write: 워크스페이스 루트와 백엔드가 약속한 임시 구역만 쓰기 가능, 바깥은 전부 차단.
  • danger-full-access: 격리를 우회. 소비자가 원본 명령을 바로 spawn하고 ctx.sandbox는 호출하지 않습니다.
노브 2 · approval/policy
  • ask(기본): 조합된 응답자 체인에 넘깁니다. 응답자가 하나도 없으면 체인이 unavailable로 끝나고, 그래도 거부로 처리합니다.
  • never: 결정적으로 rejected를 반환하고 응답자를 분배하지 않습니다. CI·무인 운전의 표준 자세.
  • 유일한 통과 값은 allowed-once이며, 물은 그 조작만 인가합니다. rejected·cancelled·unavailable — 호출 측은 셋 모두 거부로 실행합니다.
프리셋: 칸에 이름만 붙이기

그다음이 드롭다운입니다. ctx.permissionPresets가 이름→노브 조합 표를 유지합니다. 기본 표는 두 줄: workspace-write → workspace-write + ask, danger-full-access → danger-full-access + never(permission-presets/src/index.ts 167–176행). 선택 능력이고 agent loop 본선에 없으며 강제 집행권도 없습니다.

프리셋 전환 시? 이벤트 셋, 순서 고정: 먼저 로그만 하는 permission/preset으로 의도를 남기고, 두 노브의 정규 setter로 sandbox/mode·approval/policy를 쓰되 실제 유효 값이 바뀐 쪽만. 현재 프리셋을 다시 고르면? 아무것도 안 붙입니다. 리플레이 때 실행 층은 노브 이벤트만 인정하고, preset 이벤트의 유일한 역할은 같은 조합을 공유하는 프리셋 둘 중 당신이 고른 이름을 기억하는 것입니다.

그리고 custom. 예약어라 표에 그 이름 항목이 있으면 안 되고(플러그인 로드 시 예외). 노브를 표에 없는 조합으로 돌릴 때만 current()가 custom을 파생해 클라이언트에 보여 줍니다. 나가기만: 현재 상태는 될 수 있어도 전환 대상은 영원히 아니고 이벤트 payload에도 안 나옵니다. 데모 드롭다운이 회색 비선택인 이유가 곧 소스 동작입니다.

fail-closed가 처음부터 끝까지

응답자 없음, 응답자 예외, 어휘 밖 값, 팝업 중 UI 닫기 — 전부 unavailable 또는 cancelled로 정규화되고 호출 측은 거부로 칩니다. 어디서든 명확한 allowed-once가 없으면 통과 없습니다.

승인은 모델 컨텍스트에 안 들어감

approval/askedapproval/decided가 세션 로그에 쌍으로 들어가 감사만 합니다. 모델이 보는 것은 도구 결과와 런타임 컨텍스트 스냅샷. ApprovalRequest는 의도적으로 도구 인자를 빼며 callId로 이미 스트리밍된 호출을 가리켜, 어긋날 수 있는 사본을 렌더하지 않습니다.

승격은 일회성

쓰기가 샌드박스에 막히면 모델은 sandbox_permissions로 승격 재시도를 요청할 수 있고, 승인 시 allowed-once가 나옵니다. 그 재시도는 명시적으로 더 넓은 모드로 정책을 한 번 해석하고 버립니다. 세션 노브 설정은 바뀌지 않습니다.

핵심 증거 · never는 폭포 전에 막아 둔다

경계 질문 하나: 승인 서비스가 마운트된 뒤 플러그인이 prepend로 응답자 체인 맨 앞에 전부 허용 응답자를 넣으면 never를 우회할 수 있을까요? 아니요 — never는 체인에서 실행되지 않으니까요. decide() 앞부분을 보세요:

packages/interaction/user-approval/src/index.ts304–312행 발췌
  private async decide(req: ApprovalRequest, session: Session): Promise<ApprovalOutcome> {
    const signal = req.signal
    if (signal?.aborted) return 'cancelled'
    // The 'never' policy is decided HERE, before any dispatch: a listener
    // registered with `prepend: true` after this service mounts would sit
    // ahead of any gate LISTENER, so a listener-shaped gate cannot keep the
    // documented promise that 'never' rejects deterministically regardless
    // of registration order — only the service's own request path can.
    if (this.effectivePolicy(session) === 'never') return 'rejected'
소스 스냅샷 안내:로컬 저장소 deepseek-harness-master 기준, 확인 파일 packages/interaction/user-approval/src/index.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.

주석이 설계 의도를 끝냅니다: never의 판정점은 서비스 자신의 요청 경로에 있고, 리스너 형태 게이트는 등록 순서와 무관하다는 약속을 지킬 수 없습니다. 317–329행의 ask 폴백도 빈틈없습니다: 체인 무응답 기본값은 unavailable, 예외 응답자는 unavailable로 접히고, 이상한 반환도 unavailable로 정규화됩니다.

프리셋 전환의 완전한 쓰기 경로

“프리셋은 바로가기일 뿐”의 구현 전부가 열네 줄 private apply()입니다. 일 셋, 순서 고정. 1) resolve(name) 표 조회 — 없으면 예외, 알려진 키도 나열. 2) current() 파생 결과와 비교: 고른 이름이 유효 프리셋과 다를 때만 permission/preset을 붙이고, 현재를 다시 고르면 이 단계는 아무것도 안 씁니다. 3) 노브 둘을 검사해 프리셋의 샌드박스 모드가 이벤트 접힌 유효 값(없으면 배포 기본값)과 다르면 setSandboxMode(), 승인 정책도 주입된 setApproval로. 유효 값이 안 바뀐 쪽은 건너뜁니다.

따라서 프리셋 전환에 세 번째 실행 경로나 숨은 상태는 없습니다: 의도 로그 하나와 많아 봐야 정규 setter를 통한 노브 쓰기 두 번 — 로그에 붙인 것이 곧 실행 층이 접는 것입니다. custom 파생은 같은 파일 derive(): 지난 프리셋이 현재 노브와 맞으면 유지, 아니면 선언 순 첫 매칭, 없으면 custom.

출처:packages/interaction/permission-presets/src/index.ts 379–392행(apply()), 309–321행(derive()), 확인일 2026-08-13。

가로 비교 · 단일 모드 축 vs 이중 노브
Claude Code: 모드 축 하나 + 규칙 표

Claude Code는 같은 영토를 단일 permission mode 축으로 만듭니다: default·plan·acceptEdits·bypassPermissions·dontAsk(복원 소스 restored-src/src/utils/permissions/PermissionMode.ts 44–91행, 원고 7장), 여기에 allow/deny/ask 규칙 표를 더하고 Bash(git commit:*) 같은 명령 접두까지 세밀해질 수 있습니다.

나란히 보면 접는 대가가 보입니다. dontAsk ≈ DSH never, bypassPermissions ≈ danger-full-access + never — 하지만 다섯 칸이 한 축이라 샌드박스 조임과 “물을지”가 묶여 팔립니다. DSH 이중 노브는 read-only + ask를 표현할 수 있어요: 샌드박스는 읽기 전용 바닥, 정말 쓰기가 필요할 때 한 번 팝업. CC 모드 축에는 직접 대응하는 칸이 없습니다. 반대로 CC는 이중 노브가 못 하는 것: 도구·명령 접두 세분 규칙 표. DSH 노브는 세션 전역이라 도구 단위 게이트는 hook 층이 필요합니다. 복잡도를 두는 곳이 다릅니다. Grok Build 인가 체인은 또 다른 길(도구 요청→제한 실행 층층이) — Grok 완전 인가 체인을 보세요.

수업 실습
01

까다로운 시나리오 둘 추론하기

시나리오 1: 승인 팝업이 뜬 뒤 사용자가 브라우저를 닫고 UI 응답자가 dispose로 제거됩니다. 이번 요청은 어떤 결과로 정산되고, 도구 호출은 통과일까요 거부일까요? 시나리오 2: 정책이 never인데 플러그인이 prepend로 항상 allowed-once를 반환하는 응답자를 등록합니다. request()부터 반환값까지 경로를 쓰고, 왜 그 응답자가 한 번도 호출되지 않는지 설명하세요. (힌트: 두 답 모두 이 페이지 소스 패널과 그 뒤 문단에 있습니다.)

Takeaway:DSH 바닥에서 권한은 직교 변수 둘뿐입니다: sandbox/mode는 파일 효과, approval/policy는 물을지, 프리셋은 조합 이름 표 키, custom은 나가기만. 승인 전 구간 fail-closed: 유일한 통과는 그 조작만의 allowed-once; never는 서비스 안, 폭포 분배 전에 막아 아무도 끼어들지 못합니다.