스폰서LobeHubLobeHub자세히 알아보기
dshfind

5강: 설정과 배포: 설정 가능, 배포 가능

한 문장 요약: 플러그인이 나 혼자 쓰기에만 잘 동작하는 것으로는 끝이 아닙니다. 「배포마다 달라질 수 있는」 매개변수를 모두 설정 가능한 스키마로 선언하고, 플러그인을 설치 가능한 번들로 패키징하여 배포하면, 다른 사람들이 명령어 하나로 설치하고 설정에서 필요에 따라 조정할 수 있습니다. 이 강의에서 「설정 가능, 배포 가능」을 다루고 나면, 여러분의 플러그인은 마침내 「졸업」입니다.


1. 사용자 스토리: 「혼자 쓰기」에서 「쓸 수 있고, 바꿀 수 있게」로

D 군은 앞의 세 강에서 배운 실력으로 「리포지토리 요약」 플러그인을 만들었습니다. 리포지토리 경로를 주면 agent가 자동으로 README를 읽고, 코드 양을 집계하고, 요약을 생성합니다. 자기 컴퓨터에서는 아주 잘 돌아갔습니다.

금요일 오후, 동료 H 군이 다가왔습니다. 「이 요약 플러그인 정말 유용한데, 나한테도 설치해 줘!」

D 군은 곧바로 세 가지 문제를 발견했습니다.

  1. H 군은 사용하는 모델도, 타임아웃 시간도 다릅니다. 그런데 TIMEOUT = 30000소스 코드에 하드코딩되어 있어서 바꿀 수가 없습니다.
  2. 소스 폴더 전체를 통째로 복사해 주고, 이후 버그를 고칠 때마다 수동으로 다시 동기화할 수는 없습니다.
  3. H 군은 직접 매개변수를 조정하고 싶지만, 핵심 로직은 절대 망가뜨리면 안 됩니다.

전통적인 프레임워크의 답은 「소스 복사 + 코드 수정」입니다. 포크해서, 매개변수를 하드코딩하고, 각자 알아서 고치며, 업그레이드할 때는 고통스러운 병합을 겪습니다. DSH의 답은 두 단어입니다: 설정 가능배포 가능.

  • 설정 가능: 「배포마다 다른 값이 필요할 수 있는」 매개변수를 모두 설정 필드로 선언하고, 사용자가 설정에서 값을 전달합니다. H 군이 타임아웃을 바꾸고 싶으면 코드를 건드리지 않고 설정만 바꾸면 됩니다(2절).
  • 배포 가능: 플러그인을 표준 번들로 패키징하여 배포하면, 다른 사람들이 명령어 하나로 설치하고 설정에서 조립할 수 있습니다(3, 4절).

🎁 비유: 이전 강의에서 여러분이 만든 것은 「쓰기 좋은 드라이버 한 자루」였습니다. 이번 강에서 만들 것은 「그 드라이버를 표준 공구함에 넣을 수 있고, 사 간 사람이 손잡이를 교체할 수 있게 하는 것」입니다. 설정이 손잡이이고, 배포가 포장입니다.


2. 플러그인에 설정 추가하기: 스키마, 기본값, 조립

Config 타입을 정의하고, 기본값은 스키마에 작성

Cordis의 규약은 이렇습니다. 플러그인에서 Config 타입을 내보내고, 같은 이름의 Schemastery 스키마도 함께 제공합니다. 기본값은 스키마에 직접 작성합니다(출처: docs/user/develop/basic/config.zh.md):

import type { Context } from 'cordis'
import Schema from 'schemastery'

export const name = 'my-plugin'

export interface Config {
  greeting: string
  maxRetries: number
  verbose?: boolean
}

export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  maxRetries: Schema.number().default(3),
  verbose: Schema.boolean().default(false),
})

export function apply(ctx: Context, config: Config) {
  console.log(config.greeting)  // User value or schema default.
}

부분별로 살펴 봅시다.

  • export interface Config: 플러그인에 필요한 설정이 어떤 모양인지 선언합니다. TypeScript 타입이라 코드를 작성할 때 자동 완성과 힌트를 받을 수 있습니다.
  • export const Config = Schema.object({ ... }): 같은 이름의 스키마로, 각 필드의 타입을 기술하면서 기본값(.default(...))도 함께 제공합니다.
  • apply(ctx, config): 두 번째 매개변수가 조립된 설정입니다. 사용자가 값을 넘겼으면 그 값을, 넘기지 않았으면 스키마의 기본값을 사용합니다.

