본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 27. 14:40

【#4】 OpenClaw 해독하기 — 코어에 능력을 주입하는 단 하나의 창구

요약

OpenClaw의 플러그인 아키텍처를 분석하여 코어와 플러그인 간의 의존성을 분리하는 메커니즘을 설명합니다. 매니페스트와 로더를 통해 코드를 실행하지 않고도 플러그인의 능력을 파악하는 제어 평면(control plane)과 실행 평면(runtime plane)의 분리 원칙을 다룹니다.

핵심 포인트

  • 매니페스트를 통한 플러그인 능력의 정적 선언 및 검증
  • 제어 평면과 실행 평면의 엄격한 분리로 시스템 효율성 증대
  • 코드 실행 없이 메타데이터만으로 플러그인 정보 파악 가능
  • 플러그인 SDK와 레지스트리를 통한 확장성 확보

본 기사의 코드 참조는 OpenClaw maincee2aca409 (version 2026.6.10) 시점입니다. 행 번호는 업데이트에 따라 어긋날 수 있습니다.

연재 「OpenClaw 해독하기」

#01에서 「코어는 플러그인 비의존적(plugin-agnostic)」이라는 최중요 원칙을 살펴보았습니다. 이번에는 그것을 실제로 성립시키고 있는 메커니즘——매니페스트(manifest), 로더(loader), capability 레지스트리(registry), 그리고 300개가 넘는 서브 패스 엔트리 포인트(sub-path entry point)를 가진 플러그인 SDK——를 src/plugins/src/plugin-sdk/를 통해 해독합니다.

각 네이티브 플러그인의 루트에는 openclaw.plugin.json (PLUGIN_MANIFEST_FILENAME, src/plugins/manifest.ts:26)이 필수입니다. 그 내용을 나타내는 타입 PluginManifest (src/plugins/manifest.ts:297)가 플러그인이 무엇을 제공하는지를 선언합니다.

