DeepSeek Harness · Code Mode

Code Mode: 코드 한 편이 여러 툴 라운드를 대체

모델이 작은 프로그램을 쓰고, run_code가 worker 샌드박스로 보내면, 툴 콜 다섯 번을 한 라운드로 끝냅니다. 이번 레슨은 그 설계 뒤의 아이디어 두 가지를 다룹니다.

강의 목표읽고 나면 두 가지를 말할 수 있어요. 모델에게 프로그램을 쓰게 하는 편이 툴 콜을 하나씩 보내는 것보다 왜 더 빠르고 싼지, 그리고 모델이 쓴 코드가 샌드박스에서 돌 때 호스트가 어떻게 스스로를 지키며 권한이 왜 한 치도 풀리지 않는지요.
먼저 해보기 · 코드 한 편 vs 다중 라운드 호출
같은 과제: 로그 파일 3개 읽고, 통계를 모아, 보고서 1부 쓰기
전통 도구 호출0 샘플링 라운드0k 토큰
모델 · 샘플링 라운드 1먼저 첫 파일 logs/a.log를 읽어요.
read_file({ path: "logs/a.log" })
412줄 전문이 그대로 돌아와 컨텍스트에 들어감
모델 · 샘플링 라운드 2이제 logs/b.log를 읽어요.
read_file({ path: "logs/b.log" })
398줄 전문이 그대로 돌아와 컨텍스트에 들어감
모델 · 샘플링 라운드 3아직 logs/c.log가 남았어요.
read_file({ path: "logs/c.log" })
441줄 전문이 그대로 돌아와 컨텍스트에 들어감
모델 · 샘플링 라운드 4컨텍스트에 있는 세 편의 전문으로 통계를 내고 보고서를 써요.
write_file({ path: "report.md", ... })
쓰기 성공
모델 · 샘플링 라운드 5집계가 끝났어요. 최종 답변을 내요.
과제 완료 · 샘플링 라운드 총 5회, 매 라운드마다 커지는 컨텍스트를 다시 보냄
Code Mode(PTC)0 샘플링 라운드0k 토큰
모델 · 샘플링 라운드 1프로그램 한 편을 써서, 이 다섯 단계를 샌드박스가 오케스트레이션하게 해요.
예시 프로그램(5줄, 수업용 예시 — 소스 인용 아님)
let total = 0, errors = 0 // 统计值,全程留在沙箱里for (const p of ['a.log', 'b.log', 'c.log']) { // 三个文件,循环里读 total += 统计(await tools.read_file({ path: p })) }await tools.write_file({ path: 'report.md', … }) // 写报告,照走审批return { total, errors } // 只有这一行回到模型
worker 샌드박스 · 격리 환경에서 실행, 중간 결과는 모델로 돌아가지 않음
await tools.read_file("logs/a.log")호스트가 전달하며, 그대로 승인 파이프라인을 탐
await tools.read_file("logs/b.log")결과는 샌드박스 변수에 남음
await tools.read_file("logs/c.log")결과는 샌드박스 변수에 남음
루프 안에서 통계 누적순수 계산, 왕복 제로
await tools.write_file("report.md")여전히 같은 권한 파이프라인
return { total, errors }이 값과 로그만 샌드박스를 떠남
한 번에 반환 { total: 1251, errors: 17 } · 여전히 샘플링 라운드 1
재생을 눌러, 같은 과제가 두 모드에서 어떻게 도는지 보세요.
샘플링 라운드: 5 vs 1왼쪽은 동작마다 샘플링 라운드 하나; 오른쪽은 한 라운드로 프로그램을 쓰고, 샌드박스가 나머지 동작을 대신 돌립니다.
처리량 스케치: 약 18.3k vs 약 2.6k왼쪽은 매 라운드 커지는 컨텍스트를 다시 보내고; 오른쪽 컨텍스트에는 프로그램과 일회성 반환값만 있어요.
중간 결과의 행방왼쪽은 파일 세 편의 전문이 전부 컨텍스트에 들어가고; 오른쪽은 전부 샌드박스 변수에 남아 print나 return한 부분만 돌아옵니다.
수업용 스케치: 라운드 수와 토큰 수는 두 모드의 구조 차이를 보여 주기 위한 수업용 추정치이며, 오른쪽 프로그램은 5줄 수업 예시로 소스 인용이 아닙니다.
아이디어 1 · 모델에게 프로그램을 쓰게 하고, 리모컨처럼 쓰지 마세요

