dsh) 딥다이브DeepSeek Harness(명령어 이름 dsh)는 "LLM + 도구 + 루프"를 하나의 제품으로 묶는 에이전트 하네스다. Claude Code나 OpenAI Codex처럼 "터미널에서 도는 코딩 에이전트"를 떠올리면 된다. 하지만 그런 도구들이 대개 하나의 통짜 프로그램인 반면, dsh는 정반대 설계를 택했다. 에이전트를 이루는 조각 하나하나 — 어떤 모델을 쓸지, 어떤 도구를 쥘지, 파일을 어떻게 읽을지, 명령을 어디서 실행할지, 대화를 어떻게 저장할지 — 를 전부 독립된 플러그인으로 만들고, 부팅할 때 설정 파일 하나로 조립한다.
그래서 dsh의 공식 슬로건이 "Everything is a Plugin"(모든 것은 플러그인이다)이다. 모델 어댑터도 플러그인, 도구 레지스트리도 플러그인, 세션 로그도 플러그인, 에이전트의 핵심 루프(agent-loop)마저 플러그인이다. "여기가 심장부니까 함부로 못 고친다"는 특권적 코어가 아예 없고, 모든 부분이 설정으로 교체·제거·추가된다.
보통의 에이전트 도구는 아이폰에 가깝다 — 잘 만들어졌지만 기본 앱을 마음대로 바꾸긴 어렵다. 모델을 바꾸거나 도구를 끼우려면 그 프로그램의 코드를 직접 뜯어야 한다.
dsh는 안드로이드에 가깝다. "모델을 DeepSeek에서 다른 것으로", "파일 읽기를 로컬 대신 원격 샌드박스로", "기억을 SQLite 대신 JSONL로" 같은 교체가 코드 수정이 아니라 설정 한 줄로 끝난다. 부품이 다 규격화된 소켓(서비스)에 꽂히기 때문이다.
가장 빠른 체험은 설치 없이 한 줄이면 된다. 아래 명령은 로컬에 웹 UI를 띄운다.
# npm으로 바로 실행 (Node.js만 있으면 됨)
npx @deepseek-ai/dsh web
# → http://127.0.0.1:3080 에 웹 UI가 뜨고 브라우저가 열린다
단, 저장소는 스스로 개발자 프리뷰(v0.1.1-rc.2)임을 명시한다. README에 "호환성을 깨는 변경이 있을 것"이라 못 박아 두었으니, 지금은 "완성품"이라기보다 설계를 공부하고 플러그인을 실험하는 대상으로 보는 게 맞다. 그럼에도 규모는 이미 거대하다 — 227개 npm 패키지가 47개 그룹으로 나뉜 대형 TypeScript 모노레포다.
비슷한 에이전트 하네스(Claude Code, Codex, LangGraph, OpenHands 등)는 이미 많다. 그런데 dsh가 공개되자마자 GitHub 별 16만 개를 넘기고 TrendShift 상위에 오른 건 네 가지가 겹쳤기 때문이다.
DeepSeek는 저비용 고성능 모델로 이름을 알린 곳이다. 그런 곳이 "우리 모델을 어떤 런타임 위에서 돌리길 기대하는가"를 코드로 공개했다는 점 자체가 신호다. 라이선스는 MIT로 완전 개방이고, 모델 어댑터(dsh-llm-deepseek)는 deepseek-v4-pro·deepseek-v4-flash를 겨냥하지만 구조상 어떤 모델도 붙일 수 있다.
대부분의 에이전트 프레임워크는 "고정된 엔진 + 확장 포인트" 구조다. 엔진은 못 건드리고, 정해진 훅에만 코드를 꽂는다. dsh는 그 발상을 뒤집어, 엔진조차 플러그인으로 만들었다. 에이전트 루프가 마음에 안 들면 루프 플러그인을 통째로 갈아끼우면 된다. 이 "끝까지 밀어붙인 조립식(composability)" 철학이 개발자들의 흥미를 끌었다.
dsh에는 에이전트가 "지금 돌고 있는 자기 런타임"을 들여다보고, 새 플러그인을 즉석에서 짜서 장착·해제하는 도구(cordis_* 툴셋)가 들어 있다. 모델이 cordis_inspect로 현재 서비스·도구·이벤트를 살피고, cordis_define·cordis_run으로 자기가 쓸 새 도구를 만들어 mount하는 식이다(데모: pnpm run demo:cordis). "에이전트가 자기 능력을 스스로 확장한다"는 아이디어를 실물 코드로 보여 준 게 화제가 됐다.
dsh는 하위 작업을 떼어 서브에이전트에게 시킬 때, 그 서브에이전트의 정체를 고를 수 있다. 같은 dsh를 fork하는 건 물론, Claude Code(공식 Agent SDK 경유), Codex, ACP(Agent Client Protocol) 호환 에이전트까지 하나의 하네스 아래에 묶는다. "여러 에이전트를 조율하는 상위 에이전트"를 표준 방식으로 만들 수 있다는 뜻이다.
| 도구 | 강점 | dsh와의 차이 |
|---|---|---|
| Claude Code | 완성도·안정성 높은 코딩 에이전트 | 통짜 제품. 내부 루프·도구 교체는 제한적(dsh는 서브에이전트로 품음) |
| OpenAI Codex | 터미널 코딩 에이전트, 넓은 사용자층 | 모델·루프 교체보다 "바로 쓰기" 중심. dsh는 백엔드로 편입 가능 |
| LangGraph | 그래프로 에이전트 흐름 설계 | 라이브러리(직접 조립). dsh는 부팅·세션·샌드박스까지 갖춘 런타임 |
| deepseek-harness | 모든 부품이 플러그인 | 특권적 코어 없음 + 설정으로 전면 교체 + 자기수정 + 멀티 백엔드 서브에이전트 |
dsh는 TypeScript 일색의 모노레포다(소스 .ts 2,472개 + .tsx 262개). 전 구간이 ESM(ECMAScript 모듈)이고, 패키지 관리는 pnpm@11.7 워크스페이스, 실행 환경은 Node 22/24다. 모든 npm 패키지가 @deepseek-ai/dsh-* 이름을 달고, 하나같이 Cordis를 peerDependency로 요구한다 — 즉 "패키지 = 플러그인"이 규칙으로 강제된다. 스택은 "프레임워크 층", "빌드/품질 층", "런타임 표면(웹/네이티브/파이썬) 층"으로 나눠 보면 깔끔하다.
| 요소 / 버전 | 역할 |
|---|---|
| Cordis 4.0.1 | 이 프로젝트의 뼈대. 플러그인이 서비스·타입 이벤트·되돌릴 수 있는 효과를 공유 컨텍스트에 기여하는 프레임워크. vendor/cordis에 고정 동봉. |
| cosmokit 1.8.2 · schemastery 3.18.1 | Cordis 생태계 유틸. schemastery는 플러그인 Config를 타입 기반 스키마로 검증(잘못된 설정은 로드 시 즉시 실패). |
| core/* 패키지 | 제품 API 척추: ctx.agents(agent) · ctx.agentLoop(agent-loop) · ctx.tools(tools) · ctx.sessions(session) · ctx.systemPrompt. |
| llm/* 패키지 | 모델 어댑터. dsh-llm(중립 인터페이스) + dsh-llm-deepseek + dsh-llm-pi-ai(대체 백엔드) + dsh-llm-retry + dsh-token-meter. |
| 요소 / 버전 | 역할 |
|---|---|
| TypeScript ^6.0.3 | host/client 두 프로그램으로 분리 빌드(tsconfig.host.json · tsconfig.client.json). 뒤 4번에서 이유 설명. |
| tsdown ^0.22 · tsx ^4.22 | 번들러 + TS 즉시 실행기. dsh CLI는 tsx/esm 훅으로 소스를 바로 돈다. |
| vitest ^4.1 (+coverage-v8) | 테스트 러너. CI 게이트가 파일별 커버리지 100%를 요구한다. fast-check로 속성 기반 테스트도. |
| oxlint 1.76 (+tsgolint) | eslint가 아니라 Rust로 작성된 초고속 린터. knip(미사용 코드)·jscpd(복붙 탐지)·publint·lefthook(git 훅) 동반. |
| typert (typert/*) | 타입 그래프 기반 RPC 생성기. host↔client·프로세스 경계의 통신 타입을 자동 생성. |
| 요소 | 역할 |
|---|---|
React + Vite (apps/web) | 웹 UI. 흥미롭게도 UI 자체가 ~50개 Cordis 플러그인(client/ui-*)으로 쪼개져, 슬롯(SlotMap) 레지스트리로 조립된다. |
C11 Landlock 런처 (native/) | Rust가 아닌 C11로 쓴 ~300줄 샌드박스 런처(musl 정적 링크). 리눅스 커널의 Landlock으로 프로세스를 스스로 가둔 뒤 명령을 exec. |
Python SDK (python/sdk) | pip install deepseek-harness-sdk. pydantic 2.12 기반. dsh를 서브프로세스로 띄워 JSON-RPC(stdio)로 조종한다. |
| 샌드박스 백엔드 | 리눅스 bwrap/Landlock · macOS Seatbelt · Windows ACL — 실패 시 닫힘(fail-closed)으로 안전하게 거부. |
| node-pty · @vscode/ripgrep | 영속 터미널(PTY) 세션 + 초고속 파일 검색(glob/grep). 셸·터미널·검색 도구의 뒷단. |
스택 곳곳에서 두 가지 취향이 읽힌다. 첫째, "경계에서만 검증하고, 안에서는 타입을 믿는다" — schemastery로 설정을, typert로 프로세스 경계를 지키되, 같은 프로세스 안 타입 경계는 TypeScript를 신뢰한다. 둘째, "느린 도구를 빠른 도구로 교체" — eslint 대신 oxlint(Rust), 검색은 ripgrep, 샌드박스는 커널 기능(Landlock)을 직접 쓴다. 프리뷰 단계인데도 테스트 커버리지 100%를 CI로 강제할 만큼 엔지니어링 기준이 높다.
dsh를 이해하는 열쇠는 밑에 깔린 Cordis다. Cordis의 핵심 발상은 다섯 문장으로 요약된다. ① 플러그인은 apply(ctx) 함수(또는 Service 클래스)다. ② 컨텍스트(ctx)는 서비스들의 저장소이고, 서비스는 ctx.tools처럼 안정적인 이름(키)을 차지한다. ③ 플러그인은 필요한 서비스를 inject로 선언하면, 그게 준비될 때까지 자동으로 기다린다(부팅 순서를 손으로 짤 필요 없음). ④ 통신은 타입이 붙은 이벤트로 한다. ⑤ 모든 등록은 ctx.effect()로 이뤄지는 되돌릴 수 있는 효과라, 리로드·해제가 깔끔하게 풀린다.
ctx.llm, ctx.fs …)에 꽂힌 부품", 효과는 "꽂았다 뺐다 할 수 있는 연결". 다른 플러그인은 구체적 구현을 import하지 않고 슬롯 이름으로 부품을 찾는다. 그래서 부품을 바꿔 꽂아도 나머지가 그대로 동작한다.실행 중인 dsh는 부팅 시 프로파일(profile)이 번들(bundle)들을 순서대로 쌓아 만든 하나의 플러그인 트리다. dsh-base가 항상 첫 층(모델·도구·저장·샌드박스·정책·설정·자격증명·텔레메트리)이고, dsh-web-app이 브라우저 앱을, dsh-headless가 일회성 러너를 얹는다. 무엇이 조립됐는지는 dsh --profile web --dump-config로 들여다볼 수 있다.
dsh 설계의 중심 개념이 seam(이음새)이다. 하나의 능력은 항상 세 역할로 이뤄진다 — 서비스 정의(인터페이스), 서비스 제공자(구현), 소비자(보통 모델이 쥐는 도구). 예컨대 파일시스템은 ctx.fs(정의) + dsh-fs-local(제공자) + dsh-tool-fs(소비자)로 구성된다. 파일·셸·샌드박스 제공자가 하나의 실행 세계를 공유하도록 설계돼 있어, 이 제공자들을 원격 샌드박스로 가리키기만 하면 Bash·터미널·LSP가 통째로 그 원격으로 옮겨간다. "한 부품 교체로 제품 전체 동작이 바뀐다"는 게 이 구조의 힘이다.
에이전트가 한 번 "생각→도구 사용→답변"을 도는 과정은 전부 타입 이벤트의 흐름이다. 스텝(step)은 모델 요청 1회 + 그에 딸린 도구 호출들이고, 턴(turn)은 0개 이상의 스텝이다. 아래가 실제 이벤트 순서다.
agent/pre-step·agent/request·llm/stream·tools/*는 워터폴(waterfall) 이벤트라, 리스너가 다음으로 넘기려면 반드시 next()를 불러야 한다(부르지 않으면 그 자리에서 결정을 가로챈다). 미들웨어처럼 "감싸기"가 되는 구조다. 새 정책(요청 가로채기, 도구 검열, 재시도)은 루프 코드를 고치는 게 아니라 이 이벤트에 리스너를 플러그인으로 붙여 넣는다.
deriveMessages()가 로그→히스토리로 투영). 그래서 모델에게 새 입력을 주려면 반드시 새 세션 이벤트를 남겨야 하고, 런타임이 이 규칙을 실제로 검사한다. 재현·감사·리플레이가 공짜로 따라오는 설계다.dsh-tool-cordis는 모델에게 지금 돌고 있는 Cordis 런타임을 다루는 5개 도구를 준다. cordis_inspect(서비스·플러그인·도구·이벤트 관찰), cordis_define(모델이 짠 플러그인 등록 — host 코드와 브라우저 client 코드 양쪽), cordis_run(호스트 절반을 vm 샌드박스에서 평가), cordis_stop, cordis_undefine. 이렇게 만든 동적 플러그인은 프로세스 메모리에만 산다. 앞서 본 "되돌릴 수 있는 효과"가 있기에, 모델이 즉석에서 장착한 기능도 안전하게 해제된다.
README와 AGENTS.md 모두 프리뷰임을 강조한다. SESSION_FORMAT_VERSION은 아직 0이고 "호환성 셔임보다 올바른 토대를 우선한다"는 방침이라, 세션 포맷·API가 예고 없이 바뀔 수 있다. 외부 PR도 현재는 닫혀 있다(플러그인 생태계는 dsh-plugin 토픽으로 개방). 지금 시점의 dsh는 "프로덕션 투입"이 아니라 설계 학습과 플러그인 실험에 맞다.
모노레포의 최상위는 역할별로 명확히 갈린다. 압도적 대부분의 코드가 packages/<그룹>/<패키지>/ 아래 잘게 흩어져 있는 게 특징이다.
구조가 곧 철학이다. 이 저장소에서 "기능을 추가한다"는 건 대부분 코드 흐름을 고치는 게 아니라 packages/ 아래에 새 패키지(=플러그인)를 하나 더 놓는 것이다 — 그래서 이 레포는 하나의 앱이 아니라 "플러그인이 자라는 플랫폼"에 가깝다. 새 모델은 ctx.llm에 어댑터를 등록하는 패키지로, 새 도구는 ctx.tools에 등록하는 패키지로 들어온다.
.agents/notes/에는 implemented·proposed·rejected·archived로 나뉜 날짜별 결정 기록이 쌓이고, AGENTS.md는 "사소하지 않은 변경은 반드시 Agent Note를 함께 남겨라"고 규정한다. CLAUDE.md가 AGENTS.md로 향하는 심볼릭 링크인 것도 같은 맥락 — 모든 에이전트에게 같은 규칙을 준다.dsh는 "잘 만든 소프트웨어 아키텍처"의 교보재다. 에이전트에 관심이 없어도, 아래 주제를 실전 코드로 배울 수 있다.
"고정 코어 + 확장 포인트"가 아니라 "전부 플러그인"이라는 극단을 실제로 어떻게 구현하는지 볼 수 있다. 서비스를 이름으로 찾고(ctx.tools), 필요한 것을 inject로 선언하면 프레임워크가 순서를 풀어 준다. 스프링(Java)이나 NestJS의 DI를 "왜, 어떻게"의 관점에서 다시 이해하게 된다.
모든 등록을 ctx.effect()로 하면 "설치의 역순으로 자동 해제"가 공짜로 생긴다 — 플러그인을 껐다 켜도, 리로드해도, 자원이 새지 않는다. 이것이 Cordis가 말하는 "시공간적 조립성(spatiotemporal composability)"의 실체다. 리액트의 useEffect 클린업이 왜 중요한지 아는 사람이라면 이 개념이 프레임워크 전체로 확장된 모습을 보게 된다.
에이전트 루프를 "고칠 수 없는 엔진"이 아니라 "next()로 넘기는 미들웨어 체인"으로 만든 설계는 Express·Koa의 미들웨어, 혹은 Redux 미들웨어와 정확히 같은 뼈대다. "가로채고, 감싸고, 넘긴다"는 패턴을 에이전트 문맥에서 다시 익힌다.
파일·셸·샌드박스를 인터페이스(정의)와 구현(제공자)으로 갈라 두면, 테스트에선 mock 제공자를, 운영에선 원격 샌드박스 제공자를 꽂을 수 있다. "구현이 아니라 인터페이스에 의존하라"는 원칙(의존성 역전)이 제품 규모에서 어떻게 이득을 내는지 체감할 수 있다.
227개 패키지를 pnpm 워크스페이스로 묶고, host/client 두 tsconfig로 분리 빌드하는 이유(양쪽이 같은 Context 키를 서로 다른 서비스로 선언 병합하므로, 한 프로그램이 둘 다 보면 충돌)를 배운다. 커버리지 100% 게이트, oxlint, knip, jscpd, lefthook까지 "대형 레포를 무너지지 않게 지키는 장치"의 실물 사례다.
에이전트가 함부로 파일을 못 건드리게 막는 native/landlock-run은 리눅스 Landlock LSM을 C로 직접 부른 ~300줄짜리 교과서다. "자신에게 규칙을 걸고 exec하면 그 제한이 자식 프로세스까지 상속된다"는 fail-closed 샌드박스 기법을 배운다.
dsh 본체는 Node로 돌기 때문에 무거운 GPU가 필요 없다(모델 추론은 API로 위임). 대신 버전 요건이 다소 최신이다.
| 항목 | 요구 / 참고 |
|---|---|
| Node.js | ^22.19.0 || >=24.0.0 — 비교적 최신 LTS. npx @deepseek-ai/dsh web이면 바로 실행. |
| 패키지 매니저 | 소스 빌드 시 pnpm@11.7.0(모노레포 워크스페이스). npm/yarn 아님. |
| 모델 자격증명 | DEEPSEEK_API_KEY(선택 DEEPSEEK_BASE_URL). 웹 UI 설정에서 입력하거나 루트 .env에 둔다. |
| 웹 UI | 기본 http://127.0.0.1:3080. --no-open으로 브라우저 자동 열기 생략. |
| 샌드박스(선택) | Landlock은 리눅스 커널 5.13+ 필요(ABI 레벨로 full/partial 판정). 없으면 bwrap/macOS Seatbelt/Windows ACL로 대체, 다 안 되면 닫힘. |
| OS | Windows/macOS/Linux 크로스 플랫폼. Windows 영속성엔 koffi·node-pty(ConPTY) 사용. |
| Python SDK(선택) | python >=3.10 + pydantic 2.12. pip install deepseek-harness-sdk로 dsh를 서브프로세스로 조종. |
npx @deepseek-ai/dsh web으로 로컬 웹 UI를 띄운다. Settings → Models에서 DeepSeek API 키(platform.deepseek.com 발급)를 넣고, 작업 폴더를 고른 뒤 "이 폴더 요약해 줘" 같은 간단한 작업을 시켜 본다. 목표: 하네스가 모델·도구·작업 폴더를 어떻게 엮는지 전체 흐름을 몸에 익히기.
소스를 클론해 pnpm install && pnpm run build 후, pnpm dsh --profile headless "summarize this workspace"로 일회성 세션을 돌린다. 그다음 examples/headless-agent/cordis.yml을 열어 어떤 플러그인이 {id, name, config} 행으로 조립됐는지 읽는다. 배우는 것: 프로파일·번들·cordis.yml이 실제로 무엇을 켜고 끄는가.
examples/mcp-memory/를 참고해 dsh web --patch "$PWD/examples/mcp-memory/memorix.cordis.yml"로 외부 MCP 메모리 서버를 붙인다. 도구가 mcp__<서버>__<도구> 형태로 노출되는 걸 확인한다. 배우는 것: 코드 수정 없이 설정 오버레이(--patch)만으로 능력을 얹는 방법, MCP 연결 구조.
dsh --profile web --dump-config로 최종 조립된 플러그인 트리(어떤 서비스가 어떤 순서로 켜졌는지)를 관찰한다. 이어 pnpm run demo:cordis를 돌려, 에이전트가 cordis_inspect로 자기 런타임을 보고 cordis_define/cordis_run으로 새 플러그인을 즉석에서 장착하는 과정을 지켜본다. 배우는 것: 부팅 조립의 실체, 자기 참조 도구의 동작.
seam 3역할을 따라 직접 만든다. (A) ctx.tools에 새 도구를 등록하는 플러그인 — 예: 현재 시간을 반환하는 get_time 도구를 apply(ctx)에서 ctx.effect()로 등록하고 cordis.yml에 한 줄 추가해 켠다. (B) 더 도전적으로는 ctx.llm에 다른 모델 제공자 어댑터를 붙여 DeepSeek 대신 그 모델로 루프를 돌려 본다. 배우는 것: "패키지=플러그인" 규칙과 되돌릴 수 있는 등록을 직접 손으로 익히기.
| 주차 | 주제 | 무엇을 · 왜 |
|---|---|---|
| 1주 | Cordis 기초 | Context·Service·inject·apply·effect. docs/cordis-primer.md부터. 프레임워크의 문법이자 세계관. |
| 2주 | dsh 부팅 구조 | profile·bundle·cordis.yml·--dump-config·--patch. "설정으로 조립"의 실제 메커니즘. |
| 3주 | 에이전트 루프·이벤트 | turn/step, 워터폴 vs serial, agent/*·tools/* 이벤트. 미들웨어로 루프를 확장하는 법. |
| 4주 | 능력 이음새(seam) | fs·shell·sandbox의 정의/제공자/소비자 3역할. 제공자 교체로 이식·테스트하기. |
| 5주 | 서브에이전트·도구 | claude-code/codex/dsh-sdk 백엔드, 도구 레지스트리 설계, MCP·LSP 연결. 제품 수준 능력 구성. |
| 6주 | 자기수정·샌드박스 | cordis_* 자기 참조 도구 + Landlock/bwrap 샌드박스. "안전하게 자라는 에이전트"의 마무리. |
dsh의 코드는 처음 보면 파일이 227개 패키지로 흩어져 압도적이다. 하지만 밑바닥 문법(Context에 서비스를 꽂고, inject로 기다리고, effect로 되돌린다)은 하나뿐이다. 1~2주에 이 문법과 부팅 조립만 확실히 잡으면, 나머지 패키지는 전부 "같은 문법으로 쓰인 변주"로 읽힌다. 반대로 이 토대를 건너뛰고 에이전트 기능부터 파면 코드가 미로처럼 느껴진다. 프레임워크를 먼저, 기능을 나중에가 이 레포에선 특히 중요하다.
ctx.tools, ctx.llm, ctx.on(...), ctx.effect(...)처럼 쓴다. "메인보드"에 해당.ctx.<키>라는 안정적 이름을 차지하는 능력. 다른 플러그인은 구현을 import하지 않고 이 키로 찾는다. 하나의 서비스 키를 여러 구현이 갈아끼울 수 있다(= seam).inject는 "이 서비스들이 준비되면 나를 켜라"는 의존성 선언(부팅 순서를 대신함). apply(ctx)는 플러그인이 실제로 하는 일 — 서비스 등록·이벤트 리스너 부착이 여기서 일어난다.ctx.effect()/ctx.on()으로 한 모든 등록은 해제 함수(disposer)를 돌려준다. 플러그인을 끄면 설치의 역순으로 자동 정리돼 자원 누수가 없다. Cordis의 "시공간적 조립성"의 핵심.ctx.fs + dsh-fs-local + dsh-tool-fs. 제공자만 바꿔도 제품 동작이 통째로 바뀐다.(...인자, next)를 받아, next()를 부르면 다음 리스너로 넘기고(감싸기 가능), 안 부르면 그 자리에서 결정을 가로채는 이벤트. 미들웨어 체인과 같다. agent/request·tools/execute 등이 이 방식.web·headless). cordis.yml은 {id, name, config} 행으로 무엇을 mount할지 적은 중앙 설정 파일.landlock-run 런처로 도구 실행을 가둔다(커널 5.13+).