export type PluginManifest = {
id: string;
configSchema: JsonSchemaObject;
...

특히 효과적인 것이 contracts (manifest.ts:406)입니다.

export type PluginManifestContracts = {
embeddingProviders?: string[];
speechProviders?: string[];
...

이는 「이 플러그인은 speechProviders로서 cloud-tts를 가진다」와 같은 **능력의 정적 목록(static catalog)**입니다. 코어는 이 목록을 읽는 것만으로 「누가 무엇을 제공할 수 있는지」를, 플러그인의 코드를 실행하지 않고도 파악할 수 있습니다.

src/plugins/AGENTS.md가 핵심으로 삼는 것은 control plane과 runtime plane의 분리입니다.

Keep control-plane and runtime-plane concerns separate: discovery, manifest parsing, config validation, setup/onboarding hints, and activation planning belong to the control plane; actual plugin execution belongs to runtime resolution.

로더의 흐름 (공개 엔트리 loadOpenClawPlugins(), src/plugins/loader.ts:1821)은 이 두 측면으로 구성됩니다.

  • Discovery scan (control plane) — 플러그인의 루트를 찾아, openclaw.plugin.jsonpackage.json코드를 실행하지 않고 읽는다.
  • Manifest registry (control plane) — 스키마, 환경 변수 의존성, 인증 메타데이터를 검증한다.
  • Registry assembly (runtime) — 여기서 처음으로 플러그인의 모듈을 로드하고, 등록 훅(registration hook)을 호출하며, capability 레지스트리를 조립한다.

「메타데이터만으로 동작하는 부분」과 「실행이 필요한 부분」을 엄격히 나눔으로써, openclaw --help와 같은 가벼운 조작 시 플러그인 본체를 로드하지 않아도 되도록 함——#02의 fast-path와 정확히 같은 사상입니다.

플러그인의 출처도 타입으로 구분됩니다 (PluginCandidate, src/plugins/discovery.ts:69). origin (PluginOrigin, src/plugins/plugin-origin.types.ts:2)은 'bundled' | 'global' | 'workspace' | 'config', format (PluginFormat, src/plugins/manifest-types.ts:12)은 'openclaw' | 'bundle'입니다. 번들 형식 (PluginBundleFormat, manifest-types.ts:15

'codex' | 'claude' | 'cursor'

)도 식별하며, 코어 배포판에 포함되는 내부 플러그인과 외부 플러그인을 올바르게 구분합니다.

src/plugins/registry.ts:407

createPluginRegistry()가 핵심입니다. 포인트는 레지스트리가 capability(기능) 구동 방식이지 플러그인 구동 방식이 아니라는 점입니다.

export function createPluginRegistry(registryParams: PluginRegistryParams) {
const registry = createEmptyPluginRegistry();
const { registerModelCatalogProvider, registerSpeechProvider, /* ... */ } = 
...

코드에 `

와 같은 일부 서브패스(sub-path)는 번들 플러그인(bundle plugin) 모듈에 대한 에일리어스(alias)이며, 외부 플러그인에서는 import 할 수 없도록 빌드 시점에 가드(guard)됩니다 (supportedBundledFacadeSdkEntrypoints).

*.runtime이라는 명명 규칙(예: time-runtime, text-runtime)은 "비동기 경로(asynchronous path)에서만 필요한 무거운 API"를 나타내며, ./core는 "모든 채널이 기동 시점에 eager import 하는 가벼운 계약(contract)"을 나타냅니다. 이러한 입도(granularity) 설계가 거대한 SDK를 "기동이 느려지지 않는" 상태로 유지합니다.

src/plugin-sdk/AGENTS.mdsrc/plugins/AGENTS.md로부터 플러그인 제작자와 코어 개발자 양측이 준수해야 할 철칙을 추출합니다.

호스트는 내부로 손을 뻗게 하지 않는다: "Host loads plugins; plugins should not reach through the SDK into arbitrary host internals." src/channels/**, src/agents/**, src/plugins/**의 구현 편의성을 의도적인 공개 계약이 아닌 한 SDK에 노출하지 않습니다.

엔트리(entry)는 모듈 로드(module load) 시점에 저렴해야 한다: 기동 핫 패스(hot path)가 무거운 import를 끌어오지 않도록, send/monitor/probe/login/setup용 헬퍼(helper)는 *.runtime 서브패스로 분리합니다.

동일 런타임 측면에서 static과 dynamic import를 섞지 않는다 (#12의 빌드 회차에서 다루는 [INEFFECTIVE_DYNAMIC_IMPORT] 검사와 직결됨).

plugin-owned를 core-owned로 스며들게 하지 않는다: "plugins.entries.<id>.config를 무관한 코어 경로에서 직접 읽는 것"을 금지합니다. 범용 헬퍼, 플러그인 런타임 훅(runtime hook), 매니페스트 메타데이터를 사용합니다.

openclaw.plugin.json (선언)
│ discovery scan ── 코드를 실행하지 않고 읽음 (control plane)
▼
...

플러그인은 openclaw.plugin.json으로 능력(capability)을 선언하며, 코어는 그 목록(contracts)을 코드 실행 전에 읽습니다.

  • 로더(loader)는 control plane(메타데이터) / runtime plane(실행)을 분리하여 지연(laziness)을 유지합니다.
  • 레지스트리(registry)는 **capability 구동 방식의 범용 핸들러(handler)**로, 코어에 플러그인 이름에 따른 분기(branch)가 나타나지 않습니다.
  • SDK의 얇은 서브패스 그룹은 "기동을 가볍게 유지하기 위한 지연 로드 경계(lazy load boundary)"이며, 무질서가 아닌 규율의 산물입니다.

#05는 가장 수가 많은 플러그인 유형인 **채널 계층(channel layer, transport-only)**을 해독합니다. 20개 이상의 메시징 서비스를 "비즈니스 로직을 갖지 않는 순수한 전달 계층"으로서 어떻게 추상화하고 있는지 살펴봅니다. 수신 이벤트의 정규화(normalization), 드래프트 스트리밍(draft streaming), 그리고 "채널이 명령을 추측하게 하지 않는다"라는 설계 원칙을 extensions/telegram을 소재로 따라가 봅니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0