⚠️ 일반 객체를 Config로 내보내지 마세요. Cordis가 요구하는 Standard Schema 인터페이스를 만족하지 않아 플러그인이 검증할 수 없습니다. 타입과 스키마가 같은 이름을 쓰는 것은 Cordis의 규약이니, 두 개의 이름으로 나누지 마세요.

조립 시 config 전달

설정은 어디에 쓸까요? 그 오랜 친구 cordis.yml입니다. 플러그인 항목에 config 키를 추가합니다(출처: docs/user/develop/basic/config.zh.md):

- insert:
    - id: hello
      name: './src/my-plugin.ts'
      config:
        greeting: 'Hi there'
        maxRetries: 5

플러그인이 로드될 때 Cordis는 내보낸 스키마로 이 설정을 검증하고, 제공되지 않은 필드의 기본값을 채웁니다. 여기서는 verbose가 없으므로 false가 됩니다. 검증에 실패하면 어떻게 될까요? 「설정 오류는 명확해야 합니다」. 스키마는 플러그인 로드 시점에 검증을 수행하고, 설정이 유효하지 않으면 플러그인이 로드에 실패하며 명확한 오류 메시지를 제공합니다. 잘못된 설정을 달고 계속 실행되지 않습니다.

config 변경 → 증분 리로드

사용자가 설정을 수정한 다음에는요? 프로그램 전체를 재시작할 필요가 없습니다. 문서 원문입니다(출처: docs/user/develop/basic/config.zh.md 「HMR과 함께」):

설정 변경은 플러그인 핫 스왑을 트리거합니다. cordis.yml에서 어떤 플러그인의 config를 수정하면 프레임워크는 이전 인스턴스를 언로드하고 새 인스턴스를 로드합니다. 등록은 모두 effect이며 자동으로 정리되므로, 교체 후 이전 인스턴스의 등록이 남지 않습니다.

이것이 바로 2장 논문의 「시간적 합성 가능성」이 펼쳐지는 현장입니다. 이전 인스턴스 언로드 = 그 이펙트를 모두 롤백, 새 인스턴스 로드 = 재등록. 변경된 플러그인만 이 재조립을 거치고, 다른 플러그인은 전혀 영향을 받지 않습니다. 이것이 그림에 적힌 「증분 리로드」입니다. 필드 단위로 조율하고, 움직여야 할 것만 움직입니다.

두 가지 설계 원칙

공식 문서에서(출처: docs/user/develop/basic/config.zh.md 「설계 원칙」):

  • 하드코딩된 조정 가능 매개변수 금지: 배포마다 다른 값이 필요할 수 있는 매개변수는 반드시 설정 필드로 정의해야 합니다. 검증 기준은 단 한 문장입니다. 코드를 수정하지 않고 cordis.yml에서 이 값을 바꿀 수 있는가? 안 되면 설정으로 끌어올리세요.
  • 설정 오류는 명확하게: 스키마에 자기완결적인 제약을 표현하여, 유효하지 않은 설정이 플러그인 로드 시점에 실패하도록 합니다. 잘못된 값으로 조용히 실행되는 일이 없도록 말입니다.

3. 배포: 플러그인을 설치 가능한 「번들」로 만들기

플러그인이 설정 가능해졌습니다. 다른 사람이 설치하게 하려면 어떻게 해야 할까요? 먼저 두 가지 개념을 구분합시다(출처: docs/user/develop/basic/publish.zh.md):

  • 번들(bundle): 설정 레이어를 동반하는 npm 패키지입니다. 매니페스트는 dsh.bundle을 선언하며, 「이 패키지가 무엇을 제공하는가?」에 답합니다. 플러그인 행을 삽입하거나 덮어쓰는 patch 파일 하나입니다.
  • profile: $DSH_HOME/profiles/name 아래에 있는, 시작 가능한 조합을 기술하는 디렉터리입니다. 매니페스트는 dsh.profile을 선언하며, 「이 구성은 어떤 번들들이 어떤 순서로 이루어졌는가?」에 답합니다.