먼저 이름을 분명히 할게요. 공식 발표는 PTC, 즉 프로그램형 도구 호출이라고 부르고, preset 메타데이터에도 name: PTC 模式라고 적혀 있어요(출처: apps/cli/config/agent-presets/code/preset.yml 1행). 소스에서 PTC를 찾아도 안 나와요. 내부 이름은 처음부터 끝까지 code mode예요: 설정 mode: code, 도구 이름 run_code. 이름 두 개, 같은 메커니즘.

어떤 문제를 푸는가

네이티브 도구 호출은 모델에게 리모컨을 쥐여 주는 것과 같아요. 한 번 누르면 파일 하나 읽고, 결과가 오면 또 누릅니다. 누를 때마다 완전한 샘플링 라운드이고, 모델은 커진 컨텍스트를 다시 읽어야 다음을 정할 수 있어요. 로그 파일 3개를 읽고 보고서를 쓰면 이미 5라운드이고, 50개로 늘리면 예산과 인내가 함께 타 버립니다. 비용 병목은 도구 자체가 아니라, 한 스텝에 한 번 샘플링하는 리듬에 있어요.

아이디어는 무엇인가

한 샘플링 라운드에서 모델이 TypeScript 작은 프로그램을 바로 쓰고, 프로그램 안에서 await tools.name(args)를 원하는 만큼 호출해요. 루프와 분기도 괜찮아요. 프로그램이 샌드박스에서 일을 끝내면 중간 결과는 전부 샌드박스 변수에 남고, 끝난 뒤에는 print와 return 내용만 모델로 돌아갑니다. 도구 설명 원문은 “Only what you print or return comes back — curate it.”예요(출처: packages/core/tools/src/code-mode.ts 52행).

리모컨 모드(네이티브 도구 호출) 모델 도구 왕복 5회 — 매번 샘플링 라운드가 필요하고, 컨텍스트는 계속 커짐 프로그램 모드(Code Mode) 모델 샘플링은 1라운드만 worker 샌드박스 루프에서 도구 5개를 연달아 호출, 중간 결과는 변수에 남음 프로그램 한 편 print와 return만 돌아옴
수업용 구조도: 같은 다섯 단계 과제 — 위는 왕복 5회, 아래는 1회.
왕복 다섯 번이 한 번이 됩니다.

이 문장에는 출처가 있어요. preset 파일 머리의 설계 의도 주석 원문이 “five round trips becomes one”이고, 이번 레슨 제목도 여기서 나왔어요:

apps/cli/config/agent-presets/code/agent.cordis.yml1–6행 발췌
# The `code` agent preset: the standard coding agent, presented as Code Mode.
#
# Everything in `standard` is here unchanged. What is added is the `tool-presentation`
# row: instead of one tool call per action, the model writes a TypeScript
# program against a generated SDK and `run_code` executes it, so a sequence
# that would be five round trips becomes one.
소스 스냅샷 안내: 로컬 저장소 deepseek-harness-master 기준, 확인 파일 apps/cli/config/agent-presets/code/agent.cordis.yml, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.

주석 후반에는 복선도 있어요. 이 preset이 바꾸는 건 도구 제시 방식뿐이고, 레지스트리 자체는 호스트에 남습니다(출처: 같은 파일 8–11행). 아이디어 2에서 말하는 권한 추종도 이 배치에서 출발해요.

왜 오래가는가

이건 네트워크 프로그래밍 수십 년의 오래된 이치예요. 왕복은 비싸고, 배치는 싸요. DB에는 일괄 쓰기가 있고, RPC 프레임워크도 배치를 모읍니다. 모델 샘플링 한 라운드는 네트워크 왕복 한 번보다 훨씬 비싸서, N번 왕복을 한 번으로 합치면 이득은 더 커져요. 언젠가 DSH를 다른 언어로 다시 써도 이 장부는 그대로 성립합니다.

아이디어 2 · 남이 쓴 코드를 돌릴 때는, 일단 적으로 보세요
어떤 문제를 푸는가

