DeepSeek Harness 07: 한 줄 설정으로 능력 전체 교체
dsh가 fs를 직접 import하지 않는 이유와 Shell 실행을 원격 샌드박스로 전환하는 '능력 Seam' 설계를 설명한다.
중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.
먼저 가정용 비유 하나로 시작하자
집의 전등 스위치는 전기가 어디서 오는지 신경 쓰지 않는다. 시내 전원이든, 태양광이든, 발전기든 상관없다. 스위치는 단 한 가지 일만 정의한다. "켜짐/꺼짐". 뒤에 연결된 것이 이 인터페이스를 준수하기만 하면, 스위치를 누르면 불이 켜진다.
이것이 Seam(틈새)의 본질이다. 하나의 인터페이스 경계로, "이 능력을 쓴다"와 "이 능력을 어떻게 구현하는가"를 분리한다.
dsh에서는:
- 파일 시스템 조작 은 하나의 Seam( ctx.fs )
- Shell 실행 은 하나의 Seam( ctx.shell )
- LLM 호출 은 하나의 Seam( ctx.llm )
- 프로세스 샌드박스 는 하나의 Seam( ctx.sandbox )
기본 로컬 파일 시스템을 E2B 샌드박스 파일 시스템으로 바꿔도, 도구 코드는 한 줄도 고치지 않는다. 이것이 Capability Seam이 해결하려는 문제다.
Seam의 세 가지 역할
모든 Seam은 세 종류의 참여자로 구성된다:
┌────────────────────────────────────────────────────────┐ │ Consumer: tool-fs │ │ import ctx.fs → calls ctx.fs. readFile (path) │ │ ctx.fs. writeFile (path, content) │ └─────────────────────┬──────────────────────────────────┘ │ 의존 서비스 "ctx.fs" ┌─────────────────────▼──────────────────────────────────┐ │ Service Definition: dsh-fs │ │ interface FileSystem { readFile, writeFile, ... } │ └─────────────────────┬──────────────────────────────────┘ │ 구현 ┌───────────────┴───────────────┐ ▼ ▼ fs-local fs-e2b (로컬 파일 시스템) (E2B 샌드박스)
세 역할의 분담:
역할 책임 예시 Service Definition 인터페이스 + 서비스 이름 정의 dsh-fs : ctx.fs 의 메서드 시그니처 정의 Service Provider 인터페이스 구현, ctx 에 등록 fs-local 、 fs-e2b 、 fs-sandbox Service Consumer 인터페이스 사용, 구현은 신경 쓰지 않음 tool-fs : 파일 읽기/쓰기 도구 플러그인
Consumer는 서비스 이름(예: ctx.fs )만 알 뿐, 뒤에 어떤 Provider가 있는지는 모른다. Provider 전환은 Bundle의 설정에만 영향을 미치고, Consumer는 전혀 느끼지 못한다.
핵심 Seam 일람
dsh에는 다음과 같은 핵심 Seam이 내장되어 있다:
Seam 서비스 이름 기본 구현 대체 가능 파일 시스템 ctx.fs fs-local (본机) fs-e2b (E2B 샌드박스)、 fs-sandbox (제한된 본机) Shell 실행 ctx.shell bash-local bash-sandbox (제한 실행)、원격 Shell 프로세스 샌드박스 ctx.sandbox sandbox-local (bwrap/Seatbelt/ACL) 컨테이너、microVM LLM 어댑터 ctx.llm llm-deepseek llm-pi-ai 등 서드파티 인증 자격 증명 ctx.credentials credentials-local 원격 vault 사용자 설정 ctx.settings settings-file 원격 설정 서비스 영속 저장소 ctx.sessionPersistence session-persistence-jsonl SQLite、원격
중점: ctx.fs(파일 시스템 Seam)
왜 그냥 import fs from 'node:fs' 하지 않는가?
Node.js 내장 fs 모듈을 직접 import하면 세 가지 구체적인 문제가 생긴다:
- 테스트할 때 : "파일 읽기" 로직을 테스트하려면 디스크에 실제로 파일을 만들어야 한다. 아니면 mock을 쓰는데, Node.js 내장 모듈을 mock하려면 추가 도구(jest.mock 등)가 필요해서 번거롭다.
- 샌드박스에서 실행할 때 : E2B는 자체 파일 시스템 인터페이스를 가지며, 로컬 Node.js fs 가 아니다. 도구 코드가 node:fs 를 직접 import했다면, E2B 안에서는 전혀 돌아가지 않는다.
- 감사 : 로그 기록, 권한 검사 같은 작업을 위해 모든 파일 읽기/쓰기 조작을 통일적으로 가로챌 수 없다. 호출 지점이 각 도구마다 흩어져 있기 때문이다.
ctx.fs Seam을 쓰면 이 세 가지 문제가 모두 사라진다:
- 테스트 시 인메모리 파일 시스템 Provider 주입
- E2B 안에는 fs-e2b Provider 주입
- 감사 시 Provider 계층에서 모든 호출을 통일적으로 기록
Consumer 코드 예시(개념 예시)
// 도구 플러그인: 파일 읽기——ctx.fs에만 의존하고 구현은 신경 쓰지 않음 // 'fs' 서비스 의존 선언 export const inject = [ 'fs' ] export function apply ( ctx: Context ): void { ctx. tools . register ( defineTool ({ name : 'read_file' , description : '로컬 파일 내용 읽기' , execute : async (args, exec) => { // ctx.fs는 파일 시스템 Seam——로컬, 샌드박스, E2B는 어떤 Provider가 로드됐는지에 달려 있음 // 여기서는 뒤에 어떤 파일 시스템이 쓰이는지 전혀 신경 쓰지 않음 const content = await ctx. fs . readFile (args. path ) return content }, })) }
// 샌드박스 파일 시스템으로 전환하고 싶다면? // Bundle에서 Provider 플러그인만 바꾸면 됨: // bundle.ts(개념 예시) export default [ // 이 줄만 바꾸면 충분하고, 도구 코드는 고칠 필요 없음: // '@deepseek-ai/dsh-fs-local' → 로컬 파일 시스템(기본) // '@deepseek-ai/dsh-fs-sandbox' → 제한된 로컬 파일 시스템 // '@deepseek-ai/dsh-fs-e2b' → E2B 샌드박스 파일 시스템 '@deepseek-ai/dsh-fs-sandbox' , // ← 이 줄만 변경 // Consumer 코드는 한 줄도 그대로 '@deepseek-ai/dsh-tool-fs' , // ... 기타 플러그인 ]
중점: ctx.sandbox(프로세스 샌드박스 Seam)
Shell 실행 뒤에는 프로세스 샌드박스가 있다. dsh는 세 가지 샌드박스 모드를 정의한다:
// 프로세스 샌드박스의 세 가지 모드(개념 예시) type SandboxMode = | 'read-only' // 읽기 전용 모드: 자식 프로세스는 어떤 파일에도 쓸 수 없음 | 'workspace-write' // 작업 공간 쓰기 모드: 작업 디렉터리 아래에만 파일을 쓸 수 있음 | 'danger-full-access' // 무제한 모드: 샌드박스를 거치지 않고 자식 프로세스를 직접 spawn
플랫폼 구현은 자동으로 적응한다:
플랫폼 저수준 메커니즘 Linux bwrap + Landlock macOS Seatbelt Windows ACL 제한 토큰
한 줄 설정으로 백엔드 교체(개념 예시):
로컬 개발: sandbox-local(플랫폼 네이티브 샌드박스) # CI/CD: sandbox-e2b(E2B 컨테이너, 완전 격리, 샌드박스가 무너져도 호스트에 영향 없음)
동일한 tool-shell 코드를 로컬에서 돌릴 때는 sandbox-local , CI에서 돌릴 때는 sandbox-e2b 를 쓰고, 전환은 Bundle 설정 계층에서만 일어난다.
중점: ctx.llm(LLM 어댑터 Seam)
LLM도 Seam이다. 이 설계는 언뜻 다소 의외로 보이지만, 곰곰이 생각하면 매우 합리적이다.
왜 LLM 호출도 추상화해야 하는가?
- 공급자 전환 : 동일한 Agent를 로컬 테스트에는 경량 모델, 프로덕션에는 DeepSeek으로 쓸 때 Provider 플러그인만 교체.
- 재생 테스트 : llm-replay Provider——실제 API 요청을 보내지 않고, 과거 session의 assistant 응답을 순서대로 재생한다. Agent 동작을 테스트하는 데 API key가 필요 없고, 무작위성도 없다.
- 다중 모델 라우팅 : 작업 유형에 따라 다른 모델로 라우팅하는 Provider를 구현할 수 있다.
// llm-replay의 용도(개념 예시) // 테스트에서: // 1. 먼저 실제 대화를 한 번 돌려 session 로그를 저장 // 2. 이후 테스트를 돌릴 때 Provider를 llm-replay로 교체, // replay adapter가 과거 assistant/message 이벤트를 순서대로 재생 // 3. 테스트는 완전히 결정적이고, API quota를 소비하지 않으며, 속도가 10x 빠름 // 전환 방식: Bundle에서 한 줄 // '@deepseek-ai/dsh-llm-deepseek' → 실제 API // '@deepseek-ai/dsh-llm-replay' → 재생 모드(테스트용)
실전: 직접 Provider 하나 구현하기
읽기 전용 파일 시스템 Provider를 구현한다——모든 쓰기 조작은 곧바로 오류를 내고, Agent에게 읽기 전용 접근 권한을 주기에 적합하다.
// 읽기 전용 파일 시스템 Provider(개념 예시) // 적용 시나리오: Agent가 코드베이스만 읽고 어떤 파일도 수정하지 못하게 하고 싶을 때 export const name = 'my-readonly-fs' export function apply ( ctx: Context ): void { // 프레임워크에 ctx.fs의 Provider로 등록 ctx. provide ( 'fs' , { // 읽기 조작: 정상 실행 async readFile ( path : string ): Promise < string > { return await localReadFile (path) }, // 쓰기 조작: 곧바로 가로채고, 명확한 오류를 던짐 async writeFile ( path : string , content : string ): Promise < void > { throw new Error ( `Read-only filesystem: cannot write to ${path} ` ) }, // 디렉터리 목록: 정상 실행 async listFiles ( dir : string ): Promise < string []> { return await localListFiles (dir) }, // ... 기타 메서드(mkdir、rm 등은 모두 읽기 전용 오류를 던짐) }) }
// Bundle에서 사용자 정의 Provider 사용(개념 예시) export default [ // 기본 fs-local을 자신의 읽기 전용 Provider로 교체 './my-readonly-fs' , // tool-fs는 평소처럼 사용하고, 뒤가 읽기 전용인지 모름 '@deepseek-ai/dsh-tool-fs' , ]
이 패턴의 강력한 점: tool-fs의 코드를 전혀 고치지 않고도 Agent의 파일 시스템 권한을 제한할 수 있다.
Seam 설계의 핵심 가치
표 하나로 비교해 보자:
| 시나리오 | Seam 미사용 | Seam 사용 | | --- | --- | --- | | LLM 공급자 전환 | 모든 API 호출 코드 수정 | Provider 플러그인 하나 교체 | | 단위 테스트 파일 작업 | node:fs를 mock 하거나 디스크에 실제 파일을 생성해야 함 | 메모리 파일 시스템 Provider를 직접 주입 | | 샌드박스 환경에 배포 | 샌드박스 API에 맞춰 대량의 도구 코드를 수정해야 함 | fs-sandbox 또는 fs-e2b로 교체 | | 플랫폼/백엔드 지원 추가 | 핵심 코드를 수정하며 버그가 발생할 수 있음 | 새 Provider를 구현하며 Consumer는 영향을 받지 않음 | | 권한 제어 | 각 도구에 if 판단을 추가 | Provider 계층에서 일괄 차단 |
Seam은 본질적으로 의존성 주입(DI)의 특화된 형태이며, 다만 플러그인 시스템에서는 주입의 단위가 생성자 매개변수가 아니라 "서비스 이름"이다.
설계 요약
dsh Capability Seam의 핵심 사고방식:
Consumer는 인터페이스 이름만 알고, Provider는 인터페이스만 알며, 전환은 구성 계층에서 일어난다.
| 설계 결정 | 이유 | | --- | --- | | 모든 외부 능력이 Seam을 통과 | 테스트, 배포, 다중 백엔드 지원이 모두 이점을 얻음 | | LLM도 Seam | replay 테스트, 공급자 전환, 다중 모델 라우팅 지원 | | 샌드박스도 Seam | 로컬/CI/원격 환경에서 서로 다른 샌드박스 백엔드를 사용하며 Agent 코드는 변경되지 않음 | | Consumer는 서비스 이름만 선언 | 결합이 철저히 해소되어 어떠한 구체 구현에도 의존하지 않음 | | Bundle은 구성 계층 | 서로 다른 Provider를 조합할 때 한 곳만 수정하면 되고 비즈니스 코드는 수정하지 않음 |
시리즈 다음 편
다음 편에서는 다중 Agent 협업을 다룬다: 하나의 Agent가 복잡한 작업을 해결하지 못할 때, dsh가 여러 Agent의 분업과 협력을 어떻게 지원하는지——위임(delegation), 하위 Agent, 병렬 실행, 그리고 이러한 메커니즘이 Session 계층에서 어떻게 표현되는지를 설명한다.
PrimeSkills에서는 실제 기업 시나리오에서 검증된 AI Agent 기술과 워크플로를 찾을 수 있다. 데모 수준이 아니라 실제 프로젝트에서 사용되는 것이다.
더 많은 내용은 내 개인 홈페이지에서 볼 수 있다.
冬奇Lab
소프트웨어 아키텍처
508
글
319k
읽음
739
팔로워