한 문장으로 기억하세요. 번들은 여러분이 작성하고 배포하는 것이고, profile은 사용자가 시작하는 것입니다. 둘을 겸하는 것은 없습니다.

패키지 구조: 세 가지 세트

번들은 보통 이런 모습입니다(출처: docs/user/develop/basic/publish.zh.md):

hello-plugin/
├── package.json       # declares dsh.bundle
├── cordis.patch.yml   # the layer applied when a profile lists this bundle
└── index.js           # plugin modules the patch rows reference

package.jsondsh.bundle로 자신이 번들임을 선언합니다.

{
  "name": "dsh-hello-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "index.js",
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}

patch 파일의 형태는 앞에서 작성한 --patch overlay와 같습니다. patch 항목의 YAML 배열입니다. 다만 플러그인 행이 상대 소스 경로가 아닌 패키지 이름으로 이 패키지를 참조한다는 점이 다릅니다. 그래야 Node의 모듈 해석이 설치된 코드를 찾을 수 있습니다(출처: docs/user/develop/basic/publish.zh.md):

- insert:
    - id: hello
      name: dsh-hello-plugin

빌드와 세 가지 배포 경로

배포 전에 빌드합니다. build 스크립트(예: tsdown)가 TypeScript를 lib/ 산출물로 컴파일합니다. 배포 경로는 세 가지입니다(출처: docs/user/develop/basic/publish.zh.md):

경로명령사용자가 받는 것
npm에 배포pnpm publish(배포 시 lib/를 빌드해 둠)사전 빌드된 코드. dsh plugin add your-package로 바로 설치
tarball 전달pnpm packhello-plugin-0.1.0.tgz 파일 하나
GitHub에서 직접 설치git 리포지토리에 푸시소스 코드. 아래의 함정 참조

⚠️ git 설치라는 관문: git 설치가 가져오는 것은 빌드 산출물이 아니라 소스 코드입니다. 어떤 단계에서도 여러분의 build 스크립트를 실행하지 않으므로, TypeScript 패키지는 lib/ 출력 없이 도착하여 로드에 실패합니다. 그래서 양쪽이 각각 한 가지씩 해야 할 일이 있습니다(출처: docs/user/develop/basic/publish.zh.md):

  • 작성자: prepare 스크립트를 제공합니다. pnpm은 git 설치 후 이를 실행해 소스에서 배포 엔트리를 빌드하며, 반드시 자기완결적이어야 합니다(개발 환경에만 존재하는 컨텍스트, 예를 들어 옆에 monorepo 체크아웃이 있다는 가정을 해서는 안 됩니다).
  • 사용자: 빌드를 승인합니다. pnpm ≥ 10은 명시적으로 허용되기 전까지 git 의존성의 prepare 스크립트 실행을 거부하므로 첫 add는 실패합니다. dsh가 해결 방법을 알려 줍니다. pnpm이 출력한 정확한 패키지 키를 해당 profile의 pnpm-workspace.yaml에 복사하면 됩니다:
allowBuilds:
  dsh-hello-plugin: true