아이디어 1에는 풀리지 않은 전제가 있어요. 샌드박스에서 도는 건 모델이 방금 쓴 코드고, 한 줄도 심사받지 않았어요. 편하자고 호스트 프로세스에서 바로 돌리면 사고 방식이 널려 있어요. 환경 변수의 API key를 집어 가고, while (true) 하나가 메모리를 다 먹으면 호스트도 같이 죽고, 프로그램이 메시지를 위조해 도구 결과인 척 상위 계층을 속일 수도 있어요. 그래서 호스트는 첫날부터 상대가 나쁜 짓을 한다고 가정해야 하고, 그 가정이 서면 나머지는 엔지니어링 문제입니다.

아이디어는 무엇인가

DSH의 방식은 방어선 세 겹에, 권한 규칙 하나예요.

첫째, 자원을 용접해 닫아요. 실행마다 새 worker를 띄우고 쓰면 버립니다. 시작 인자로 길을 막아요. 환경 변수를 비워 프로그램이 호스트 자격증명을 못 갖게 하고, 상속 로더 플래그를 끊고, 힙 상한을 용접해 두면 프로그램이 힙을 터뜨려도 worker가 바로 종료합니다(출처: packages/code-runtime/code-runtime-worker-thread/src/index.ts 378–387행).

둘째, 통신은 구조화 메시지만 인정해요. 호스트와 worker 사이에는 메시지 포트 하나뿐이고 메모리는 공유하지 않아요. 업스트림 메시지는 call, log, output-limit, done 네 가지뿐이고, 인바운드마다 먼저 모양을 검사한 뒤 필드별로 깨끗한 사본을 다시 만들며, 쓰레기는 조용히 버립니다(출처: 같은 파일 142–165행). worker 쪽 tools 네임스페이스는 null-prototype으로 만들어, __proto__ 같은 위조 이름으로는 아무것도 만질 수 없어요(출처: bootstrap.ts 324–326행).

셋째, 시간과 바이트를 양쪽에서 기장하고, worker가 스스로 보고한 숫자는 믿지 않아요. 시간은 장부 두 권이에요. computeMs는 worker의 실측 바쁜 시간을 폴링해서 핫 루프를 숨기지 못하게 하고, 느린 도구를 기다리는 시간은 억울하게 과금하지 않아요. maxWallMs는 최후 보루로, 예산 두 개가 한도에 닿으면 바로 강제 종료합니다(출처: index.ts 534–545행). 바이트는 worker가 보내기 전 자체 사전 검사하고, 호스트는 받은 뒤 OutputLedger로 다시 적어요(출처: index.ts 169–229행). 듣기 좋은 숫자만 보고해서는 안 됩니다.

모델 로그와 반환값만 봄 호스트 프로세스(검수, 승인, 기장) 검수: 타입을 벗기고 바인딩 명단을 확인 불합격 프로그램은 바로 거부, worker는 시작하지 않음 승인: 네이티브 모드와 같은 도구 파이프라인 하위 호출마다 부모 token을 달고, 먼저 pre-execute를 통과 기장: 시간과 바이트를 양쪽에서 대조 computeMs、maxWallMs、OutputLedger worker 샌드박스 프로그램(모델이 씀) 실행마다 새로, 쓰면 버림 tools 네임스페이스 명단에 있는 도구만 포함 자원 상한 빈 환경, 힙 상한, 강제 종료 프로그램 reply call 로그 + 반환값 호스트와 worker 사이에는 구조화 메시지만, 인바운드마다 먼저 모양을 검사한 뒤 필드별로 재구성
수업용 구조도: 노드와 연결은 소스 관계를 설명하며, 내용은 수업에 맞게 정리했습니다.

그다음이 권한 규칙이에요. 프로그램이 샌드박스에 들어가도 승인은 한 치도 풀리지 않아요. 매번 await tools.xxx는 호스트가 하위 디스패치로 감싸, 부모 호출의 token을 달고 네이티브 모드와 같은 pre-execute 승인 폭포를 탑니다. 하위 호출 id는 callId:code:n 형태고, 이벤트에 처음부터 끝까지 흔적이 남아요(출처: packages/core/tools/src/code-mode.ts 545, 477, 470행). 프로그램이 바인딩할 수 있는 도구는 System Prompt에 선언된 것들뿐이고, 제한된 도구는 명단에서 바로 사라집니다(출처: 같은 파일 601–608행). 반대로 mode: code에서 run_code를 우회해 네이티브 호출을 바로 보내면, 정책 파이프라인에 들어가기 전에 UNKNOWN_TOOL로 거부됩니다(출처: docs/subsystems/tools.zh.md). 입구는 좁혔지만, 권한의 문은 바꾸지 않았어요.

