플러그인·클라이언트
fac5e60.
ds-harness-remote는 DeepSeek Harness의 플러그인으로 동작하는 Host와 여러 Client를 연결합니다. 사용자가 Android에서 프롬프트를 보내면 실제 작업은 Host의 Harness 또는 연결된 에이전트 백엔드가 수행합니다. 작업 환경을 휴대전화로 옮기는 대신, 원래 컴퓨터의 작업 환경에 암호화된 요청을 보내는 구조입니다.
Host는 공구와 작업물이 있는 작업실, Client는 조종석입니다. Server는 어느 작업실이 온라인인지 알려 주고 연결을 중개하는 안내 데스크입니다. 대화 내용은 봉인된 운송 상자에 들어가므로, 중계 역할만 하는 Server는 상자 속 업무 내용을 해독할 키를 갖지 않습니다.
다만 문을 통과했다고 모든 공구를 쓸 수 있는 것은 아닙니다. Host는 요청마다 허용된 API인지 다시 검사합니다. 연결의 신원 확인과 작업의 권한 확인이 별도로 필요합니다.
Desktop에서는 Harness의 기존 UI를 재사용하고, Android는 React Native 앱을 제공합니다.
VS Code 확장과 Chrome/Edge 확장도 포함되지만 역할이 다릅니다.
apps/browser는 온라인 Host를 골라 Remote Web을 여는 입구이며, 자체 대화 UI나 원격 전송 엔진을 구현한 브라우저 앱은 아닙니다.
처음에는 233줄 분량인 packages/client-core/src/index.ts부터 읽어 보세요.
요청 ID, 타이머, 취소, 연결 종료만으로도 네트워크 프로그램의 핵심 실패 상황을 배울 수 있습니다.
그다음 packages/webrtc, packages/crypto, Host bridge 순서로 넓히면 됩니다.
이 글의 선정 근거는 TrendShift Daily #16입니다. 2026-10-23 GitHub 기준 스타 699, 포크 70이며, 생성일은 2026-08-14입니다. 이 숫자만으로 사용자 증가 속도나 인기의 원인을 확정할 수는 없습니다. 아래의 주목 포인트는 README와 구현을 바탕으로 한 기술적 해석입니다.
코딩 에이전트는 프로젝트 파일, 터미널, 인증 상태, 대화 이력에 의존합니다. 이동할 때마다 다른 기기에 환경을 복제하면 같은 작업을 이어 가기 어렵습니다. 이 프로젝트는 오래 실행되는 Host에 여러 화면을 연결해 그 마찰을 줄이려 합니다.
주목할 지점은 원격 접속 자체보다, 코딩 에이전트의 세션·승인·파일·터미널을 연결 단위로 다루는 설계입니다.
ConnectionController.acceptOne()은 인증된 연결마다 별도의 router를 만들고,
HarnessRemoteBridge는 스트림과 큰 응답의 전송 상태를 관리합니다.
채팅 화면만 붙이는 일과는 구현 범위가 다릅니다.
아래 표는 특정 상용 제품의 최신 기능표가 아니라 접근 방식의 비교입니다. 우열은 원격으로 무엇을 하려는지에 따라 달라집니다.
| 접근 방식 | 주로 보내는 것 | 이 레포의 상대적 장점과 경계 |
|---|---|---|
| SSH + 터미널 세션 | 명령과 터미널 출력 | 에이전트의 작업공간·대화·승인을 앱 UI로 표현합니다. 일반 셸 관리의 범용성은 별도 문제입니다. |
| 원격 데스크톱 | 화면과 입력 이벤트 | 업무 API와 이벤트를 전달하므로 휴대전화용 UI를 따로 구성할 수 있습니다. 전체 데스크톱 조작은 제공하지 않습니다. |
| VPN + 기존 웹 UI | 사설망 접근 경로 | 기기 신뢰, 연결별 RPC와 업무 권한 검사가 프로젝트 안에 통합됩니다. VPN의 일반 네트워크 접근과 목적이 다릅니다. |
| 범용 터널 | 포트로 흐르는 바이트 | 고정 API 허용 목록과 세션 범위를 적용합니다. 임의 TCP 전달을 위한 도구로 해석하면 안 됩니다. |
| ds-harness-remote | 암호화한 RPC·이벤트 | Harness UI 재사용과 다중 접속에 초점을 둡니다. Harness 버전과 별도 Server 기능에 영향을 받습니다. |
공유 전송 구현인 AdaptiveTransport의 기본 선호 순서는
['lan', 'p2p', 'turn', 'relay']입니다.
하지만 실제 경로는 양쪽 capability와 Server 지원을 협상해 정합니다.
직접 통신을 시도한다고 항상 직접 연결되는 것은 아닙니다.
apps/server/src/gateway.ts는 webrtcEnabled: false와
['transport.relay']를 응답합니다. 자체 호스팅 버전은 단일 계정·단일 프로세스의 Relay 서버입니다.
전체 Remote Web 대화 화면, 다중 계정 관리, WebRTC/TURN 서비스는 이 서버에 포함되지 않습니다.
README의 Web 지원은 제품 전체 설명입니다. 이 저장소에 모든 Web 서비스의 구현이 들어 있다는 뜻은 아닙니다.
.github/release-notes.md의 v0.5.18 기록은 Android 로그인 방식 자동 조회,
서버 변경 후 늦게 도착한 로그인 결과의 격리, 도구 응답 파싱 경계, 대기 메시지 표시 수정 등을 설명합니다.
이는 새로운 네트워크 기술만큼 비동기 상태의 일관성이 제품 품질에 중요함을 보여 줍니다.
해당 릴리스 기록에 따르면 전체 794개 테스트가 통과했습니다. v0.5.18에서 추가된 실기기·기기 간 검증은 없다고 명시되어 있으므로, 자동화 테스트 결과와 실환경 검증 범위를 구분해야 합니다.
루트 package.json과 각 하위 패키지는 버전 0.5.18로 맞춰져 있습니다.
pnpm-workspace.yaml은 apps/*, packages/*, examples/*를 묶습니다.
여러 앱이 같은 프로토콜을 공유하되, 각 실행 환경의 의존성과 버전은 따로 관리합니다.
| 영역 | 의존성 파일의 실제 표기 | 구현에서 맡는 일 |
|---|---|---|
| 자체 호스팅 서버 | Node.js 22 · ws ^8.21.3 · zod ^3.24.1 | Node HTTP 서버, WebSocket 제어·중계, 요청·기기 정보 검증 |
| Host 확장 기반 | @deepseek-ai/cordis 4.0.5 · @deepseek-ai/dsh-api-gateway 0.2.1-rc.1 | 개발 의존성 기준 플러그인 구성과 공식 Gateway 접속 |
| Host WebRTC | werift 0.24.4 · 선택 의존성 @roamhq/wrtc 0.10.0 | Node 환경의 WebRTC 구현 선택지 |
| 암호화 | @lukeburns/clatterjs 1.0.0 · @noble/ciphers ^1.1.3 | Noise 핸드셰이크와 인증 암호화 원시 기능 |
| 키와 해시 | @noble/curves ^1.7.0 · @noble/hashes ^1.6.1 | X25519 키, HKDF, SHA-256 |
| Host 터미널 | @xterm/headless 6.0.0 · @xterm/addon-serialize 0.14.0 | 화면 상태 유지와 재접속 스냅샷 |
근거 파일은 apps/server/package.json, packages/plugin/package.json,
packages/crypto/package.json입니다.
자체 호스팅 서버는 Express·NestJS 없이 node:http의 createServer()를 사용합니다.
apps/server/src/store.ts는 데이터베이스 대신 JSON 파일과 메모리 상태를 조합합니다.
| 접속면 | 선언 버전 | 읽어 볼 구현 |
|---|---|---|
| Android | expo ~57.0.13, react 19.2.3, react-native 0.86.2 | apps/android/src/state와 apps/android/src/screens |
| Android 전송·상태 | react-native-webrtc ^124.0.8, zustand ^5.0.15 | 네이티브 WebRTC와 상태 저장소 |
| Android 보관·렌더링 | expo-secure-store ~57.0.1, react-native-webview 13.16.1, pdfjs-dist 6.3.289 | 자격 증명 보관과 로컬 번들 기반 문서·터미널 표현 |
| 서버 상태 Web UI | react ^18.3.1, antd ^5.22.7, vite ^6.0.3 | apps/server/web/src/main.tsx의 로그인·기기 화면 |
| Desktop 플러그인 UI | React peer 범위 >=18.2.0 <19 | packages/plugin/src/client/에서 Harness UI 확장 |
| VS Code | 엔진 ^1.96.0, @vscode/vsce ^3.2.1 | apps/vscode의 확장과 SecretStorage 연결 |
| 브라우저 확장 | Manifest V3 · lucide ^0.469.0 | apps/browser/manifest.json의 popup·service worker |
^3.24.1은 package.json이 허용하는 범위이고,
실제로 고정한 버전은 잠금 파일에서 확인합니다.
루트 패키지의 Zod는 pnpm-lock.yaml에서 3.25.76으로 고정되어 있습니다.
선언 버전과 설치 결과를 같은 숫자로 취급하면 재현성 설명이 틀어집니다.packageManager: pnpm@9.15.4를 지정합니다.
공유 패키지 의존성은 workspace:*로 같은 저장소의 코드를 가리킵니다.tsconfig.base.json은 target: ES2022,
module: NodeNext, strict: true입니다. Android는 TypeScript ~6.0.3,
공유 패키지들은 주로 ^5.7.3을 선언합니다.^0.25.0을 사용합니다.
Android는 Expo·Metro 계열의 별도 빌드 경로를 가집니다.^2.1.8을 선언합니다.
요청 취소·연결 종료 같은 핵심 동작의 검증 사례를 담고 있습니다.apps/server/Dockerfile은
node:22-bookworm-slim으로 빌드·실행 단계를 나눕니다. 실행 사용자는 node입니다..github/workflows/ci.yml은 Node 22,
.github/workflows/release.yml의 주 릴리스 작업은 Node 24, Android 작업은 Node 22와 Java 17을 사용합니다.루트 package.json에는 의도적으로 scripts가 없습니다.
AGENTS.md의 설명에 따르면 Harness가 GitHub에서 플러그인을 설치할 때 루트 패키지를 사용하기 때문입니다.
커밋된 packages/plugin/dist/index.js와 packages/plugin/dist/client.github.js는 배포 진입점이며,
CI가 다시 만든 번들과 차이가 없는지 검사합니다. 일반적인 “모든 dist는 무시한다” 규칙을 그대로 적용할 수 없는 사례입니다.
위 도식의 보안 전송 래퍼는 역할을 나타냅니다. 실제 구현은 Client마다 접속 코드가 있으며,
Desktop 쪽 대표 구현이 ClientSecureTransport입니다.
Server와 오가는 제어 연결과 기기끼리 보내는 업무 데이터는 개념적으로 분리되지만,
Relay를 선택하면 둘 다 같은 WebSocket을 이용할 수 있습니다.
packages/webrtc/src/transport.ts의 RemoteTransport는
바이트를 보내고 받는 최소 약속을 정의합니다.
RPC 계층은 현재 경로가 WebRTC인지 Relay인지 몰라도 이 약속만 사용하면 됩니다.
// 원본 RemoteTransport에서 핵심 메서드만 발췌
export interface RemoteTransport {
connect(): Promise<void>
send(data: Uint8Array): Promise<void>
onMessage(cb: (data: Uint8Array) => void): () => void
onClose?(cb: () => void): () => void
close(): Promise<void>
getStats(): TransportStats
}
AdaptiveTransport.negotiate()는 capability로 걸러진 선호 경로를 이용합니다.
WebRTC를 쓸 수 있으면 tryWebRtc()를 호출하고, 협상 실패 시 허용된 Relay로 내려갑니다.
forceRelay: true는 선호 경로를 Relay 하나로 제한합니다.
업무 요청은 같은 형태를 유지하고, 연결 경로와 암호화는 그 아래 계층에서 책임집니다. 이런 분리는 새 전송 수단을 추가할 때 UI나 모든 RPC 호출부를 바꾸지 않게 해 줍니다.
negotiate()의 실패는 Relay 선택으로 이어질 수 있습니다.
반면 이미 사용 중인 WebRTC가 닫히면 rtc.onClose()가 failConnection()을 호출합니다.
README도 새 전송과 새 Noise 채널로 다시 연결한다고 설명합니다.
기존 암호화 상태를 그대로 유지한 무중단 경로 전환으로 표현하면 부정확합니다.
packages/plugin/src/client-secure-transport.ts의 ClientSecureTransport는
내부 SecureHandshakeTransport를 감싸면서 자신도 RemoteTransport를 구현합니다.
기존 인터페이스를 유지한 채 보안 책임을 추가하는 데코레이터 패턴으로 읽을 수 있습니다.
connect()가 내부 전송을 연결하고, 반환된 기기 ID가 예상한 Host·Client와 같은지 확인합니다.NoiseIkSession을 만들고 createNoisePrologue()에 연결 ID와 두 기기의 ID를 묶습니다.waitForResponder()가 두 번째 핸드셰이크 메시지를 기다립니다. 기본 제한 시간은 10초입니다.send()는 메시지를 분할한 뒤 각 조각을 암호화합니다. 수신은 복호화 후 재조립합니다.packages/crypto/src/noise.ts는 암호 모음을
Noise_IK_25519_ChaChaPoly_SHA256으로 고정합니다.
verifyRemoteStatic()은 상대의 정적 공개키가 로컬에서 신뢰한 키와 같은지 확인합니다.
계정 로그인 성공만으로 상대 기기의 정체까지 보장되지는 않는다는 뜻입니다.
// createNoisePrologue()가 만드는 문맥의 형태
DSH-REMOTE\0v=1\0connection=...\0host=...\0client=...
// 메시지의 계층별 변화
RPC JSON 바이트
-> SecureMessageCodec.encode()
-> NoiseIkSession.encrypt()
-> DataChannel 또는 Relay
-> NoiseIkSession.decrypt()
-> SecureMessageCodec.decode()
-> RPC 요청 처리
Noise 공식 명세는 메시지 상한을 65,535바이트로 정합니다.
그래서 이 프로젝트의 SecureMessageCodec은 48 KiB보다 큰 메시지를 먼저 분할합니다.
완성 메시지 상한은 4 MiB, 동시에 조립할 수 있는 메시지는 8개입니다.
분할 헤더에는 메시지 ID·조각 순번·전체 조각 수·전체 길이가 들어갑니다.
decode()는 순번과 길이를 검증하므로, 조각을 모으는 배열 하나로 끝나는 구현이 아닙니다.
packages/client-core/src/index.ts의 RemoteClientCore.rpc()는
pending: Map<string, PendingCall>에 요청별 완료 함수와 타이머를 넣습니다.
응답은 requestId로 원래 요청을 찾아 완료시킵니다. 응답 순서가 바뀌어도 요청을 구별할 수 있습니다.
// 원본 제어 흐름을 줄여 표현한 학습용 의사 코드
요청 A 등록 -> pending[A] = 완료 함수 + 타이머 + 취소 해제 함수
요청 B 등록 -> pending[B] = 완료 함수 + 타이머 + 취소 해제 함수
응답 B 도착 -> takePending(B) -> B만 완료
A 취소 -> takePending(A) -> A만 거절
늦은 응답 A -> 등록된 요청 없음 -> 무시
takePending()은 Map에서 제거하고 타이머와 취소 리스너를 함께 정리합니다.
기본 RPC 제한 시간은 30초이며, 전송 자체를 await한 뒤 기다리지 않습니다.
send()가 끝나지 않는 상황에서도 타임아웃·취소·연결 종료가 호출자를 풀어 줄 수 있게 한 선택입니다.
여기서의 취소는 우선 Client의 기다림을 끝내는 동작입니다.
이것만 보고 Host의 실행 중인 작업도 반드시 취소됐다고 말하면 안 됩니다.
작업 중단은 session/cancel 등 해당 업무 API의 의미를 따로 확인해야 합니다.
packages/plugin/src/connection-controller.ts의 acceptOne()은
Noise 프로토콜, 비어 있지 않은 연결·membership 문맥, IdentityStore.isTrusted()를 확인합니다.
통과한 연결에는 createRouter({ connectionId, peerDeviceId }, ...)로 router를 만듭니다.
이후 packages/plugin/src/harness-remote-bridge.ts의
HarnessRemoteBridge.call()이 HARNESS_REMOTE_ALLOWLIST와 입력 스키마를 검사합니다.
예를 들어 일반 설정 API가 허용 목록에 있어도 Remote 자신의 설정을 바꾸는 요청은 별도로 거절합니다.
// HarnessRemoteBridge.call()의 원본 일부
if (['settings/update', 'settings/replace', 'settings/mutate'].includes(params.endpoint)) {
const args = requestArgs(params.payload)
if (args.ns === 'ds-harness-remote' || args.ns === 'dsh-remote') {
throw new RpcError('PERMISSION_DENIED',
'Remote access settings can only be changed locally on the Host.')
}
}
이 검사는 원격 접속자가 터미널·포트 접근 정책을 원격에서 다시 넓히는 것을 막는 경계입니다.
ACP도 packages/plugin/src/acp/method-policy.ts의 parseAcpCall()에서
메서드와 매개변수를 제한합니다. 암호화와 허용 목록은 서로 다른 문제를 해결합니다.
연결별 router는 호텔 객실별 프런트 담당과 같습니다. 같은 계정의 기기라도 어느 연결의 승인·구독·터미널 입력권인지 구분해야 합니다. 방 번호를 구분하지 않으면 A가 요청한 승인에 B가 응답하는 식의 혼선이 생깁니다.
protocol은 통신 양쪽이 동의해야 할 데이터 형태를 제공합니다.
crypto는 기기 키와 암호화 상태를 담당하고, webrtc는 전송 경로를 담당합니다.
client-core는 그 위에서 요청·응답과 업무 이벤트를 연결합니다.
앱은 이 공유 코드를 조합하되 로그인 UI, 네이티브 보관소, 네트워크 생명주기 같은 플랫폼별 문제를 직접 처리합니다. Host 플러그인은 공식 Harness API와 외부 에이전트 프로세스에 접속하므로 가장 많은 업무 코드가 모입니다. 폴더를 분리했다는 사실보다 어느 계층이 어느 사실을 알아야 하는가가 중요합니다.
집계 대상은 커밋 fac5e60의 추적 파일이며, 확장자별 물리적 줄 수를 기준으로 합니다.
빈 줄·주석·테스트를 포함하며, packages/plugin/dist/ 아래 배포 번들은 별도 항목입니다.
JSON·YAML·Markdown·이미지는 아래 언어별 합계에 포함하지 않았습니다.
| 언어·형식 | 파일 수 | 물리적 줄 수 | 해석 |
|---|---|---|---|
| TypeScript (.ts) | 364 | 90,647 | 프로토콜·Host·클라이언트 서비스와 테스트 포함 |
| TSX (.tsx) | 42 | 11,001 | React·React Native 화면 |
| JavaScript 계열 | 18 | 1,317 | .js·.mjs·.cjs 빌드 보조 등 |
| CSS | 3 | 2,615 | 서버 Web UI 등의 스타일 |
| Kotlin | 4 | 364 | Android 네이티브 연동 |
| Shell / HTML | 1 / 2 | 251 / 15 | 설치 스크립트와 HTML 입구 |
| 제외한 배포 번들 | 2 | 48,155 | packages/plugin/dist/의 JavaScript, 원본 규모와 중복 |
TypeScript와 TSX를 합치면 406개 파일, 101,648줄입니다.
그 안의 TypeScript 테스트는 tests·__tests__ 경로나
.test. 파일명 기준 117개 파일, 31,276줄입니다.
줄 수는 구현의 복잡도나 품질 점수가 아니라, 읽기 범위를 가늠하기 위한 대략적인 규모입니다.
같은 기준으로 Plugin의 TypeScript·TSX는 테스트 포함 51,555줄, Android는 32,552줄입니다. 처음부터 화면 전체를 읽기보다, 요청 하나를 여러 계층에 걸쳐 추적하는 편이 낫습니다.
이 레포에서 가장 좋은 공부 방법은 정상 요청 하나와 실패 요청 하나를 같은 경로로 따라가는 것입니다. 예를 들어 “세션 목록이 보인다”와 “세션 목록 응답 전에 연결이 끊긴다”를 함께 그려 보세요. 오류 처리가 어느 클래스의 책임인지 선명해집니다.
TypeScript의 타입은 개발 중 실수를 줄여 주지만 네트워크에서 도착한 JSON이 그 타입이라고 보장하지 않습니다.
packages/protocol/src/index.ts의 parseControlFrame()은
바깥 메시지 형태를 검사한 뒤, controlFramePayloadSchemas에서 종류별 본문 스키마를 선택합니다.
// 원본 parseControlFrame()의 핵심
const frame = controlFrameSchema.parse(input)
const payloadSchema = controlFramePayloadSchemas[frame.type]
if (payloadSchema) {
return { ...frame, payload: payloadSchema.parse(frame.payload) }
}
return frame
실습 아이디어: relayPayloadSchema를 읽고
counter가 음수·소수·문자열인 입력을 각각 종이에 써 보세요.
어떤 조건이 거절하는지 표시하면 “타입이 숫자”와 “프로토콜에서 유효한 숫자”의 차이를 이해할 수 있습니다.
RemoteClientCore는 성공, RPC 오류, 타임아웃, 취소, 전송 실패, 연결 종료를 처리합니다.
등록된 요청의 완료·실패 경로가 takePending()를 거쳐 정리되도록 만든 점을 보세요.
“응답이 오면 resolve한다”만 구현하면 나머지 경로의 타이머·리스너·Map 항목이 남을 수 있습니다.
실습 아이디어: packages/client-core/tests/client-core.test.ts에서
LoopbackTransport를 읽고, 실제 네트워크 없이도 연결 종료를 표현하는 drop()을 찾아보세요.
이는 테스트가 외부 서비스 대신 작은 인터페이스를 대체하는 방법입니다.
packages/webrtc/src/websocket-backpressure.ts의 waitForRelayCapacity()는
bufferedAmount가 512 KiB를 초과하면 기다립니다.
10초를 넘기거나 소켓이 닫히면 실패합니다. 소비자가 느릴 때 생산자의 속도를 조절하는 장치입니다.
SerialSend.run()은 Promise 체인으로 송신을 순서대로 실행하고,
대기량을 최대 8 MiB·256건으로 제한합니다.
암호화와 송신 순서를 함께 지켜야 nonce 상태와 실제 전송 순서가 어긋나지 않습니다.
// SerialSend.run()의 원본 핵심
if (this.bytes + bytes > 8 * 1024 * 1024 || this.count >= 256) {
throw new Error('Encrypted send queue exceeds its limit')
}
this.bytes += bytes; this.count += 1
const task = this.tail.then(send)
this.tail = task.catch(() => undefined)
try { await task } finally { this.bytes -= bytes; this.count -= 1 }
실습 아이디어: 첫 작업이 대기 중일 때 두 번째 작업이 시작되는지,
첫 작업이 실패하면 세 번째 작업은 계속 가능한지 추적해 보세요.
packages/webrtc/tests/serial-send.test.ts가 순서와 대기량을 검증하는 이유도 설명할 수 있어야 합니다.
packages/webrtc/src/rtc-adapter.ts는 실행 환경의 WebRTC 구현을 추상화하고,
packages/webrtc/src/rtc-data-channel.ts는 실제 DataChannel과 ICE 협상을 다룹니다.
packages/webrtc/src/adaptive-transport.ts는 그 결과를 업무 전송 경로로 선택합니다.
ICE는 연결 가능한 후보를 찾는 절차이고, STUN은 외부에서 보이는 주소를 찾는 데 도움을 주며, TURN은 직접 통신이 어려울 때 중계합니다. 이 기본 구분은 MDN의 WebRTC 프로토콜 설명에서 확인할 수 있습니다.
실습 아이디어: tryWebRtc()에서 ICE 서버 조회가 실패했을 때의 코드를 읽어 보세요.
빈 ICE 서버 목록으로도 직접 연결 후보를 시도하는 흐름과,
최종 협상 실패 후 Relay를 선택하는 흐름을 두 단계로 나눠 그립니다.
packages/crypto/src/index.ts에는 deriveSharedKey()와 encryptFrame()도 있습니다.
하지만 현재 Desktop 원격 연결 경로의 핵심은 packages/crypto/src/noise.ts의 NoiseIkSession입니다.
폴더에서 함수 하나를 찾았다는 이유로 그 함수가 현재 통신 전체를 구현한다고 판단하면 안 됩니다.
실습 아이디어: packages/crypto/tests/noise.test.ts와
fixtures/crypto/v1/noise-ik.json을 비교해 보세요.
고정 입력과 기대 출력이 왜 다른 구현끼리의 호환성을 확인하는 데 유용한지 적습니다.
학습용 고정 난수와 실제 실행용 난수의 목적도 구분해야 합니다.
apps/android/src/state/account-actions.ts의 createAccountActions()는
accountGeneration을 증가시키고, captureAccount()로 현재 계정·설정·identity가 여전히 같은지 확인합니다.
exclusive()는 자격 증명 관련 쓰기를 Promise 체인으로 순서화합니다.
사용자가 A 서버 로그인 도중 B 서버로 전환한 뒤 A의 응답이 도착할 수 있습니다. 이때 단순히 “가장 늦게 도착한 값을 저장”하면 새 서버의 상태를 덮어씁니다. 요청을 시작한 세대와 현재 세대가 같은지 확인하는 방식으로 그런 결과를 걸러낼 수 있습니다.
실습 아이디어: A 로그인 시작 → B 서버 선택 → B 로그인 완료 → A 응답 도착의 시간표를 만드세요. 각 단계의 세대 번호를 쓰고, 어떤 결과가 보관소와 화면 상태를 바꿀 수 있는지 표시합니다.
apps/server/src/store.ts의 Store.save()는 임시 파일을 쓰고 renameSync()로 교체합니다.
폴더 생성은 0o700, 파일 생성은 0o600을 지정합니다.
장기 보관되는 토큰은 원문 대신 SHA-256 다이제스트로 색인합니다.
파일 교체 방식이 곧 다중 프로세스 데이터베이스의 동시성 보장을 뜻하지는 않습니다. 이 구현은 단일 프로세스를 전제로 하며, 동기 파일 쓰기와 상태 크기도 운영 특성에 영향을 줍니다. 실습 아이디어: 두 프로세스가 같은 파일을 따로 읽고 저장한다면 어떤 갱신이 사라질 수 있는지 그립니다.
CI의 전체 빌드에는 --workspace-concurrency=1이 지정되어 있습니다.
워크플로 주석은 Android 사전 빌드가 공유 패키지를 다시 컴파일하므로,
Plugin 번들이 작성 중인 dist를 읽지 않도록 직렬화한다고 설명합니다.
빌드 도구의 빠른 병렬 실행도 산출물 간 의존성을 무시할 수 없다는 사례입니다.
원격 접속 프로그램의 자원 요구량과, 그 뒤에서 실행되는 AI 모델의 자원 요구량은 별개입니다. 이 저장소 자체가 모델 가중치를 제공하거나 GPU 추론 서버를 구현하는 것은 아닙니다. Host에서 사용하는 모델 제공자·도구·프로젝트에 따라 추가 자원이 필요합니다.
| 역할 | 확인된 요구·설정 | 출처와 해석 |
|---|---|---|
| Harness Host | Remote 플러그인이 연결되는 Harness 환경 | README의 현재 기준은 dsh-v0.2.1-rc.1이며 0.2.0 rc.2/rc.1 호환을 설명합니다. |
| Node 개발 환경 | Node.js 22 · pnpm 9.15.4 | 서버 README·CI·Dockerfile 기준. 주 릴리스 작업만 Node 24를 사용합니다. |
| 자체 호스팅 서버 | 기본 포트 8080 · JSON 상태 파일 | DB·Redis 서비스는 Compose에 없습니다. 영속 볼륨 server-data가 구성되어 있습니다. |
| Android 기기 | minSdkVersion: 26 | apps/android/app.json의 네이티브 빌드 설정입니다. Android 8.0 이상에 해당합니다. |
| Android 개발 | Android SDK·네이티브 빌드 도구 · CI의 Java 17 | WebRTC 네이티브 모듈 때문에 Expo Go로 실행할 수 없다고 문서에 명시합니다. |
| VS Code | engines.vscode: ^1.96.0 | 확장 package.json의 엔진 조건입니다. 모든 기능의 실기기 검증을 의미하지 않습니다. |
| 브라우저 확장 | Chrome/Edge의 Manifest V3 | 현재 manifest의 Host 권한은 지정된 서비스 출처로 한정됩니다. |
| GPU·최소 RAM | 정량 최소값 미명시 | 서버·Android README는 CPU·RAM의 정량 최소 사양을 제시하지 않습니다. |
README의 “공개 수신 포트가 필요 없다”는 설명은 Harness Host에 대한 것입니다. Client와 Host가 외부로 연결하더라도 중계 Server 자체에는 접근 가능한 주소가 있어야 합니다. 자체 호스팅 시에는 Server의 공개 경로와 Host의 내부 작업 환경을 나눠 설계해야 합니다.
apps/server/compose.yaml의 기본 호스트 바인딩은 127.0.0.1이고,
컨테이너 안의 DSH_SERVER_HOST는 0.0.0.0입니다.
이 둘은 모순이 아닙니다. 앞의 값은 호스트에서 공개할 인터페이스,
뒤의 값은 컨테이너 내부에서 수신할 인터페이스입니다.
서버 README는 공개 서비스 앞에 HTTPS 역방향 프록시를 두고,
/ws/v1/connect의 WebSocket Upgrade와 75초보다 긴 유휴 시간을 지원하도록 안내합니다.
실제 Gateway의 ping 주기는 25초, 응답이 없을 때의 종료 기준은 75초입니다.
| 제한 | 코드 값 | 찾을 곳 |
|---|---|---|
| 제어 프레임 / Relay 프레임 | 64 KiB / 1 MiB | packages/protocol/src/index.ts |
| 보안 메시지 재조립 | 메시지당 4 MiB · 동시 8개 | SecureMessageCodec |
| 전송 대기열 | 8 MiB · 256건 | SerialSend.run() |
| Harness 원격 스트림 | 연결당 16개 상한 | MAX_ALPHA_STREAMS_PER_CONNECTION |
| 자체 호스팅 연결 수 | WebSocket·논리 링크 각각 256 상한 | Gateway의 수락·연결 생성 검사 |
| CodeX / ACP 터미널 | 세션당 8개 · 컨트롤러의 전체 상한 256개 | packages/plugin/src/remote-terminal-controller.ts |
| 터미널 화면 복원 | 스크롤백 1,000줄 · 스냅샷 256 KiB | 같은 파일의 SCROLLBACK·MAX_SNAPSHOT_BYTES |
256개를 허용하는 검사문이 있다고 256개 접속에서 일정한 지연 시간을 보장하는 것은 아닙니다. 현재의 하드 상한, 안정적으로 처리 가능한 부하, 실제 운영 용량은 서로 다른 수치입니다. 특히 큰 대화 이력과 파일 전송은 짧은 텍스트 프롬프트보다 메모리·중계 대역폭을 더 사용합니다.
packages/plugin/src/config.ts에서 terminal.enabled는 기본 활성화이고,
loopback.ports는 기본 빈 목록입니다.
README는 Linux 설치기가 systemd 시스템 서비스를 구성하며 root 권한으로 실행할 수 있음을 명시합니다.
연결 암호화는 잘못 부여한 운영체제 권한을 줄여 주지 않습니다.
실제 도입 전에는 Host 실행 계정, 터미널 허용 여부와 포트 목록을 먼저 검토해야 합니다.
아래 과제는 읽기와 계산으로 완성할 수 있습니다. 저장소 코드나 설치기를 실행할 필요가 없습니다. 각 과제의 목표는 기능 이름을 설명하는 데서 끝나지 않고, 실패할 때 어느 상태가 바뀌는지 보여 주는 것입니다.
읽을 파일: packages/client-core/src/index.ts, packages/webrtc/src/transport.ts,
packages/plugin/src/client-secure-transport.ts. 모두 공유 라이브러리와 플러그인을 담은 packages/ 아래입니다.
할 일: “세션 목록 요청” 카드에 요청 ID를 적고,
RemoteClientCore → 보안 래퍼 → 전송 → Host 순서로 놓습니다.
각 단계에서 다루는 것이 업무 객체인지, JSON 바이트인지, 암호문인지 표시합니다.
완료 조건: Server의 Relay가 왜 세션 목록의 내부 내용을 알 필요가 없는지 설명하고, 응답을 원래 Promise에 연결하는 ID는 어느 계층이 쓰는지 구별합니다.
읽을 파일: 루트 package.json,
apps/android/package.json, apps/server/package.json, pnpm-lock.yaml.
할 일: 프로젝트 릴리스 버전, 의존성 허용 범위, 잠금 버전을 각각 다른 열로 적습니다.
Zod의 ^3.24.1과 루트 잠금 버전 3.25.76,
Android React 19.2.3과 서버 Web React ^18.3.1을 예로 사용합니다.
완료 조건: “같은 저장소니까 React 버전이 모두 같다”가 왜 틀린지,
--frozen-lockfile을 CI에서 쓰는 목적이 무엇인지 자기 말로 적습니다.
읽을 파일: packages/client-core/src/index.ts와
packages/client-core/tests/client-core.test.ts.
할 일: 요청 A·B를 시작하고 B 응답, A 취소, A의 늦은 응답,
연결 종료 순서로 진행합니다. 단계마다 pending의 키,
살아 있는 타이머, 호출자가 받은 결과를 표로 기록합니다.
완료 조건: A의 늦은 응답이 이미 취소된 Promise를 바꾸지 않는 이유를
takePending()의 반환값으로 설명합니다.
전송 Promise가 끝나지 않아도 제한 시간이 작동하는 이유까지 설명하면 충분합니다.
읽을 파일: packages/protocol/src/index.ts의
SecureMessageCodec와 packages/webrtc/src/serial-send.ts.
할 일: 120 KiB 메시지를 48 KiB 기준으로 나눕니다. 각 조각의 본문 크기, 17바이트 헤더를 더한 크기, 마지막 조각의 크기를 계산합니다. 이어 3 MiB 메시지 두 건이 송신 중이거나 대기 중일 때 3 MiB 메시지를 추가하면 어떤 검사가 실패하는지 찾습니다.
완료 조건: 본문은 48·48·24 KiB의 세 조각입니다. 조각 1보다 2가 먼저 도착했을 때 왜 거절하는지, 4 MiB 재조립 상한과 8 MiB 송신 대기열 상한이 왜 서로 다른지도 적습니다.
읽을 파일: packages/webrtc/src/adaptive-transport.ts,
apps/server/src/gateway.ts, packages/plugin/src/connection-controller.ts,
packages/plugin/src/harness-remote-bridge.ts.
할 일: “Server가 Relay만 지원”, “WebRTC 협상 실패”, “활성 WebRTC 종료”, “상대 공개키 불일치”, “Remote 자신의 설정 수정”의 다섯 상황을 그립니다. 각 상황을 경로 선택, 재연결, 신원 거절, 업무 권한 거절 중 어디에서 처리하는지 표시합니다.
완료 조건: 같은 계정이라도 키가 다르면 연결이 거절되고, 올바른 키라도 금지된 API는 호출할 수 없음을 함수명으로 설명합니다. 자체 호스팅 Relay만으로 P2P 실험 결과를 주장할 수 없는 이유도 포함합니다.
“연결이 안전하다” 대신 “어느 함수가 어느 입력을 검사하고, 실패하면 어떤 자원을 정리하는가”를 적으세요. 한 장의 상태 전이 표와 정확한 파일·함수 이름이면, 긴 기능 목록보다 훨씬 좋은 학습 결과가 됩니다.
주차는 권장 순서이며, 정해진 학습 시간이나 숙련 속도를 보장하는 계획은 아닙니다. JavaScript의 함수·배열·객체를 읽을 수 있다면 1주차부터, Promise와 타입을 이미 이해한다면 2주차부터 시작할 수 있습니다.
| 주차 | 집중할 개념 | 읽을 소스 | 남길 학습 결과 |
|---|---|---|---|
| 1주차 | TypeScript 인터페이스·유니언·제네릭, Promise·Map·Set | packages/webrtc/src/transport.ts, packages/client-core/src/index.ts | 요청 A·B와 응답의 연결 표, 생소한 문법 10개의 설명 |
| 2주차 | JSON 계약·런타임 검증·버전 협상 | packages/protocol/src/index.ts의 Control 스키마·인코더 | 잘못된 입력 5개와 각각 실패하는 스키마 조건 |
| 3주차 | 키 고정·Noise IK·인증 암호화·문맥 결합 | packages/crypto/src/noise.ts, packages/plugin/src/client-secure-transport.ts | 공개키·개인키·연결 키를 구별한 두 메시지 핸드셰이크 그림 |
| 4주차 | 제어면·데이터면·ICE·Relay·역압력 | packages/webrtc/src/adaptive-transport.ts, packages/webrtc/src/serial-send.ts, 서버 apps/server/src/gateway.ts | 협상 실패와 활성 경로 장애를 나눈 상태 전이도 |
| 5주차 | 모바일 비동기 상태·보관소·로그인 세대 | apps/android/src/state/account-actions.ts, apps/android/src/services/login/, apps/android/src/services/storage.ts | 서버 전환 중 늦은 응답의 처리 시간표 |
| 6주차 | Host 권한·프로세스·자체 호스팅·배포 계약 | packages/plugin/src/harness-remote-bridge.ts, packages/plugin/src/acp/method-policy.ts, apps/server/Dockerfile·.github/workflows/ci.yml | 허용 API와 거절 API 표, 소스·설계·검증 기록의 차이 목록 |
학습 순서는 화면의 화려함보다, 데이터의 형태와 요청의 수명을 먼저 이해하는 쪽으로 잡는 것이 좋습니다. 화면부터 읽으면 접속·승인·재연결 상태가 한꺼번에 나타납니다. 바닥의 인터페이스와 정리 규칙을 알고 돌아오면 같은 UI 코드가 훨씬 단순해집니다.
암호 알고리즘을 새로 설계하거나 운영 서비스를 재현하는 일은 이 로드맵의 선행 조건이 아닙니다. 먼저 이미 있는 코드에서 실패 경계와 자원 상한을 읽을 수 있는 수준을 목표로 삼습니다.
| 용어 | 쉬운 설명 |
|---|---|
| Harness | 모델과 도구, 대화, 작업공간을 연결하는 실행 환경입니다. 여기서는 DeepSeek Harness 제품을 가리킵니다. |
| Host / Client | 실제 작업이 있는 기기 쪽과, 그 작업에 접속하는 기기 쪽입니다. |
| 플러그인 | 기존 프로그램의 확장 지점에 붙는 코드입니다. 이 프로젝트의 Host는 Harness를 통째로 대체하지 않고 확장합니다. |
| RPC | 다른 프로세스·기기에 함수 호출처럼 요청을 보내고 결과를 받는 방식입니다. 네트워크 실패를 일반 함수보다 더 신경 써야 합니다. |
| 이벤트 / 스트림 | 응답 하나로 끝나지 않고 상태 변화나 생성 결과가 계속 도착하는 흐름입니다. |
| 요청 상관관계 | 응답의 requestId로 어떤 요청에 대한 답인지 찾는 일입니다. 응답 순서에 의존하지 않게 합니다. |
| Control plane | 로그인, 기기 선택, 연결 ID와 경로 협상처럼 통신을 준비하고 관리하는 제어면입니다. |
| Data plane | 프롬프트·응답·파일 등의 실제 업무 데이터를 운반하는 데이터면입니다. |
| WebSocket | 서버와 연결을 유지하며 양방향 메시지를 주고받는 통신 방식입니다. 이 프로젝트의 제어와 Relay에 쓰입니다. |
| WebRTC DataChannel | WebRTC로 바이트 데이터를 보내는 통로입니다. 영상 통화만을 위한 기능은 아닙니다. |
| P2P / LAN | 기기 간 통신 / 같은 로컬 네트워크를 뜻합니다. 이 프로젝트는 LAN과 인터넷 직접 경로를 구별해 표시합니다. |
| NAT / ICE | 공유기의 주소 변환 / 실제로 연결 가능한 후보를 찾아 시험하는 절차입니다. |
| STUN / TURN | 외부에서 보이는 주소를 찾는 데 도움을 주는 서버 / 직접 연결 대신 데이터를 중계하는 서버입니다. |
| Relay | 중간 서버가 데이터를 전달하는 방식입니다. 이 레포의 WebSocket Relay와 WebRTC의 TURN 경로는 구별됩니다. |
| E2EE | 양 끝 기기에서 업무 내용을 암호화·복호화합니다. 중간 경로가 내용을 읽지 못해도 연결 시간과 크기는 보일 수 있습니다. |
| Noise IK | 미리 알고 있는 상대 정적 공개키를 사용하는 Noise 핸드셰이크 패턴입니다. 이 구현은 고정된 암호 모음만 허용합니다. |
| X25519 | 상대와 공유 비밀을 만드는 데 사용하는 키 교환 방식입니다. 공개키와 개인키를 구분해야 합니다. |
| ChaCha20-Poly1305 | 내용을 숨기는 암호화와 변조 검사를 함께 제공하는 인증 암호화 방식입니다. |
| HKDF / SHA-256 | 비밀에서 용도별 키를 유도하는 방법 / 데이터를 정해진 길이의 해시로 바꾸는 함수입니다. |
| Identity pinning | 신뢰한 기기의 공개키를 기억해, 같은 기기 ID가 다른 키로 바뀌면 거절하는 방식입니다. |
| Prologue | 핸드셰이크에 묶는 공통 문맥입니다. 여기서는 연결 ID와 두 기기의 ID를 포함합니다. |
| Nonce / 재전송 공격 | 암호화에서 반복 사용을 막아야 하는 값 / 이전 메시지를 다시 보내 동작을 유도하려는 시도입니다. |
| Capability | 상대가 제공한다고 알리는 기능 표지입니다. 구현 지원과 실제 백엔드 준비 상태까지 항상 같은 뜻은 아닙니다. |
| Allowlist | 허용한 API만 통과시키는 목록입니다. 로그인한 상대도 이 목록 밖의 기능은 쓸 수 없습니다. |
| Fail closed | 신원·형식·순서가 불확실하면 계속 허용하지 않고 거절하거나 연결을 닫는 원칙입니다. |
| Backpressure | 받는 쪽이 느릴 때 보내는 쪽도 기다리게 하여 대기열이 무한히 커지지 않게 하는 역압력입니다. |
| AbortSignal | 진행 중인 비동기 처리를 그만 기다리거나 취소하도록 알리는 신호입니다. 실제 작업 취소 여부는 구현에 달렸습니다. |
| ACP / MCP | 에이전트와 클라이언트의 통신 / 모델·에이전트에 도구와 문맥을 연결하는 프로토콜 계열입니다. 이 레포의 ACP 데이터면을 MCP 서버로 부르면 부정확합니다. |
| PTY | 셸이 실제 터미널에 연결된 것처럼 동작하도록 하는 가상 터미널 장치입니다. |
| Golden vector | 같은 입력에 같은 결과가 나오는지 비교할 수 있도록 고정한 기준 데이터입니다. |
| 모노레포 / workspace | 여러 앱과 라이브러리를 한 저장소에서 관리하고 서로 참조하게 하는 구성입니다. |
| Shallow clone | 일부 최신 이력만 가져온 복제본입니다. 전체 커밋 수나 과거 기여도 분석에는 충분하지 않습니다. |
저장소 메타데이터는 2026-10-23 GitHub 기준입니다.
코드·설정·줄 수의 기준은 커밋 fac5e60입니다.
릴리스 검증 결과의 출처는 아래에 명시한 릴리스 기록입니다.
| 항목 | 확인한 상태 |
|---|---|
| 저장소 / 선정 | liguobao/ds-harness-remote · TrendShift Daily #16 |
| 스타 / 포크 | 699 / 70 — 2026-10-23 GitHub 기준 |
| 생성일 / 최근 push | 2026-08-14 / 2026-10-23 — 2026-10-23 GitHub 기준 |
| 주 언어 | TypeScript — GitHub 메타와 실제 코드 구성 일치 |
| 최신 릴리스 | v0.5.18 · 2026-10-23 — 2026-10-23 GitHub 기준. 패키지 버전과 변경 기록도 0.5.18 |
| 최근 커밋 | fac5e60 · 2026-10-23 13:43:47 +08:00 · chore(release): prepare v0.5.18 |
| 코드 규모 | TypeScript 90,647줄 + TSX 11,001줄. 테스트 포함, 배포 번들 48,155줄 제외. 추적 파일은 전체 644개. |
| 라이선스 메타 | 없음 — 2026-10-23 GitHub 기준. 루트 LICENSE 파일 없음. |
| 실제 라이선스 표기 | 루트·Plugin package.json에 MIT 표기. packages/plugin/LICENSE와 apps/vscode/LICENSE에 MIT 본문 존재. |
| 자체 호스팅 범위 | 단일 계정·JSON 보관·Relay·로그인/기기 상태 Web UI. 전체 Remote Web·WebRTC/TURN은 이 서버에 없음. |
| 릴리스 검증 기록 | .github/release-notes.md 기준 전체 794개 테스트와 빌드 통과. v0.5.18의 신규 실기기·기기 간 검증은 없다고 명시. |
| 수치의 의미 | 코드 규모는 추적 파일의 정적 통계입니다. 테스트 수와 코드 상한은 운영 성능이나 기기 간 호환성의 보장 수치가 아닙니다. |
라이선스: GitHub 메타의 “없음”과 일부 패키지의 MIT 파일이 공존합니다. 저장소 전체의 배포·재사용 조건을 이 글만으로 단정하지 말고, 사용할 경로와 배포물의 실제 조건을 확인해야 합니다.
보안: E2EE는 중계 서버에서 업무 내용을 숨기지만, 연결 시간·기기·전송 크기는 숨기지 않습니다. 암호화 문서는 장기 연결 rekey와 독립 보안 검토를 남은 과제로 적습니다. Host나 Client가 침해되거나 전달받은 Web 코드가 바뀌는 문제까지 암호화만으로 해결되지는 않습니다.
지원 범위: README는 VS Code를 실험 단계, iOS를 공개 배포되지 않은 TestFlight 베타로 설명합니다.
설정에 특정 백엔드 명령이 있어도 설치·실행 준비가 완료됐다는 뜻은 아닙니다.
현재 packages/plugin/src/config.ts에는 Codex·Cursor·Antigravity·Pi·Claude·OpenCode가 등록되며,
오래된 설계 기록의 기본값보다 현재 소스와 capability 응답을 우선 확인해야 합니다.
문서와 라우팅: 자체 호스팅 README는 로그인 후 /app/remote 이동을 안내하고,
Web 소스도 그 주소로 화면을 전환합니다. 그러나 apps/server/src/server.ts가 HTML을 반환하는 경로 목록에는
/app/remote가 빠져 있습니다. 따라서 이 서버에 해당 주소를 직접 요청하는 경우 404가 반환되는 코드 경로로 이어집니다.
이력과 성능: 특정 시점의 스타·포크 수만으로 전체 기여도나 성장 속도를 판단할 수는 없습니다. README의 “낮은 지연 시간”과 “안전”이라는 설명도 정량 벤치마크나 독립 보안 감사의 완료를 뜻하지는 않습니다. 지원 설명과 실제 구현의 제한을 함께 살펴봐야 합니다.