이 승인을 있는 그대로 받아들이세요. 이것은 해당 패키지의 코드가 설치 시점에 여러분의 컴퓨터에서 실행되는 것을 허용하는 것이며, agent가 실행되는 어떤 샌드박스 안에도 있지 않습니다. 소스를 신뢰할 수 있는 패키지에만 승인하고, 커밋을 고정하세요(github:you/hello-plugin#sha). 그래야 이후의 푸시가 실제로 실행되는 내용을 몰래 바꿀 수 없습니다. 사용자에게 이 승인을 시키고 싶지 않다고요? 그럼 빌드 산출물을 배포하세요. npm도 tarball도 어떤 빌드 권한도 필요하지 않습니다.

버전 관리: 배포 가능성의 기반

package.jsonversion유의적 버전(SemVer)입니다. 0.1.0 = 메이저.마이너.패치. 업그레이드는 규칙을 지켜야 합니다. 호환성을 깨는 변경은 메이저를, 기능 추가는 마이너를, 버그 수정은 패치를 올립니다. 왜 이렇게 중요할까요? 버전이 「발견과 호환성」의 초석이기 때문입니다. 4절에서 곧 다룹니다. 누군가 0.1.0을 설치한 후에 여러분이 인터페이스를 몰래 바꾸면, 결과는 인터페이스 드리프트입니다.

① 写包src + 配置② 构建构建产物③ 发布npm 等注册表④ 安装dsh plugin add配置:插件可按字段协调(config 变更 → 增量重载)

发布即分发:别人一条命令装上你的插件,配置变化自动协调

배포가 곧 배포 완료입니다. 다른 사람이 명령어 하나로 여러분의 플러그인을 설치하고, 설정 변경은 자동으로 조율됩니다.


4. 다른 사람이 사용하는 방법: 설치, 조립, 필요에 따른 설정, 그리고 명명과 발견

명령어 하나로 profile에 설치

상대방이 여러분의 패키지(또는 체크아웃)를 받아 자기 컴퓨터에서 다음을 실행합니다(출처: docs/user/develop/basic/publish.zh.md):

cd hello-plugin
dsh plugin --profile demo add .

이 명령을 분해해 봅시다.

  • dsh plugin --profile demo add .은 profile 디렉터리 안에서 pnpm으로 전달되므로, pnpm의 모든 서브커맨드를 사용할 수 있습니다.
  • 처음 사용할 때는 profile을 초기화합니다. @deepseek-ai/dsh-base가 첫 번째 번들이 됩니다.
  • 여러분의 패키지가 dsh.bundle을 선언했기 때문에, dsh는 이를 dsh.profile.bundles에 추가합니다:
{
  "name": "dsh-profile-demo",
  "private": true,
  "dependencies": {
    "dsh-hello-plugin": "link:/path/to/hello-plugin"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "dsh-hello-plugin"
      ]
    }
  }
}

시작하지 않고 이 레이어만 검증한 다음, 시작합니다.

dsh --profile demo --dump-config   # shows a "# == dsh-hello-plugin" layer
dsh --profile demo

제거하고 싶다고요? dsh plugin --profile demo remove dsh-hello-plugin이 의존성과 해당 레이어를 함께 제거합니다.

조립과 필요에 따른 설정: 뒤 레이어가 앞 레이어를 덮어씀

유효한 설정은 빈 루트 위에서 순서대로 레이어를 쌓아 구성됩니다(출처: docs/user/develop/basic/publish.zh.md 「로드 순서」):

  1. profile의 dsh.profile.bundles 목록에 나엘된 각 번들의 patch. 목록 순서대로.
  2. profile 자신의 cordis.patch.yml.
  3. home 레벨의 $DSH_HOME/cordis.patch.yml(여러 profile이 공유하는 머신 로컬 선호 설정).
  4. --patch overlay. argv 순서대로.
  5. 런처 플래그 patch(예: dsh web --port).

나중에 적용된 레이어가 행 단위로 이깁니다. 여기서 번들 작성자에게 두 가지 귀결이 나옵니다.

  • 여러분의 patch는 id로 앞 레이어들의 행을 덮어쓸 수 있지만, patch는 대상 행의 config 값 전체를 교체할 뿐 각 키를 깊게 병합하지 않습니다. 덮어쓸 때는 바꾸려는 키 하나만 쓰지 말고, 그 행에 필요한 모든 키를 다시 명시해야 합니다.
  • 사용자는 자기 profile의 cordis.patch.yml에서 여러분의 패키지를 건드리지 않고 여러분의 행을 덮어쓸 수 있습니다. 그러니 배포할 때는 「사용자가 그대로 유지할 가능성이 높은 설정 기본값을 먼저 제공하고, 나머지는 스키마에 맡기세요」.

다시 말해, 여러분은 쓰기 좋은 기본값을 스키마에 적어 두고, 선택권은 사용자의 설정 레이어에 넘기는 것입니다. 이것이 「설정 가능, 배포 가능」이 합쳐진 모습입니다.

명명과 발견: 버전 호환성과 인터페이스 드리프트(논문 5.5와 호응)

플러그인이 발견되고, 설치되고, 오래 쓰이려면 배포만으로는 부족합니다. 「발견」이라는 관문도 통과해야 합니다. 2장 13강에서 다룬 논문 5장을 기억하나요? 그중 5.5절이 두 가지 함정을 특별히 경고합니다.