DSH 스스로도 이 격리의 위치를 냉정하게 잡아요. README는 바로 이렇게 시작합니다:

이것은 격리 조치이지 보안 경계가 아닙니다: 신뢰 입장은 의도적으로 bash와 동등…하지만 bash에 없는 격리를 제공합니다: 독립 isolate, 빈 환경, 힙 상한, 강제 종료.”

출처: packages/code-runtime/code-runtime-worker-thread/README.zh.md, 줄임표는 원문 인용 생략
왜 오래가는가

불신 경계는 보안 설계의 보편 형태이고, worker_threads라는 구체 기술과 상관없어요. 컨테이너, V8 isolate, 다른 언어의 서브프로세스로 바꿔도 할 일은 이 네 가지예요. 깨끗한 환경을 새로 열고, 자원을 상한에 두고, 좁은 인터페이스로 통신하며 메시지마다 검증하고, 장부는 양쪽이 각자 적습니다. 브라우저가 웹페이지에, OS가 프로세스에 쓰는 것과 같은 생각이고, 모델 코드는 명단에 새로 온 한 명일 뿐 대우는 그대로예요.

가로 비교 · 누가 등가물을 갖고 있나

Claude Code

등가물이 없어요. bash 도구는 범용 비상구라 모델이 스크립트를 써 실행할 수 있지만, Read·Edit 같은 도구 API는 스크립트에 프로그래밍 가능한 바인딩으로 노출되지 않고, 스크립트 안의 동작도 각 도구 자신의 파이프라인을 타지 않아요. Anthropic 공식 블로그 “Code execution with MCP”가 같은 아이디어를 냈지만, 확인일 기준 Claude Code 제품에 내장된 동급 run_code 메커니즘은 보이지 않았어요.

로컬 study 자료 검색 기준

Grok Build

이 메커니즘은 없어요. 공개 증거 기준입니다. 로컬 grok-build-main 저장소 전체에서 run_code와 code mode를 검색하면, 텔레메트리 이벤트 이름이 글자만 겹칠 뿐이에요. 도구 체계는 네이티브 호출과 toolset preset 조합이고, 모델이 프로그램을 써서 샌드박스가 도구 API를 오케스트레이션하는 통로는 없습니다.

로컬 소스 검색 기준 · 2026-08-13

Codex CLI와 Cloudflare

아이디어는 통하지만, 이번 레슨은 두 곳 소스를 대조하지 않았고 사실만 말해요. DSH 설계 노트는 Cloudflare 블로그를 명시적으로 인용하며, 핵심 관찰은 모델이 코드를 쓰는 능력이 도구 호출을 연달아 보내는 것보다 낫다는 점입니다(출처: .agents/notes/implemented/feature/2026-06-15-code-mode.zh.md). Codex CLI 쪽에도 비슷한 공개 논의가 있지만, 역시 아이디어만 언급합니다.

소스 미대조 · 공개 서술만 인용
수업 실습
01

악의적 프로그램 두 편, 각각 어느 예산에 부딪히나?

프로그램 A는 동기 핫 루프 while (true) {}이고, 프로그램 B는 영원히 resolve되지 않는 await new Promise(() => {})예요. 아이디어 2의 세 번째 방어선에 맞춰 추론해 보세요. A와 B는 각각 computeMs와 maxWallMs 중 무엇에 종료되나요? A가 미완료 도구 호출 하나를 걸어 과금을 피할 수 없는 이유는 뭔가요?

Takeaway: Code Mode(공식 발표명 PTC)는 샘플링 라운드 한 번으로 N번 왕복을 바꿉니다. 모델이 프로그램을 쓰고, 샌드박스가 대신 돌리며, 중간 값은 컨텍스트에 들어가지 않아요. 샌드박스는 격리이지 보안 경계는 아니고, 빈 환경·힙 상한·이중 예산·양쪽 기장으로 스스로를 지킵니다. 샌드박스에 들어갔다고 권한이 풀리지 않으며, 하위 호출마다 같은 승인 폭포를 그대로 탑니다.