문제무엇인가결과
인터페이스 드리프트제공자가 개정하면서 키 k와 연관된 인터페이스를 바꿨지만(필드 추가, 메서드 시그니처 변경, 동작 계약 변경), 이전 인터페이스에 맞춰 컴파일된 소비자는 여전히 같은 키 k를 선언함잔여 이펙트 수준에서는 의존성이 「충족」되지만, 런타임 값이 기대와 맞지 않음: 타입 오류, 메서드를 찾을 수 없음, 조용한 동작 이탈
키 충돌독립적으로 개발된 두 제공자가 전혀 무관한 인터페이스에 같은 키 이름 k를 사용함소비자가 호환성 검사 없이 다른 제공자의 값을 받아들여, 장애가 예측 불가능하고 진단도 어려움

논문은 세 가지 보완 방법을 제시합니다. 키 네임스페이스화(키 정체성에 인터페이스를 정의한 패키지 식별자를 포함시켜 구조적으로 키 충돌을 제거), 피어 의존성(Cordis가 현재 채택. 호스트 언어의 패키지 매니저로 버전 제약을 선언하여 버전 비호환을 설치 시점에 발견하고 런타임 장애로 끌고 가지 않음. 대가는 제공자가 유의적 버전 규약을 자발적으로 지키는 데 의존하며 강제할 수 없다는 점), 구조적 호환성(인터페이스 구조가 소비자의 기대를 포함하는지로 판단하지만 동작 계약은 복잡함). 여러분의 일상에 적용하면:

  • 패키지 이름은 고유하게: npm에 배포할 때 네임스페이스를 잘 활용하세요(플랫폼 관례는 @deepseek-ai/dsh-* 같은 접두사).
  • 버전 규칙을 지키기: 유의적 버전 규약을 지키고, 인터페이스를 몰래 바꾸지 마세요. 호환성을 깨는 변경은 메이저를 올리고 변경 기록을 작성하세요.
  • 새 기능에는 새 키를: 서비스와 도구의 등록 키를 정할 때 기존 플러그인의 키 이름을 피해, 「인터페이스 드리프트」를 배포 전에 차단하세요.

핵심 요점 복습

  1. 설정 가능: 플러그인은 Config 타입 + 같은 이름의 스키마를 내보내고, 기본값은 스키마에 작성합니다. 사용자는 cordis.ymlconfig 키로 값을 전달하고, Cordis가 검증하여 기본값을 채웁니다. 「코드를 바꾸지 않고 설정에서 이 값을 바꿀 수 있는가」가 하드코딩의 검증 기준입니다.
  2. 증분 리로드: config 변경은 플러그인 핫 스왑을 트리거합니다. 이전 인스턴스를 언로드하고 새 인스턴스를 로드하며, 등록은 effect라 자동으로 정리됩니다. 논문의 「시간적 합성 가능성」과 호응합니다.
  3. 번들과 profile: 번들(dsh.bundle)은 작성자가 배포하는 것이고, profile(dsh.profile)은 사용자가 시작하는 조합입니다. 배포 경로는 npm, tarball, GitHub 세 가지이며, git으로 소스를 설치하려면 prepare 스크립트 + allowBuilds 승인이 필요합니다.
  4. 설치와 조립: dsh plugin --profile demo add .로 profile에 설치합니다. 유효한 설정은 레이어별로 구성되고 뒤 레이어가 앞 레이어를 덮어씁니다. patch는 깊은 병합이 아니라 config를 행 전체로 교체합니다.
  5. 명명과 발견: 패키지 이름 고유성, 유의적 버전, 인터페이스 드리프트와 키 충돌 회피. 이것이 논문 5.5절이 배포자에게 걷는 세 장의 「벌금 면제 카드」입니다.

🚀 다음 강의 「실전 심화: LLM 어댑터와 자기 참조 도구」에서는 실제 세계의 플러그인이 어떤 모습인지 살펴습니다. 에이전트에 새로운 모델 제공자를 연결하는 방법, 그리고 「자기 자신을 호출할 수 있는」 플러그인이 어떤 경험인지 알아봅니다.

자가 테스트 · 설정과 배포

답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.

1. 플러그인에 설정을 추가하는 올바른 방법은? (docs/user/develop/basic/config.zh.md 근거)
2. 사용자가 cordis.yml에서 어떤 플러그인의 config를 수정하면 어떻게 되나요?
3. 플러그인 배포에 관해 올바른 설명은?
4. 동료가 여러분이 배포한 플러그인을 사용하려 합니다. 올바른 절차는?