메인 콘텐츠로 건너뛰기

플러그인 내부 구조

이 문서는 심층 아키텍처 참조입니다. 실용적인 가이드는 다음을 참조하세요.
이 페이지에서는 OpenClaw 플러그인 시스템의 내부 아키텍처를 설명합니다.

공개 capability 모델

Capabilities는 OpenClaw 내부의 공개 네이티브 플러그인 모델입니다. 모든 네이티브 OpenClaw 플러그인은 하나 이상의 capability 타입에 대해 등록됩니다.
Capability등록 메서드예시 플러그인
텍스트 추론api.registerProvider(...)openai, anthropic
CLI 추론 백엔드api.registerCliBackend(...)openai, anthropic
음성api.registerSpeechProvider(...)elevenlabs, microsoft
실시간 전사api.registerRealtimeTranscriptionProvider(...)openai
실시간 음성api.registerRealtimeVoiceProvider(...)openai
미디어 이해api.registerMediaUnderstandingProvider(...)openai, google
이미지 생성api.registerImageGenerationProvider(...)openai, google, fal, minimax
비디오 생성api.registerVideoGenerationProvider(...)qwen
웹 가져오기api.registerWebFetchProvider(...)firecrawl
웹 검색api.registerWebSearchProvider(...)google
채널 / 메시징api.registerChannel(...)msteams, matrix
capability를 전혀 등록하지 않지만 hook, tool, 또는 service를 제공하는 플러그인은 레거시 hook-only 플러그인입니다. 이 패턴도 여전히 완전히 지원됩니다.

외부 호환성 입장

capability 모델은 이미 core에 반영되어 있으며 현재 번들/네이티브 플러그인에서 사용되고 있지만, 외부 플러그인 호환성은 여전히 “내보냈으니 고정된 것이다”보다 더 엄격한 기준이 필요합니다. 현재 지침:
  • 기존 외부 플러그인: hook 기반 통합이 계속 동작하도록 유지하며, 이를 호환성 기준선으로 취급합니다
  • 새로운 번들/네이티브 플러그인: vendor별 직접 접근이나 새로운 hook-only 설계보다 명시적인 capability 등록을 우선합니다
  • capability 등록을 도입하는 외부 플러그인: 허용되지만, 문서에서 명시적으로 안정된 계약으로 표시하지 않는 한 capability별 helper surface는 진화 중인 것으로 취급합니다
실용적인 규칙:
  • capability 등록 API가 의도된 방향입니다
  • 레거시 hook은 전환 기간 동안 외부 플러그인에 가장 안전한 무중단 경로로 남아 있습니다
  • export된 helper subpath가 모두 동일한 것은 아닙니다. 우연히 export된 helper가 아니라, 문서화된 좁은 계약을 우선하세요

플러그인 형태

OpenClaw는 실제 등록 동작을 기준으로 로드된 모든 플러그인을 형태로 분류합니다 (정적 메타데이터만 기준으로 하지 않음).
  • plain-capability — 정확히 하나의 capability 타입만 등록합니다 (예: mistral 같은 provider 전용 플러그인)
  • hybrid-capability — 여러 capability 타입을 등록합니다 (예: openai는 텍스트 추론, 음성, 미디어 이해, 이미지 생성을 소유)
  • hook-only — hook만 등록하고, capability, tool, command, service는 등록하지 않습니다
  • non-capability — tool, command, service, route는 등록하지만 capability는 등록하지 않습니다
플러그인의 shape와 capability 구성을 확인하려면 openclaw plugins inspect <id>를 사용하세요. 자세한 내용은 CLI 참조를 참고하세요.

레거시 hook

before_agent_start hook은 hook-only 플러그인을 위한 호환성 경로로 계속 지원됩니다. 실제 레거시 플러그인들이 여전히 이에 의존합니다. 방향성:
  • 계속 동작하게 유지합니다
  • 레거시로 문서화합니다
  • 모델/provider override 작업에는 before_model_resolve를 우선합니다
  • 프롬프트 변경 작업에는 before_prompt_build를 우선합니다
  • 실제 사용량이 줄고 fixture 커버리지가 마이그레이션 안전성을 입증한 뒤에만 제거합니다

호환성 신호

openclaw doctor 또는 openclaw plugins inspect <id>를 실행하면 다음 라벨 중 하나가 표시될 수 있습니다.
Signal의미
config validconfig가 정상적으로 파싱되고 플러그인이 resolve됨
compatibility advisory플러그인이 지원되지만 오래된 패턴(예: hook-only)을 사용함
legacy warning플러그인이 deprecated된 before_agent_start를 사용함
hard errorconfig가 잘못되었거나 플러그인 로드에 실패함
hook-onlybefore_agent_start도 오늘날 플러그인을 깨뜨리지는 않습니다 — hook-only는 advisory이고, before_agent_start는 경고만 발생시킵니다. 이러한 신호는 openclaw status --allopenclaw plugins doctor에도 표시됩니다.

아키텍처 개요

OpenClaw의 플러그인 시스템은 네 개의 계층으로 구성됩니다.
  1. Manifest + discovery OpenClaw는 설정된 경로, workspace 루트, 전역 extension 루트, 번들 extension에서 후보 플러그인을 찾습니다. discovery는 먼저 네이티브 openclaw.plugin.json manifest와 지원되는 bundle manifest를 읽습니다.
  2. 활성화 + 검증 core는 발견된 플러그인이 활성화, 비활성화, 차단, 또는 memory와 같은 독점 슬롯에 선택될지를 결정합니다.
  3. 런타임 로드 네이티브 OpenClaw 플러그인은 jiti를 통해 in-process로 로드되며 중앙 레지스트리에 capability를 등록합니다. 호환 bundle은 런타임 코드를 import하지 않고 레지스트리 레코드로 정규화됩니다.
  4. surface 소비 OpenClaw의 나머지 부분은 레지스트리를 읽어 tool, channel, provider 설정, hook, HTTP route, CLI command, service를 노출합니다.
특히 플러그인 CLI의 경우, 루트 command discovery는 두 단계로 분리됩니다.
  • 파싱 시점 메타데이터는 registerCli(..., { descriptors: [...] })에서 옵니다
  • 실제 플러그인 CLI 모듈은 lazy 상태를 유지하다가 첫 호출 시 등록될 수 있습니다
이렇게 하면 플러그인 소유 CLI 코드를 플러그인 내부에 유지하면서도 OpenClaw가 파싱 전에 루트 command 이름을 예약할 수 있습니다. 중요한 설계 경계:
  • discovery + config validation은 플러그인 코드를 실행하지 않고도 manifest/schema 메타데이터만으로 동작해야 합니다
  • 네이티브 런타임 동작은 플러그인 모듈의 register(api) 경로에서 옵니다
이 분리를 통해 OpenClaw는 전체 런타임이 활성화되기 전에 config를 검증하고, 누락/비활성화된 플러그인을 설명하며, UI/schema 힌트를 구성할 수 있습니다.

채널 플러그인과 공유 message tool

채널 플러그인은 일반적인 채팅 동작을 위해 별도의 send/edit/react tool을 등록할 필요가 없습니다. OpenClaw는 core에 하나의 공유 message tool을 유지하고, 채널 플러그인은 그 뒤의 채널별 discovery 및 실행을 소유합니다. 현재 경계는 다음과 같습니다.
  • core는 공유 message tool 호스트, 프롬프트 연결, session/thread bookkeeping, 실행 dispatch를 소유합니다
  • 채널 플러그인은 scope된 action discovery, capability discovery, 채널별 schema fragment를 소유합니다
  • 채널 플러그인은 provider별 session conversation grammar를 소유합니다. 예를 들어 conversation id가 thread id를 인코딩하는 방식이나 부모 conversation에서 상속하는 방식이 여기에 해당합니다
  • 채널 플러그인은 action adapter를 통해 최종 action을 실행합니다
채널 플러그인의 경우 SDK surface는 ChannelMessageActionAdapter.describeMessageTool(...)입니다. 이 통합 discovery 호출을 통해 플러그인은 보이는 action, capability, schema 기여를 함께 반환할 수 있으므로 이 요소들이 서로 어긋나지 않습니다. core는 런타임 scope를 이 discovery 단계에 전달합니다. 중요한 필드는 다음과 같습니다.
  • accountId
  • currentChannelId
  • currentThreadTs
  • currentMessageId
  • sessionKey
  • sessionId
  • agentId
  • 신뢰된 인바운드 requesterSenderId
이것은 컨텍스트 민감한 플러그인에 중요합니다. 채널은 활성 account, 현재 room/thread/message, 또는 신뢰된 requester identity에 따라 message action을 숨기거나 노출할 수 있으며, 이를 위해 core message tool에 채널별 분기를 하드코딩할 필요가 없습니다. 이것이 embedded-runner 라우팅 변경이 여전히 플러그인 작업인 이유입니다. 러너는 공유 message tool이 현재 turn에 맞는 채널 소유 surface를 노출하도록 현재 chat/session identity를 플러그인 discovery 경계로 전달할 책임이 있습니다. 채널 소유 실행 helper의 경우, 번들 플러그인은 실행 런타임을 자체 extension 모듈 안에 유지해야 합니다. core는 더 이상 src/agents/tools 아래의 Discord, Slack, Telegram, 또는 WhatsApp 메시지 action 런타임을 소유하지 않습니다. 우리는 별도의 plugin-sdk/*-action-runtime subpath를 공개하지 않으며, 번들 플러그인은 자체 extension 소유 모듈에서 로컬 런타임 코드를 직접 import해야 합니다. 일반적으로 provider 이름이 붙은 SDK seam에도 같은 경계가 적용됩니다. core는 Slack, Discord, Signal, WhatsApp, 또는 유사한 extension의 채널별 convenience barrel을 import해서는 안 됩니다. core에 어떤 동작이 필요하다면, 번들 플러그인의 자체 api.ts / runtime-api.ts barrel을 사용하거나, 그 요구를 공유 SDK의 좁고 일반적인 capability로 승격해야 합니다. 특히 poll의 경우 실행 경로는 두 가지입니다.
  • outbound.sendPoll은 공통 poll 모델에 맞는 채널을 위한 공유 기준선입니다
  • actions.handleAction("poll")은 채널별 poll 의미론 또는 추가 poll 파라미터를 위한 권장 경로입니다
이제 core는 플러그인 poll dispatch가 action을 거절한 뒤에야 공유 poll 파싱을 수행하므로, 플러그인 소유 poll handler는 먼저 일반 poll parser에 막히지 않고 채널별 poll 필드를 받아들일 수 있습니다. 전체 시작 시퀀스는 로드 파이프라인을 참고하세요.

Capability 소유권 모델

OpenClaw는 네이티브 플러그인을 관련 없는 통합의 모음이 아니라 회사 또는 기능에 대한 소유권 경계로 취급합니다. 즉, 다음을 의미합니다.
  • 회사 플러그인은 보통 해당 회사의 OpenClaw 표면 전체를 소유해야 합니다
  • 기능 플러그인은 보통 자신이 도입하는 기능 표면 전체를 소유해야 합니다
  • 채널은 provider 동작을 임시로 재구현하지 말고 공유 core capability를 소비해야 합니다
예시:
  • 번들 openai 플러그인은 OpenAI 모델-provider 동작과 OpenAI speech + realtime-voice + media-understanding + image-generation 동작을 소유합니다
  • 번들 elevenlabs 플러그인은 ElevenLabs speech 동작을 소유합니다
  • 번들 microsoft 플러그인은 Microsoft speech 동작을 소유합니다
  • 번들 google 플러그인은 Google 모델-provider 동작과 Google media-understanding + image-generation + web-search 동작을 소유합니다
  • 번들 firecrawl 플러그인은 Firecrawl web-fetch 동작을 소유합니다
  • 번들 minimax, mistral, moonshot, zai 플러그인은 media-understanding 백엔드를 소유합니다
  • 번들 qwen 플러그인은 Qwen 텍스트-provider 동작과 media-understanding 및 video-generation 동작을 소유합니다
  • voice-call 플러그인은 기능 플러그인입니다. call transport, tool, CLI, route, Twilio media-stream bridging을 소유하지만, vendor 플러그인을 직접 import하는 대신 공유 speech, realtime-transcription, realtime-voice capability를 소비합니다
의도된 최종 상태는 다음과 같습니다.
  • OpenAI는 텍스트 모델, speech, image, 향후 video까지 걸쳐도 하나의 플러그인에 존재합니다
  • 다른 vendor도 자신의 surface area에 대해 같은 방식으로 구성할 수 있습니다
  • 채널은 어떤 vendor 플러그인이 provider를 소유하는지 신경 쓰지 않고, core가 노출하는 공유 capability 계약을 소비합니다
이것이 핵심 구분입니다.
  • plugin = 소유권 경계
  • capability = 여러 플러그인이 구현하거나 소비할 수 있는 core 계약
따라서 OpenClaw가 video 같은 새 도메인을 추가할 때 첫 질문은 “어떤 provider가 video 처리를 하드코딩해야 하는가?”가 아닙니다. 첫 질문은 “core video capability 계약은 무엇인가?”입니다. 그 계약이 존재하면 vendor 플러그인이 거기에 등록하고 채널/기능 플러그인이 이를 소비할 수 있습니다. 아직 capability가 존재하지 않는다면, 일반적으로 올바른 순서는 다음과 같습니다.
  1. core에 누락된 capability를 정의합니다
  2. 이를 typed 방식으로 plugin API/runtime을 통해 노출합니다
  3. 채널/기능을 그 capability에 연결합니다
  4. vendor 플러그인이 구현을 등록하도록 합니다
이렇게 하면 소유권을 명확하게 유지하면서도 단일 vendor 또는 일회성 플러그인별 코드 경로에 의존하는 core 동작을 피할 수 있습니다.

Capability 계층화

코드가 어디에 속해야 하는지 결정할 때 다음 사고 모델을 사용하세요.
  • core capability 계층: 공유 orchestration, 정책, fallback, config 병합 규칙, 전달 의미론, typed 계약
  • vendor plugin 계층: vendor별 API, auth, 모델 catalog, speech synthesis, image generation, 미래의 video 백엔드, usage endpoint
  • channel/feature plugin 계층: Slack/Discord/voice-call 등 통합으로, core capability를 소비해 표면에 제시합니다
예를 들어 TTS는 다음과 같은 형태를 따릅니다.
  • core는 reply 시점 TTS 정책, fallback 순서, prefs, channel delivery를 소유합니다
  • openai, elevenlabs, microsoft는 synthesis 구현을 소유합니다
  • voice-call은 telephony TTS 런타임 helper를 소비합니다
이 패턴은 미래 capability에도 우선 적용되어야 합니다.

멀티-capability 회사 플러그인 예시

회사 플러그인은 외부에서 보기에 응집력 있어야 합니다. OpenClaw에 모델, speech, realtime transcription, realtime voice, media understanding, image generation, video generation, web fetch, web search에 대한 공유 계약이 있다면, vendor는 모든 표면을 한 곳에서 소유할 수 있습니다.
import type { OpenClawPluginDefinition } from "openclaw/plugin-sdk/plugin-entry";
import {
  describeImageWithModel,
  transcribeOpenAiCompatibleAudio,
} from "openclaw/plugin-sdk/media-understanding";

const plugin: OpenClawPluginDefinition = {
  id: "exampleai",
  name: "ExampleAI",
  register(api) {
    api.registerProvider({
      id: "exampleai",
      // auth/model catalog/runtime hooks
    });

    api.registerSpeechProvider({
      id: "exampleai",
      // vendor speech config — implement the SpeechProviderPlugin interface directly
    });

    api.registerMediaUnderstandingProvider({
      id: "exampleai",
      capabilities: ["image", "audio", "video"],
      async describeImage(req) {
        return describeImageWithModel({
          provider: "exampleai",
          model: req.model,
          input: req.input,
        });
      },
      async transcribeAudio(req) {
        return transcribeOpenAiCompatibleAudio({
          provider: "exampleai",
          model: req.model,
          input: req.input,
        });
      },
    });

    api.registerWebSearchProvider(
      createPluginBackedWebSearchProvider({
        id: "exampleai-search",
        // credential + fetch logic
      }),
    );
  },
};

export default plugin;
중요한 것은 정확한 helper 이름이 아닙니다. 중요한 것은 형태입니다.
  • 하나의 플러그인이 vendor surface를 소유합니다
  • core는 여전히 capability 계약을 소유합니다
  • 채널과 기능 플러그인은 vendor 코드가 아니라 api.runtime.* helper를 소비합니다
  • contract test는 플러그인이 자신이 소유한다고 주장하는 capability를 등록했는지 검증할 수 있습니다

Capability 예시: 비디오 이해

OpenClaw는 이미 image/audio/video 이해를 하나의 공유 capability로 취급합니다. 여기에도 동일한 소유권 모델이 적용됩니다.
  1. core가 media-understanding 계약을 정의합니다
  2. vendor 플러그인이 필요에 따라 describeImage, transcribeAudio, describeVideo를 등록합니다
  3. 채널과 기능 플러그인은 vendor 코드에 직접 연결하지 않고 공유 core 동작을 소비합니다
이렇게 하면 특정 provider의 video 가정을 core에 내장하지 않게 됩니다. 플러그인은 vendor surface를 소유하고, core는 capability 계약과 fallback 동작을 소유합니다. 비디오 생성도 이미 같은 순서를 따릅니다. core가 typed capability 계약과 런타임 helper를 소유하고, vendor 플러그인이 api.registerVideoGenerationProvider(...) 구현을 등록합니다. 구체적인 rollout 체크리스트가 필요하다면 Capability Cookbook를 참고하세요.

계약과 강제

플러그인 API surface는 의도적으로 typed되고 OpenClawPluginApi에 중앙집중화되어 있습니다. 이 계약은 지원되는 등록 지점과 플러그인이 의존할 수 있는 런타임 helper를 정의합니다. 이것이 중요한 이유:
  • 플러그인 작성자는 하나의 안정된 내부 표준을 얻게 됩니다
  • core는 두 플러그인이 같은 provider id를 등록하는 식의 중복 소유권을 거부할 수 있습니다
  • 시작 시 잘못된 등록에 대해 실행 가능한 진단을 표시할 수 있습니다
  • contract test는 번들 플러그인 소유권을 강제하고 조용한 드리프트를 방지할 수 있습니다
강제는 두 계층으로 나뉩니다.
  1. 런타임 등록 강제 플러그인 레지스트리는 플러그인이 로드될 때 등록을 검증합니다. 예: 중복 provider id, 중복 speech provider id, 잘못된 등록은 정의되지 않은 동작 대신 플러그인 진단을 생성합니다.
  2. contract test 번들 플러그인은 테스트 실행 중 contract registry에 캡처되므로, OpenClaw가 소유권을 명시적으로 검증할 수 있습니다. 오늘날 이는 모델 provider, speech provider, web search provider, 번들 등록 소유권에 사용됩니다.
실질적인 효과는 OpenClaw가 어떤 플러그인이 어떤 surface를 소유하는지 처음부터 알고 있다는 점입니다. 이 덕분에 core와 채널은 소유권이 암묵적이 아니라 선언적이고, typed되어 있으며, 테스트 가능하기 때문에 매끄럽게 조합될 수 있습니다.

계약에 포함되어야 할 것

좋은 플러그인 계약은 다음과 같습니다.
  • typed
  • 작음
  • capability별로 구체적임
  • core가 소유함
  • 여러 플러그인이 재사용 가능함
  • vendor 지식 없이도 채널/기능이 소비 가능함
나쁜 플러그인 계약은 다음과 같습니다.
  • core 안에 숨겨진 vendor별 정책
  • 레지스트리를 우회하는 일회성 플러그인 탈출구
  • vendor 구현에 직접 접근하는 채널 코드
  • OpenClawPluginApi 또는 api.runtime의 일부가 아닌 ad hoc 런타임 객체
애매하다면 추상화 수준을 높이세요. 먼저 capability를 정의한 다음, 플러그인이 거기에 연결되도록 하세요.

실행 모델

네이티브 OpenClaw 플러그인은 Gateway와 동일 프로세스 내에서 실행됩니다. 샌드박스되지 않습니다. 로드된 네이티브 플러그인은 core 코드와 동일한 프로세스 수준 신뢰 경계를 가집니다. 의미하는 바:
  • 네이티브 플러그인은 tool, 네트워크 handler, hook, service를 등록할 수 있습니다
  • 네이티브 플러그인 버그는 gateway를 크래시시키거나 불안정하게 만들 수 있습니다
  • 악의적인 네이티브 플러그인은 OpenClaw 프로세스 내부의 임의 코드 실행과 동등합니다
호환 bundle은 OpenClaw가 현재 이를 메타데이터/콘텐츠 팩으로 취급하기 때문에 기본적으로 더 안전합니다. 현재 릴리스에서는 이것이 주로 번들 Skills를 의미합니다. 번들되지 않은 플러그인에는 allowlist와 명시적 install/load 경로를 사용하세요. workspace 플러그인은 프로덕션 기본값이 아니라 개발 시점 코드로 취급하세요. 번들 workspace 패키지 이름의 경우, npm 이름에 플러그인 id를 고정하세요. 기본값은 @openclaw/<id>이며, 패키지가 더 좁은 플러그인 역할을 의도적으로 노출하는 경우 -provider, -plugin, -speech, -sandbox, -media-understanding 같은 승인된 typed suffix를 사용할 수 있습니다. 중요한 신뢰 참고 사항:
  • plugins.allow는 소스 provenance가 아니라 플러그인 id를 신뢰합니다.
  • 번들 플러그인과 같은 id를 가진 workspace 플러그인은, 해당 workspace 플러그인이 활성화되거나 allowlist에 있으면 의도적으로 번들 사본을 가립니다.
  • 이는 정상이며 로컬 개발, 패치 테스트, hotfix에 유용합니다.

export 경계

OpenClaw는 구현 편의성이 아니라 capability를 export합니다. capability 등록은 공개 상태로 유지하세요. 비계약 helper export는 줄이세요.
  • 번들 플러그인 전용 helper subpath
  • 공개 API가 아닌 런타임 plumbing subpath
  • vendor별 convenience helper
  • 구현 세부사항인 setup/onboarding helper
일부 번들 플러그인 helper subpath는 호환성과 번들 플러그인 유지보수를 위해 생성된 SDK export 맵에 여전히 남아 있습니다. 현재 예시로는 plugin-sdk/feishu, plugin-sdk/feishu-setup, plugin-sdk/zalo, plugin-sdk/zalo-setup, 그리고 여러 plugin-sdk/matrix* seam이 있습니다. 이들은 새로운 서드파티 플러그인을 위한 권장 SDK 패턴이 아니라, 예약된 구현 세부 export로 취급하세요.

로드 파이프라인

시작 시 OpenClaw는 대략 다음을 수행합니다.
  1. 후보 플러그인 루트를 발견합니다
  2. 네이티브 또는 호환 bundle manifest와 패키지 메타데이터를 읽습니다
  3. 안전하지 않은 후보를 거부합니다
  4. 플러그인 config를 정규화합니다 (plugins.enabled, allow, deny, entries, slots, load.paths)
  5. 각 후보의 활성화 여부를 결정합니다
  6. 활성화된 네이티브 모듈을 jiti를 통해 로드합니다
  7. 네이티브 register(api)(또는 레거시 별칭인 activate(api)) hook을 호출하고 등록 내용을 플러그인 레지스트리에 수집합니다
  8. 레지스트리를 command/runtime surface에 노출합니다
activateregister의 레거시 별칭입니다 — 로더는 존재하는 것을 (def.register ?? def.activate) resolve하여 같은 지점에서 호출합니다. 모든 번들 플러그인은 register를 사용합니다. 새 플러그인에는 register를 우선하세요.
안전 게이트는 런타임 실행 이전에 발생합니다. 후보는 entry가 플러그인 루트를 벗어나거나, 경로가 world-writable이거나, 번들되지 않은 플러그인에 대해 경로 소유권이 수상해 보일 때 차단됩니다.

Manifest 우선 동작

manifest는 control-plane의 source of truth입니다. OpenClaw는 이를 사용해 다음을 수행합니다.
  • 플러그인 식별
  • 선언된 channel/skill/config schema 또는 bundle capability 발견
  • plugins.entries.<id>.config 검증
  • Control UI label/placeholder 보강
  • install/catalog 메타데이터 표시
네이티브 플러그인의 경우 런타임 모듈은 data-plane 부분입니다. hook, tool, command, provider flow 같은 실제 동작을 등록합니다.

로더가 캐시하는 것

OpenClaw는 다음에 대해 짧은 in-process 캐시를 유지합니다.
  • discovery 결과
  • manifest registry 데이터
  • 로드된 플러그인 레지스트리
이 캐시는 burst 형태의 시작 비용과 반복 command 오버헤드를 줄여줍니다. 이를 지속성으로 보지 말고 짧게 유지되는 성능 캐시로 생각하면 안전합니다. 성능 참고:
  • OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE=1 또는 OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE=1을 설정하면 이 캐시를 비활성화할 수 있습니다.
  • 캐시 기간은 OPENCLAW_PLUGIN_DISCOVERY_CACHE_MSOPENCLAW_PLUGIN_MANIFEST_CACHE_MS로 조정할 수 있습니다.

레지스트리 모델

로드된 플러그인은 임의의 core 전역 상태를 직접 변경하지 않습니다. 중앙 플러그인 레지스트리에 등록합니다. 레지스트리가 추적하는 것:
  • 플러그인 레코드 (identity, source, origin, status, diagnostics)
  • tool
  • 레거시 hook 및 typed hook
  • channel
  • provider
  • gateway RPC handler
  • HTTP route
  • CLI registrar
  • background service
  • 플러그인 소유 command
그 다음 core 기능은 플러그인 모듈과 직접 통신하는 대신 이 레지스트리를 읽습니다. 이렇게 하면 로딩이 한 방향으로 유지됩니다.
  • 플러그인 모듈 -> 레지스트리 등록
  • core 런타임 -> 레지스트리 소비
이 분리는 유지보수성 측면에서 중요합니다. 대부분의 core surface는 “레지스트리를 읽는다”는 하나의 통합 지점만 필요하며, “모든 플러그인 모듈을 특별 취급한다”는 방식이 필요하지 않습니다.

conversation binding 콜백

conversation을 바인딩하는 플러그인은 승인 해결 시 반응할 수 있습니다. bind 요청이 승인되거나 거부된 뒤 콜백을 받으려면 api.onConversationBindingResolved(...)를 사용하세요.
export default {
  id: "my-plugin",
  register(api) {
    api.onConversationBindingResolved(async (event) => {
      if (event.status === "approved") {
        // 이 플러그인 + conversation에 대한 binding이 이제 존재합니다.
        console.log(event.binding?.conversationId);
        return;
      }

      // 요청이 거부되었습니다. 로컬 pending 상태를 정리합니다.
      console.log(event.request.conversation.conversationId);
    });
  },
};
콜백 payload 필드:
  • status: "approved" 또는 "denied"
  • decision: "allow-once", "allow-always", 또는 "deny"
  • binding: 승인된 요청에 대한 resolve된 binding
  • request: 원래 요청 요약, detach 힌트, sender id, conversation 메타데이터
이 콜백은 알림 전용입니다. 누가 conversation을 바인딩할 수 있는지는 변경하지 않으며, core 승인 처리가 끝난 뒤 실행됩니다.

Provider 런타임 훅

이제 provider 플러그인은 두 계층을 가집니다.
  • manifest 메타데이터: 런타임 로드 전에 가벼운 env-auth 조회를 위한 providerAuthEnvVars, 그리고 런타임 로드 전에 onboarding/auth-choice label 및 CLI flag 메타데이터를 위한 providerAuthChoices
  • config 시점 훅: catalog / 레거시 discoveryapplyConfigDefaults
  • 런타임 훅: normalizeModelId, normalizeTransport, normalizeConfig, applyNativeStreamingUsageCompat, resolveConfigApiKey, resolveSyntheticAuth, shouldDeferSyntheticProfileAuth, resolveDynamicModel, prepareDynamicModel, normalizeResolvedModel, contributeResolvedModelCompat, capabilities, normalizeToolSchemas, inspectToolSchemas, resolveReasoningOutputMode, prepareExtraParams, createStreamFn, wrapStreamFn, resolveTransportTurnState, resolveWebSocketSessionPolicy, formatApiKey, refreshOAuth, buildAuthDoctorHint, matchesContextOverflowError, classifyFailoverReason, isCacheTtlEligible, buildMissingAuthMessage, suppressBuiltInModel, augmentModelCatalog, isBinaryThinking, supportsXHighThinking, resolveDefaultThinkingLevel, isModernModelRef, prepareRuntimeAuth, resolveUsageAuth, fetchUsageSnapshot, createEmbeddingProvider, buildReplayPolicy, sanitizeReplayHistory, validateReplayTurns, onModelSelected
OpenClaw는 여전히 일반적인 agent loop, failover, transcript 처리, tool 정책을 소유합니다. 이러한 훅은 전체 커스텀 추론 transport가 없어도 provider별 동작을 확장할 수 있는 표면입니다. provider가 env 기반 자격 증명을 가지고 있고, 일반 auth/status/model-picker 경로가 provider 런타임을 로드하지 않고도 이를 볼 수 있어야 한다면 manifest providerAuthEnvVars를 사용하세요. onboarding/auth-choice CLI surface가 provider의 choice id, group label, 간단한 one-flag auth wiring을 provider 런타임을 로드하지 않고도 알아야 한다면 manifest providerAuthChoices를 사용하세요. provider 런타임 envVars는 onboarding label이나 OAuth client-id/client-secret setup var 같은 운영자 대상 힌트용으로 유지하세요.

훅 순서와 사용법

모델/provider 플러그인의 경우 OpenClaw는 대략 다음 순서로 훅을 호출합니다. “언제 사용하나” 열은 빠른 판단 가이드입니다.
#Hook수행 내용언제 사용하나
1catalogmodels.json 생성 중 provider config를 models.providers에 게시provider가 catalog 또는 base URL 기본값을 소유함
2applyConfigDefaultsconfig materialization 중 provider 소유 전역 config 기본값 적용기본값이 auth 모드, env, 또는 provider model-family 의미론에 따라 달라짐
(내장 모델 조회)OpenClaw가 먼저 일반 registry/catalog 경로를 시도함(플러그인 훅 아님)
3normalizeModelId조회 전에 레거시 또는 preview model-id alias를 정규화provider가 canonical 모델 resolve 전에 alias 정리를 소유함
4normalizeTransport일반 모델 조립 전에 provider-family api / baseUrl 정규화provider가 동일 transport family 내 커스텀 provider id에 대한 transport 정리를 소유함
5normalizeConfig런타임/provider resolve 전에 models.providers.<id> 정규화provider에 속해야 하는 config 정리가 필요함; 번들 Google-family helper도 지원되는 Google config entry를 보완 정리함
6applyNativeStreamingUsageCompatconfig provider에 네이티브 streaming-usage compat 재작성 적용provider가 endpoint 기반 네이티브 streaming usage 메타데이터 수정을 필요로 함
7resolveConfigApiKey런타임 auth 로드 전에 config provider에 대한 env-marker auth resolveprovider가 provider 소유 env-marker API-key resolve를 가짐; amazon-bedrock도 여기에서 내장 AWS env-marker resolver를 가짐
8resolveSyntheticAuth평문을 저장하지 않고 local/self-hosted 또는 config 기반 auth를 노출provider가 synthetic/local credential marker로 동작 가능함
9shouldDeferSyntheticProfileAuth저장된 synthetic profile placeholder의 우선순위를 env/config 기반 auth 뒤로 낮춤provider가 우선 순위를 가지면 안 되는 synthetic placeholder profile을 저장함
10resolveDynamicModel아직 로컬 registry에 없는 provider 소유 model id에 대한 동기 fallbackprovider가 임의의 업스트림 model id를 허용함
11prepareDynamicModel비동기 warm-up 후 resolveDynamicModel를 다시 실행provider가 알 수 없는 id를 resolve하기 전에 네트워크 메타데이터가 필요함
12normalizeResolvedModelembedded runner가 resolve된 모델을 사용하기 전 최종 재작성provider가 transport 재작성이 필요하지만 여전히 core transport를 사용함
13contributeResolvedModelCompat다른 호환 transport 뒤에 있는 vendor 모델에 compat flag를 기여provider가 provider를 인수하지 않고도 proxy transport에서 자체 모델을 인식함
14capabilities공유 core 로직이 사용하는 provider 소유 transcript/tooling 메타데이터provider가 transcript/provider-family 특수 동작을 필요로 함
15normalizeToolSchemasembedded runner가 보기 전에 tool schema 정규화provider가 transport-family schema 정리를 필요로 함
16inspectToolSchemas정규화 후 provider 소유 schema 진단을 노출provider가 core에 provider별 규칙을 가르치지 않고도 keyword 경고를 제공하고자 함
17resolveReasoningOutputModenative vs tagged reasoning-output 계약 선택provider가 native field 대신 tagged reasoning/final output을 필요로 함
18prepareExtraParams일반 stream option wrapper 전에 요청 파라미터 정규화provider가 기본 요청 파라미터 또는 provider별 파라미터 정리를 필요로 함
19createStreamFn일반 stream 경로를 완전히 커스텀 transport로 대체provider가 wrapper 수준이 아니라 커스텀 wire protocol을 필요로 함
20wrapStreamFn일반 wrapper 적용 후의 stream wrapperprovider가 커스텀 transport 없이 요청 header/body/model compat wrapper를 필요로 함
21resolveTransportTurnStatenative turn별 transport header 또는 메타데이터 첨부provider가 일반 transport에 provider-native turn identity를 보내고자 함
22resolveWebSocketSessionPolicynative WebSocket header 또는 session cool-down 정책 첨부provider가 일반 WS transport에서 session header 또는 fallback 정책을 조정하고자 함
23formatApiKeyauth-profile formatter: 저장된 profile을 런타임 apiKey 문자열로 변환provider가 추가 auth 메타데이터를 저장하며 커스텀 런타임 토큰 형식을 필요로 함
24refreshOAuth커스텀 refresh endpoint 또는 refresh 실패 정책을 위한 OAuth refresh overrideprovider가 공유 pi-ai refresher에 맞지 않음
25buildAuthDoctorHintOAuth refresh 실패 시 추가되는 복구 힌트provider가 refresh 실패 후 provider 소유 auth 복구 가이드를 필요로 함
26matchesContextOverflowErrorprovider 소유 context-window overflow 매처provider에 일반 heuristic이 놓치는 raw overflow 오류가 있음
27classifyFailoverReasonprovider 소유 failover 이유 분류provider가 raw API/transport 오류를 rate-limit/overload 등으로 매핑 가능함
28isCacheTtlEligibleproxy/backhaul provider를 위한 prompt-cache 정책provider가 proxy별 cache TTL 게이팅을 필요로 함
29buildMissingAuthMessage일반 missing-auth 복구 메시지 대체provider가 provider별 missing-auth 복구 힌트를 필요로 함
30suppressBuiltInModel오래된 업스트림 모델 억제 및 선택적 사용자 대상 오류 힌트provider가 오래된 업스트림 row를 숨기거나 vendor 힌트로 대체해야 함
31augmentModelCatalogdiscovery 후 synthetic/final catalog row 추가provider가 models list 및 picker에 synthetic forward-compat row를 필요로 함
32isBinaryThinkingbinary-thinking provider에 대한 on/off reasoning 토글provider가 이진형 thinking on/off만 노출함
33supportsXHighThinking선택된 모델에 대한 xhigh reasoning 지원provider가 일부 모델에서만 xhigh를 원함
34resolveDefaultThinkingLevel특정 model family에 대한 기본 /think 레벨provider가 model family에 대한 기본 /think 정책을 소유함
35isModernModelRef라이브 profile 필터 및 smoke 선택을 위한 modern-model matcherprovider가 live/smoke 선호 모델 매칭을 소유함
36prepareRuntimeAuth추론 직전에 설정된 credential을 실제 런타임 token/key로 교환provider가 토큰 교환 또는 단기 요청 credential을 필요로 함
37resolveUsageAuth/usage 및 관련 status surface를 위한 usage/billing credential resolveprovider가 커스텀 usage/quota 토큰 파싱 또는 다른 usage credential을 필요로 함
38fetchUsageSnapshotauth resolve 후 provider별 usage/quota 스냅샷을 가져와 정규화provider가 provider별 usage endpoint 또는 payload parser를 필요로 함
39createEmbeddingProvidermemory/search용 provider 소유 embedding adapter 구성memory embedding 동작이 provider 플러그인에 속함
40buildReplayPolicyprovider의 transcript 처리를 제어하는 replay 정책 반환provider가 커스텀 transcript 정책(예: thinking block 제거)을 필요로 함
41sanitizeReplayHistory일반 transcript 정리 후 replay history 재작성provider가 공유 compaction helper를 넘는 provider별 replay 재작성을 필요로 함
42validateReplayTurnsembedded runner 전 최종 replay-turn 검증 또는 재구성provider transport가 일반 sanitization 후 더 엄격한 turn 검증을 필요로 함
43onModelSelectedprovider 소유의 선택 후 부수 효과 실행provider가 모델 활성화 시 telemetry 또는 provider 소유 상태를 필요로 함
normalizeModelId, normalizeTransport, normalizeConfig는 먼저 매칭된 provider 플러그인을 확인한 다음, 실제로 model id 또는 transport/config를 변경하는 훅이 나올 때까지 다른 hook-capable provider 플러그인으로 이어집니다. 이렇게 하면 caller가 어떤 번들 플러그인이 재작성을 소유하는지 알 필요 없이 alias/compat provider shim이 동작할 수 있습니다. 어떤 provider 훅도 지원되는 Google-family config entry를 재작성하지 않으면, 번들 Google config normalizer가 그 호환성 정리를 계속 적용합니다. provider에 완전히 커스텀 wire protocol 또는 커스텀 request executor가 필요하다면, 그것은 다른 종류의 extension입니다. 이 훅은 여전히 OpenClaw의 일반적인 추론 loop 위에서 동작하는 provider 동작을 위한 것입니다.

Provider 예시

api.registerProvider({
  id: "example-proxy",
  label: "Example Proxy",
  auth: [],
  catalog: {
    order: "simple",
    run: async (ctx) => {
      const apiKey = ctx.resolveProviderApiKey("example-proxy").apiKey;
      if (!apiKey) {
        return null;
      }
      return {
        provider: {
          baseUrl: "https://proxy.example.com/v1",
          apiKey,
          api: "openai-completions",
          models: [{ id: "auto", name: "Auto" }],
        },
      };
    },
  },
  resolveDynamicModel: (ctx) => ({
    id: ctx.modelId,
    name: ctx.modelId,
    provider: "example-proxy",
    api: "openai-completions",
    baseUrl: "https://proxy.example.com/v1",
    reasoning: false,
    input: ["text"],
    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    contextWindow: 128000,
    maxTokens: 8192,
  }),
  prepareRuntimeAuth: async (ctx) => {
    const exchanged = await exchangeToken(ctx.apiKey);
    return {
      apiKey: exchanged.token,
      baseUrl: exchanged.baseUrl,
      expiresAt: exchanged.expiresAt,
    };
  },
  resolveUsageAuth: async (ctx) => {
    const auth = await ctx.resolveOAuthToken();
    return auth ? { token: auth.token } : null;
  },
  fetchUsageSnapshot: async (ctx) => {
    return await fetchExampleProxyUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn);
  },
});

내장 예시

  • Anthropic은 resolveDynamicModel, capabilities, buildAuthDoctorHint, resolveUsageAuth, fetchUsageSnapshot, isCacheTtlEligible, resolveDefaultThinkingLevel, applyConfigDefaults, isModernModelRef, wrapStreamFn을 사용합니다. 이는 Claude 4.6 forward-compat, provider-family 힌트, auth 복구 가이드, usage endpoint 통합, prompt-cache 적격성, auth 인지형 config 기본값, Claude 기본/적응형 thinking 정책, 그리고 beta header, /fast / serviceTier, context1m을 위한 Anthropic별 stream shaping을 소유하기 때문입니다.
  • Anthropic의 Claude 전용 stream helper는 현재 번들 플러그인의 자체 공개 api.ts / contract-api.ts seam에 남아 있습니다. 이 패키지 surface는 한 provider의 beta-header 규칙 때문에 일반 SDK를 넓히는 대신, wrapAnthropicProviderStream, resolveAnthropicBetas, resolveAnthropicFastMode, resolveAnthropicServiceTier, 그리고 더 낮은 수준의 Anthropic wrapper builder를 export합니다.
  • OpenAI는 resolveDynamicModel, normalizeResolvedModel, capabilities, buildMissingAuthMessage, suppressBuiltInModel, augmentModelCatalog, supportsXHighThinking, isModernModelRef를 사용합니다. 이는 GPT-5.4 forward-compat, 직접 OpenAI의 openai-completions -> openai-responses 정규화, Codex 인지형 auth 힌트, Spark 억제, synthetic OpenAI 목록 row, GPT-5 thinking / live-model 정책을 소유하기 때문입니다. openai-responses-defaults stream family는 attribution header, /fast/serviceTier, text verbosity, native Codex web search, reasoning-compat payload shaping, Responses context 관리를 위한 공유 native OpenAI Responses wrapper를 소유합니다.
  • OpenRouter는 catalog, resolveDynamicModel, prepareDynamicModel을 사용합니다. 이 provider는 pass-through이며 OpenClaw의 정적 catalog가 업데이트되기 전에 새 model id를 노출할 수 있기 때문입니다. 또한 provider별 request header, 라우팅 메타데이터, reasoning patch, prompt-cache 정책을 core 밖에 유지하기 위해 capabilities, wrapStreamFn, isCacheTtlEligible도 사용합니다. replay 정책은 passthrough-gemini family에서 오며, openrouter-thinking stream family는 proxy reasoning injection 및 지원되지 않는 모델 / auto 건너뛰기를 소유합니다.
  • GitHub Copilot은 catalog, auth, resolveDynamicModel, capabilities, prepareRuntimeAuth, fetchUsageSnapshot을 사용합니다. 이는 provider 소유 device login, model fallback 동작, Claude transcript 특수 동작, GitHub token -> Copilot token 교환, provider 소유 usage endpoint가 필요하기 때문입니다.
  • OpenAI Codex는 catalog, resolveDynamicModel, normalizeResolvedModel, refreshOAuth, augmentModelCatalog, prepareExtraParams, resolveUsageAuth, fetchUsageSnapshot을 사용합니다. 이는 여전히 core OpenAI transport 위에서 동작하지만, transport/base URL 정규화, OAuth refresh fallback 정책, 기본 transport 선택, synthetic Codex catalog row, ChatGPT usage endpoint 통합을 소유하기 때문입니다. direct OpenAI와 같은 openai-responses-defaults stream family를 공유합니다.
  • Google AI Studio와 Gemini CLI OAuth는 resolveDynamicModel, buildReplayPolicy, sanitizeReplayHistory, resolveReasoningOutputMode, wrapStreamFn, isModernModelRef를 사용합니다. 이는 google-gemini replay family가 Gemini 3.1 forward-compat fallback, native Gemini replay 검증, bootstrap replay 정리, tagged reasoning-output mode, modern-model 매칭을 소유하고, google-thinking stream family가 Gemini thinking payload 정규화를 소유하기 때문입니다. Gemini CLI OAuth는 토큰 형식화, 토큰 파싱, quota endpoint 연결을 위해 formatApiKey, resolveUsageAuth, fetchUsageSnapshot도 사용합니다.
  • Anthropic Vertex는 anthropic-by-model replay family를 통해 buildReplayPolicy를 사용하므로, Claude별 replay 정리가 모든 anthropic-messages transport가 아니라 Claude id에만 범위가 제한됩니다.
  • Amazon Bedrock은 buildReplayPolicy, matchesContextOverflowError, classifyFailoverReason, resolveDefaultThinkingLevel을 사용합니다. 이는 Anthropic-on-Bedrock 트래픽에 대한 Bedrock별 throttle/not-ready/context-overflow 오류 분류를 소유하기 때문입니다. replay 정책은 여전히 동일한 Claude 전용 anthropic-by-model guard를 공유합니다.
  • OpenRouter, Kilocode, Opencode, Opencode Go는 passthrough-gemini replay family를 통해 buildReplayPolicy를 사용합니다. 이는 OpenAI 호환 transport를 통해 Gemini 모델을 proxy하며, native Gemini replay 검증 또는 bootstrap 재작성 없이도 Gemini thought-signature 정리가 필요하기 때문입니다.
  • MiniMax는 hybrid-anthropic-openai replay family를 통해 buildReplayPolicy를 사용합니다. 이는 한 provider가 Anthropic-message와 OpenAI 호환 의미론을 모두 소유하기 때문입니다. Anthropic 쪽에서는 Claude 전용 thinking-block 제거를 유지하면서 reasoning 출력 모드를 native로 다시 override하고, minimax-fast-mode stream family는 공유 stream 경로에서 fast-mode 모델 재작성을 소유합니다.
  • Moonshot은 catalog, wrapStreamFn을 사용합니다. 여전히 공유 OpenAI transport를 사용하지만 provider 소유 thinking payload 정규화가 필요하기 때문입니다. moonshot-thinking stream family는 config와 /think 상태를 native binary thinking payload에 매핑합니다.
  • Kilocode는 catalog, capabilities, wrapStreamFn, isCacheTtlEligible를 사용합니다. provider 소유 request header, reasoning payload 정규화, Gemini transcript 힌트, Anthropic cache-TTL 게이팅이 필요하기 때문입니다. kilocode-thinking stream family는 공유 proxy stream 경로에서 Kilo thinking injection을 유지하면서, 명시적 reasoning payload를 지원하지 않는 kilo/auto와 기타 proxy model id는 건너뜁니다.
  • Z.AI는 resolveDynamicModel, prepareExtraParams, wrapStreamFn, isCacheTtlEligible, isBinaryThinking, isModernModelRef, resolveUsageAuth, fetchUsageSnapshot을 사용합니다. 이는 GLM-5 fallback, tool_stream 기본값, binary thinking UX, modern-model 매칭, 그리고 usage auth + quota fetching을 모두 소유하기 때문입니다. tool-stream-default-on stream family는 기본 활성화된 tool_stream wrapper를 provider별 수동 glue 밖에 유지합니다.
  • xAI는 normalizeResolvedModel, normalizeTransport, contributeResolvedModelCompat, prepareExtraParams, wrapStreamFn, resolveSyntheticAuth, resolveDynamicModel, isModernModelRef를 사용합니다. 이는 native xAI Responses transport 정규화, Grok fast-mode alias 재작성, 기본 tool_stream, strict-tool / reasoning-payload 정리, 플러그인 소유 tool용 fallback auth 재사용, forward-compat Grok model resolve, xAI tool-schema profile, 미지원 schema keyword, native web_search, HTML entity tool-call argument decoding 같은 provider 소유 compat patch를 소유하기 때문입니다.
  • Mistral, OpenCode Zen, OpenCode Go는 transcript/tooling 특수 동작을 core 밖에 두기 위해 capabilities만 사용합니다.
  • byteplus, cloudflare-ai-gateway, huggingface, kimi-coding, nvidia, qianfan, synthetic, together, venice, vercel-ai-gateway, volcengine 같은 catalog 전용 번들 provider는 catalog만 사용합니다.
  • Qwen은 텍스트 provider용 catalog와, multimodal surface를 위한 공유 media-understanding 및 video-generation 등록을 사용합니다.
  • MiniMax와 Xiaomi는 추론은 여전히 공유 transport를 통해 실행되더라도 /usage 동작이 플러그인 소유이기 때문에 catalog와 usage 훅을 사용합니다.

런타임 helper

플러그인은 api.runtime를 통해 선택된 core helper에 접근할 수 있습니다. TTS 예시:
const clip = await api.runtime.tts.textToSpeech({
  text: "Hello from OpenClaw",
  cfg: api.config,
});

const result = await api.runtime.tts.textToSpeechTelephony({
  text: "Hello from OpenClaw",
  cfg: api.config,
});

const voices = await api.runtime.tts.listVoices({
  provider: "elevenlabs",
  cfg: api.config,
});
참고:
  • textToSpeech는 파일/음성 노트 surface를 위한 일반 core TTS output payload를 반환합니다.
  • core messages.tts config와 provider 선택을 사용합니다.
  • PCM 오디오 버퍼 + 샘플 레이트를 반환합니다. 플러그인은 provider에 맞게 리샘플링/인코딩해야 합니다.
  • listVoices는 provider별로 선택 사항입니다. vendor 소유 voice picker 또는 setup flow에 사용하세요.
  • voice 목록은 locale, gender, personality tag 같은 더 풍부한 메타데이터를 포함할 수 있어 provider 인지형 picker에 활용할 수 있습니다.
  • 현재 telephony를 지원하는 것은 OpenAI와 ElevenLabs입니다. Microsoft는 지원하지 않습니다.
플러그인은 api.registerSpeechProvider(...)를 통해 speech provider를 등록할 수도 있습니다.
api.registerSpeechProvider({
  id: "acme-speech",
  label: "Acme Speech",
  isConfigured: ({ config }) => Boolean(config.messages?.tts),
  synthesize: async (req) => {
    return {
      audioBuffer: Buffer.from([]),
      outputFormat: "mp3",
      fileExtension: ".mp3",
      voiceCompatible: false,
    };
  },
});
참고:
  • TTS 정책, fallback, reply 전달은 core에 유지하세요.
  • vendor 소유 synthesis 동작에는 speech provider를 사용하세요.
  • 레거시 Microsoft edge 입력은 microsoft provider id로 정규화됩니다.
  • 선호되는 소유권 모델은 회사 지향적입니다. OpenClaw가 이러한 capability 계약을 추가해 갈수록 하나의 vendor 플러그인이 텍스트, speech, image, 향후 미디어 provider를 함께 소유할 수 있습니다.
이미지/오디오/비디오 이해의 경우, 플러그인은 일반적인 key/value bag 대신 하나의 typed media-understanding provider를 등록합니다.
api.registerMediaUnderstandingProvider({
  id: "google",
  capabilities: ["image", "audio", "video"],
  describeImage: async (req) => ({ text: "..." }),
  transcribeAudio: async (req) => ({ text: "..." }),
  describeVideo: async (req) => ({ text: "..." }),
});
참고:
  • orchestration, fallback, config, channel 연결은 core에 유지하세요.
  • vendor 동작은 provider 플러그인에 유지하세요.
  • 점진적 확장은 typed 상태를 유지해야 합니다: 새로운 선택적 메서드, 새로운 선택적 결과 필드, 새로운 선택적 capability.
  • 비디오 생성도 이미 같은 패턴을 따릅니다.
    • core가 capability 계약과 런타임 helper를 소유합니다
    • vendor 플러그인이 api.registerVideoGenerationProvider(...)를 등록합니다
    • 기능/채널 플러그인이 api.runtime.videoGeneration.*를 소비합니다
media-understanding 런타임 helper의 경우, 플러그인은 다음을 호출할 수 있습니다.
const image = await api.runtime.mediaUnderstanding.describeImageFile({
  filePath: "/tmp/inbound-photo.jpg",
  cfg: api.config,
  agentDir: "/tmp/agent",
});

const video = await api.runtime.mediaUnderstanding.describeVideoFile({
  filePath: "/tmp/inbound-video.mp4",
  cfg: api.config,
});
오디오 전사의 경우, 플러그인은 media-understanding 런타임 또는 기존 STT 별칭을 사용할 수 있습니다.
const { text } = await api.runtime.mediaUnderstanding.transcribeAudioFile({
  filePath: "/tmp/inbound-audio.ogg",
  cfg: api.config,
  // MIME을 안정적으로 추론할 수 없을 때 선택 사항:
  mime: "audio/ogg",
});
참고:
  • api.runtime.mediaUnderstanding.*는 image/audio/video 이해를 위한 선호 공유 surface입니다.
  • core media-understanding 오디오 config (tools.media.audio)와 provider fallback 순서를 사용합니다.
  • 전사 출력이 생성되지 않으면 { text: undefined }를 반환합니다 (예: 입력 건너뜀/미지원).
  • api.runtime.stt.transcribeAudioFile(...)는 호환성 별칭으로 남아 있습니다.
플러그인은 api.runtime.subagent를 통해 백그라운드 subagent 실행도 시작할 수 있습니다.
const result = await api.runtime.subagent.run({
  sessionKey: "agent:main:subagent:search-helper",
  message: "Expand this query into focused follow-up searches.",
  provider: "openai",
  model: "gpt-4.1-mini",
  deliver: false,
});
참고:
  • providermodel은 지속적인 session 변경이 아니라 run별 override입니다.
  • OpenClaw는 신뢰된 caller에 대해서만 이러한 override 필드를 허용합니다.
  • 플러그인 소유 fallback 실행의 경우 운영자는 plugins.entries.<id>.subagent.allowModelOverride: true로 opt-in해야 합니다.
  • 신뢰된 플러그인을 특정 canonical provider/model 대상으로 제한하려면 plugins.entries.<id>.subagent.allowedModels를 사용하고, 모든 대상을 명시적으로 허용하려면 "*"를 사용하세요.
  • 신뢰되지 않은 플러그인의 subagent 실행도 동작하지만, override 요청은 조용히 fallback되지 않고 거부됩니다.
웹 검색의 경우, 플러그인은 agent tool wiring에 직접 접근하는 대신 공유 런타임 helper를 소비할 수 있습니다.
const providers = api.runtime.webSearch.listProviders({
  config: api.config,
});

const result = await api.runtime.webSearch.search({
  config: api.config,
  args: {
    query: "OpenClaw plugin runtime helpers",
    count: 5,
  },
});
플러그인은 api.registerWebSearchProvider(...)를 통해 웹 검색 provider도 등록할 수 있습니다. 참고:
  • provider 선택, credential resolve, 공유 request 의미론은 core에 유지하세요.
  • vendor별 검색 transport에는 웹 검색 provider를 사용하세요.
  • api.runtime.webSearch.*는 agent tool wrapper에 의존하지 않고 검색 동작이 필요한 기능/채널 플러그인을 위한 선호 공유 surface입니다.

api.runtime.imageGeneration

const result = await api.runtime.imageGeneration.generate({
  config: api.config,
  args: { prompt: "A friendly lobster mascot", size: "1024x1024" },
});

const providers = api.runtime.imageGeneration.listProviders({
  config: api.config,
});
  • generate(...): 설정된 image-generation provider 체인을 사용해 이미지를 생성합니다.
  • listProviders(...): 사용 가능한 image-generation provider와 capability를 나열합니다.

Gateway HTTP route

플러그인은 api.registerHttpRoute(...)로 HTTP endpoint를 노출할 수 있습니다.
api.registerHttpRoute({
  path: "/acme/webhook",
  auth: "plugin",
  match: "exact",
  handler: async (_req, res) => {
    res.statusCode = 200;
    res.end("ok");
    return true;
  },
});
route 필드:
  • path: gateway HTTP 서버 아래의 route 경로
  • auth: 필수. 일반 gateway auth가 필요하면 "gateway"를, 플러그인 관리 auth/webhook 검증이면 "plugin"을 사용하세요.
  • match: 선택 사항. "exact"(기본값) 또는 "prefix".
  • replaceExisting: 선택 사항. 같은 플러그인이 자신의 기존 route 등록을 대체할 수 있게 합니다.
  • handler: route가 request를 처리했으면 true를 반환합니다.
참고:
  • api.registerHttpHandler(...)는 제거되었으며 플러그인 로드 오류를 발생시킵니다. 대신 api.registerHttpRoute(...)를 사용하세요.
  • 플러그인 route는 반드시 auth를 명시적으로 선언해야 합니다.
  • 정확히 같은 path + match 충돌은 replaceExisting: true가 아닌 한 거부되며, 한 플러그인이 다른 플러그인의 route를 대체할 수 없습니다.
  • 서로 다른 auth 수준의 겹치는 route는 거부됩니다. exact/prefix fallthrough 체인은 같은 auth 수준 안에서만 유지하세요.
  • auth: "plugin" route는 운영자 런타임 scope를 자동으로 받지 않습니다. 이는 특권 있는 Gateway helper 호출이 아니라 플러그인 관리 webhook/서명 검증용입니다.
  • auth: "gateway" route는 Gateway request runtime scope 안에서 실행되지만, 그 scope는 의도적으로 보수적입니다.
    • 공유 비밀 bearer auth (gateway.auth.mode = "token" / "password")는 호출자가 x-openclaw-scopes를 보내더라도 플러그인 route 런타임 scope를 operator.write에 고정합니다
    • 신뢰된 identity-bearing HTTP 모드(예: trusted-proxy 또는 사설 ingress의 gateway.auth.mode = "none")는 헤더가 명시적으로 존재할 때만 x-openclaw-scopes를 존중합니다
    • 그러한 identity-bearing 플러그인 route 요청에서 x-openclaw-scopes가 없으면, 런타임 scope는 operator.write로 fallback됩니다
  • 실용적인 규칙: gateway-auth 플러그인 route를 암묵적인 admin surface로 가정하지 마세요. route에 admin 전용 동작이 필요하다면 identity-bearing auth 모드를 요구하고, 명시적 x-openclaw-scopes header 계약을 문서화하세요.

Plugin SDK import 경로

플러그인을 작성할 때는 모놀리식한 openclaw/plugin-sdk import 대신 SDK subpath를 사용하세요.
  • 플러그인 등록 primitive에는 openclaw/plugin-sdk/plugin-entry
  • 일반 공유 plugin 대상 계약에는 openclaw/plugin-sdk/core
  • 루트 openclaw.json Zod schema export (OpenClawSchema)에는 openclaw/plugin-sdk/config-schema
  • openclaw/plugin-sdk/channel-setup, openclaw/plugin-sdk/setup-runtime, openclaw/plugin-sdk/setup-adapter-runtime, openclaw/plugin-sdk/setup-tools, openclaw/plugin-sdk/channel-pairing, openclaw/plugin-sdk/channel-contract, openclaw/plugin-sdk/channel-feedback, openclaw/plugin-sdk/channel-inbound, openclaw/plugin-sdk/channel-lifecycle, openclaw/plugin-sdk/channel-reply-pipeline, openclaw/plugin-sdk/command-auth, openclaw/plugin-sdk/secret-input, openclaw/plugin-sdk/webhook-ingress 같은 안정적인 채널 primitive는 공유 setup/auth/reply/webhook wiring용입니다. channel-inbound는 debounce, mention 매칭, envelope formatting, inbound envelope 컨텍스트 helper를 위한 공유 홈입니다. channel-setup은 좁은 optional-install setup seam입니다. setup-runtimesetupEntry / deferred startup에 사용되는 런타임 안전 setup surface이며, import-safe setup patch adapter를 포함합니다. setup-adapter-runtime은 env 인지형 account-setup adapter seam입니다. setup-tools는 작은 CLI/archive/docs helper seam입니다 (formatCliCommand, detectBinary, extractArchive, resolveBrewExecutable, formatDocsLink, CONFIG_DIR).
  • openclaw/plugin-sdk/channel-config-helpers, openclaw/plugin-sdk/allow-from, openclaw/plugin-sdk/channel-config-schema, openclaw/plugin-sdk/telegram-command-config, openclaw/plugin-sdk/channel-policy, openclaw/plugin-sdk/approval-runtime, openclaw/plugin-sdk/config-runtime, openclaw/plugin-sdk/infra-runtime, openclaw/plugin-sdk/agent-runtime, openclaw/plugin-sdk/lazy-runtime, openclaw/plugin-sdk/reply-history, openclaw/plugin-sdk/routing, openclaw/plugin-sdk/status-helpers, openclaw/plugin-sdk/text-runtime, openclaw/plugin-sdk/runtime-store, openclaw/plugin-sdk/directory-runtime 같은 도메인 subpath는 공유 런타임/config helper용입니다. telegram-command-config는 Telegram 커스텀 command 정규화/검증을 위한 좁은 공개 seam이며, 번들 Telegram 계약 surface를 일시적으로 사용할 수 없더라도 계속 제공합니다. text-runtime은 assistant-visible-text 제거, markdown render/chunking helper, redaction helper, directive-tag helper, safe-text 유틸리티를 포함하는 공유 text/markdown/logging seam입니다.
  • approval 전용 채널 seam은 하나의 approvalCapability 계약을 플러그인에 두는 방식을 우선해야 합니다. 그러면 core는 관련 없는 플러그인 필드에 approval 동작을 섞는 대신, 그 하나의 capability를 통해 approval auth, delivery, render, native-routing 동작을 읽을 수 있습니다.
  • openclaw/plugin-sdk/channel-runtime은 deprecated되었으며 오래된 플러그인을 위한 호환성 shim으로만 남아 있습니다. 새 코드는 더 좁은 일반 primitive를 import해야 하며, repo 코드도 shim의 새 import를 추가해서는 안 됩니다.
  • 번들 extension 내부는 비공개로 유지됩니다. 외부 플러그인은 openclaw/plugin-sdk/* subpath만 사용해야 합니다. OpenClaw core/test 코드는 index.js, api.js, runtime-api.js, setup-entry.js, login-qr-api.js 같은 플러그인 패키지 루트의 공개 entry point와, 필요한 경우 일부 좁은 파일을 사용할 수 있습니다. core나 다른 extension에서 플러그인 패키지의 src/*를 import하지 마세요.
  • repo entry point 분리: <plugin-package-root>/api.js는 helper/types barrel, <plugin-package-root>/runtime-api.js는 런타임 전용 barrel, <plugin-package-root>/index.js는 번들 플러그인 entry, <plugin-package-root>/setup-entry.js는 setup 플러그인 entry입니다.
  • 현재 번들 provider 예시:
    • Anthropic은 wrapAnthropicProviderStream, beta-header helper, service_tier 파싱 같은 Claude stream helper를 위해 api.js / contract-api.js를 사용합니다.
    • OpenAI는 provider builder, default-model helper, realtime provider builder를 위해 api.js를 사용합니다.
    • OpenRouter는 provider builder와 onboarding/config helper를 위해 api.js를 사용하며, register.runtime.js는 repo 로컬 사용을 위해 일반 plugin-sdk/provider-stream helper를 재export할 수 있습니다.
  • facade로 로드되는 공개 entry point는 활성 런타임 config snapshot이 있으면 이를 우선 사용하고, OpenClaw가 아직 런타임 snapshot을 제공하지 않을 때는 디스크의 resolve된 config 파일로 fallback합니다.
  • 일반 공유 primitive는 여전히 선호되는 공개 SDK 계약입니다. 소수의 예약된 호환성용 번들 채널 브랜드 helper seam은 여전히 존재합니다. 이를 새로운 서드파티 import 대상이 아니라, 번들 유지보수/호환성 seam으로 취급하세요. 새로운 교차 채널 계약은 계속해서 일반 plugin-sdk/* subpath 또는 플러그인 로컬 api.js / runtime-api.js barrel에 추가되어야 합니다.
호환성 참고:
  • 새 코드에서는 루트 openclaw/plugin-sdk barrel을 피하세요.
  • 더 좁고 안정적인 primitive를 우선하세요. 더 새로운 setup/pairing/reply/ feedback/contract/inbound/threading/command/secret-input/webhook/infra/ allowlist/status/message-tool subpath는 새로운 번들 및 외부 플러그인 작업을 위한 의도된 계약입니다. target 파싱/매칭은 openclaw/plugin-sdk/channel-targets에 있어야 합니다. message action gate와 reaction message-id helper는 openclaw/plugin-sdk/channel-actions에 있어야 합니다.
  • 번들 extension 전용 helper barrel은 기본적으로 안정적이지 않습니다. helper가 번들 extension에만 필요하다면, 이를 openclaw/plugin-sdk/<extension>로 승격하는 대신 extension의 로컬 api.js 또는 runtime-api.js seam 뒤에 두세요.
  • 새로운 공유 helper seam은 채널 브랜드가 아니라 일반적이어야 합니다. 공유 target 파싱은 openclaw/plugin-sdk/channel-targets에 두고, 채널별 내부는 소유 플러그인의 로컬 api.js 또는 runtime-api.js seam 뒤에 유지하세요.
  • image-generation, media-understanding, speech 같은 capability별 subpath는 오늘날 번들/네이티브 플러그인이 사용하기 때문에 존재합니다. 이들의 존재 자체가 export된 모든 helper가 장기적으로 고정된 외부 계약임을 의미하지는 않습니다.

Message tool schema

플러그인은 채널별 describeMessageTool(...) schema 기여를 소유해야 합니다. provider별 필드는 공유 core가 아니라 플러그인 내부에 두세요. 공유 가능한 이식형 schema fragment에는 openclaw/plugin-sdk/channel-actions를 통해 export되는 일반 helper를 재사용하세요.
  • 버튼 그리드 스타일 payload용 createMessageToolButtonsSchema()
  • 구조화된 card payload용 createMessageToolCardSchema()
어떤 schema 형태가 하나의 provider에만 의미가 있다면, 이를 공유 SDK로 승격하지 말고 해당 플러그인의 자체 소스에 정의하세요.

채널 target resolve

채널 플러그인은 채널별 target 의미론을 소유해야 합니다. 공유 outbound 호스트는 일반적으로 유지하고, provider 규칙에는 messaging adapter surface를 사용하세요.
  • messaging.inferTargetChatType({ to })는 정규화된 target이 directory 조회 전에 direct, group, channel 중 무엇으로 취급되어야 하는지 결정합니다.
  • messaging.targetResolver.looksLikeId(raw, normalized)는 core에 입력이 directory 검색 대신 id 형태의 resolve로 바로 넘어가야 하는지를 알려줍니다.
  • messaging.targetResolver.resolveTarget(...)는 정규화 후 또는 directory miss 후 최종 provider 소유 resolve가 필요할 때 플러그인 fallback입니다.
  • messaging.resolveOutboundSessionRoute(...)는 target resolve 후 provider별 session route 구성을 소유합니다.
권장 분리:
  • peers/groups를 검색하기 전에 이루어져야 하는 카테고리 결정에는 inferTargetChatType 사용
  • “이를 명시적/native target id로 취급할지” 검사에는 looksLikeId 사용
  • provider별 정규화 fallback에는 resolveTarget을 사용하고, 광범위한 directory 검색에는 사용하지 않음
  • chat id, thread id, JID, handle, room id 같은 provider-native id는 일반 SDK field가 아니라 target 값이나 provider별 파라미터 안에 유지

Config 기반 directory

config에서 directory entry를 도출하는 플러그인은 그 로직을 플러그인 안에 두고, openclaw/plugin-sdk/directory-runtime의 공유 helper를 재사용해야 합니다. 이는 채널에 다음과 같은 config 기반 peer/group이 필요할 때 사용하세요.
  • allowlist 기반 DM peer
  • 설정된 channel/group map
  • account 범위의 정적 directory fallback
directory-runtime의 공유 helper는 일반 작업만 처리합니다.
  • 쿼리 필터링
  • limit 적용
  • dedupe/정규화 helper
  • ChannelDirectoryEntry[] 구성
채널별 account 검사 및 id 정규화는 플러그인 구현 안에 남겨두어야 합니다.

Provider catalog

provider 플러그인은 registerProvider({ catalog: { run(...) { ... } } })를 통해 추론용 모델 catalog를 정의할 수 있습니다. catalog.run(...)은 OpenClaw가 models.providers에 쓰는 것과 동일한 형태를 반환합니다.
  • 하나의 provider entry에 대한 { provider }
  • 여러 provider entry에 대한 { providers }
provider별 model id, base URL 기본값, auth 게이트 모델 메타데이터를 플러그인이 소유할 때 catalog를 사용하세요. catalog.order는 플러그인의 catalog가 OpenClaw의 내장 암시적 provider와 상대적으로 언제 병합되는지를 제어합니다.
  • simple: 일반 API 키 또는 env 기반 provider
  • profile: auth profile이 존재할 때 나타나는 provider
  • paired: 여러 관련 provider entry를 합성하는 provider
  • late: 다른 암시적 provider 이후의 마지막 단계
나중에 오는 provider가 key 충돌 시 이기므로, 플러그인은 같은 provider id를 가진 내장 provider entry를 의도적으로 override할 수 있습니다. 호환성:
  • discovery는 레거시 별칭으로 여전히 동작합니다
  • catalogdiscovery가 모두 등록되면 OpenClaw는 catalog를 사용합니다

읽기 전용 채널 검사

플러그인이 channel을 등록한다면, resolveAccount(...)와 함께 plugin.config.inspectAccount(cfg, accountId) 구현을 우선하세요. 이유:
  • resolveAccount(...)는 런타임 경로입니다. credential이 완전히 materialize되었다고 가정할 수 있으며, 필요한 secret이 없으면 빠르게 실패해도 됩니다.
  • openclaw status, openclaw status --all, openclaw channels status, openclaw channels resolve, doctor/config repair flow 같은 읽기 전용 command 경로는 설정 상태를 설명하기 위해 런타임 credential을 materialize할 필요가 없어야 합니다.
권장 inspectAccount(...) 동작:
  • 설명용 account 상태만 반환합니다.
  • enabledconfigured를 유지합니다.
  • 관련이 있다면 credential source/status field를 포함합니다. 예:
    • tokenSource, tokenStatus
    • botTokenSource, botTokenStatus
    • appTokenSource, appTokenStatus
    • signingSecretSource, signingSecretStatus
  • 읽기 전용 가용성을 보고하기 위해 raw token 값을 반환할 필요는 없습니다. 상태 스타일 command에는 tokenStatus: "available"(및 일치하는 source field)면 충분합니다.
  • credential이 SecretRef로 구성되었지만 현재 command 경로에서는 사용할 수 없을 때는 configured_unavailable을 사용하세요.
이렇게 하면 읽기 전용 command가 크래시하거나 account를 미구성으로 잘못 보고하는 대신, “구성되었지만 이 command 경로에서는 사용할 수 없음”을 보고할 수 있습니다.

패키지 pack

플러그인 디렉터리는 openclaw.extensions가 포함된 package.json을 가질 수 있습니다.
{
  "name": "my-pack",
  "openclaw": {
    "extensions": ["./src/safety.ts", "./src/tools.ts"],
    "setupEntry": "./src/setup-entry.ts"
  }
}
각 entry는 하나의 플러그인이 됩니다. pack이 여러 extension을 나열하면, 플러그인 id는 name/<fileBase>가 됩니다. 플러그인이 npm 의존성을 import한다면, 해당 디렉터리에 이를 설치해 node_modules를 사용할 수 있게 하세요 (npm install / pnpm install). 보안 가드레일: 모든 openclaw.extensions entry는 symlink resolve 후에도 플러그인 디렉터리 내부에 있어야 합니다. 패키지 디렉터리를 벗어나는 entry는 거부됩니다. 보안 참고: openclaw plugins install은 플러그인 의존성을 npm install --omit=dev --ignore-scripts로 설치합니다 (라이프사이클 스크립트 없음, 런타임에 dev dependency 없음). 플러그인 의존성 트리는 “순수 JS/TS” 상태를 유지하고, postinstall 빌드가 필요한 패키지는 피하세요. 선택 사항: openclaw.setupEntry는 가벼운 setup 전용 모듈을 가리킬 수 있습니다. OpenClaw가 비활성화된 채널 플러그인의 setup surface가 필요하거나, 채널 플러그인이 활성화되어 있지만 아직 구성되지 않은 경우, 전체 플러그인 entry 대신 setupEntry를 로드합니다. 이렇게 하면 메인 플러그인 entry가 tool, hook, 기타 런타임 전용 코드를 함께 연결하더라도 시작과 setup를 더 가볍게 유지할 수 있습니다. 선택 사항: openclaw.startup.deferConfiguredChannelFullLoadUntilAfterListen은 채널 플러그인이 이미 구성된 경우에도, gateway의 pre-listen startup 단계에서 동일한 setupEntry 경로를 opt-in할 수 있게 합니다. 이 옵션은 setupEntry가 gateway가 리슨을 시작하기 전에 존재해야 하는 startup surface를 완전히 커버하는 경우에만 사용하세요. 실제로 이는 setup entry가 시작 시 의존하는 모든 채널 소유 capability를 등록해야 함을 의미합니다. 예를 들면:
  • channel 등록 자체
  • gateway가 리슨하기 전에 사용 가능해야 하는 모든 HTTP route
  • 같은 시점에 존재해야 하는 모든 gateway method, tool, service
전체 entry가 여전히 필요한 startup capability를 소유한다면 이 플래그를 활성화하지 마세요. 기본 동작을 유지하고 OpenClaw가 시작 중에 전체 entry를 로드하도록 두세요. 번들 채널은 전체 채널 런타임이 로드되기 전에 core가 조회할 수 있는 setup 전용 contract-surface helper도 게시할 수 있습니다. 현재 setup promotion surface는 다음과 같습니다.
  • singleAccountKeysToMove
  • namedAccountPromotionKeys
  • resolveSingleAccountPromotionTarget(...)
core는 레거시 single-account 채널 config를 전체 플러그인 entry를 로드하지 않고 channels.<id>.accounts.*로 승격해야 할 때 이 surface를 사용합니다. 현재 Matrix가 번들 예시입니다. named account가 이미 존재하면 auth/bootstrap key만 이름 있는 승격 account로 이동하고, 항상 accounts.default를 만드는 대신 구성된 비정규 default-account key를 유지할 수 있습니다. 이러한 setup patch adapter는 번들 contract-surface discovery를 lazy하게 유지합니다. import 시점은 가볍게 유지되고, 승격 surface는 모듈 import 중 번들 채널 startup을 다시 진입하는 대신 실제 첫 사용 시에만 로드됩니다. 이러한 startup surface에 gateway RPC method가 포함된다면, 플러그인 전용 prefix를 유지하세요. core admin namespace (config.*, exec.approvals.*, wizard.*, update.*)는 예약되어 있으며, 플러그인이 더 좁은 scope를 요청하더라도 항상 operator.admin으로 resolve됩니다. 예시:
{
  "name": "@scope/my-channel",
  "openclaw": {
    "extensions": ["./index.ts"],
    "setupEntry": "./setup-entry.ts",
    "startup": {
      "deferConfiguredChannelFullLoadUntilAfterListen": true
    }
  }
}

채널 catalog 메타데이터

채널 플러그인은 openclaw.channel을 통해 setup/discovery 메타데이터를, openclaw.install을 통해 install 힌트를 광고할 수 있습니다. 이렇게 하면 core catalog를 data-free 상태로 유지할 수 있습니다. 예시:
{
  "name": "@openclaw/nextcloud-talk",
  "openclaw": {
    "extensions": ["./index.ts"],
    "channel": {
      "id": "nextcloud-talk",
      "label": "Nextcloud Talk",
      "selectionLabel": "Nextcloud Talk (self-hosted)",
      "docsPath": "/channels/nextcloud-talk",
      "docsLabel": "nextcloud-talk",
      "blurb": "Self-hosted chat via Nextcloud Talk webhook bots.",
      "order": 65,
      "aliases": ["nc-talk", "nc"]
    },
    "install": {
      "npmSpec": "@openclaw/nextcloud-talk",
      "localPath": "<bundled-plugin-local-path>",
      "defaultChoice": "npm"
    }
  }
}
최소 예시 외에 유용한 openclaw.channel 필드는 다음과 같습니다.
  • detailLabel: 더 풍부한 catalog/status surface를 위한 보조 label
  • docsLabel: docs 링크 텍스트 override
  • preferOver: 이 catalog entry가 앞서야 하는 더 낮은 우선순위의 plugin/channel id
  • selectionDocsPrefix, selectionDocsOmitLabel, selectionExtras: selection-surface 복사 제어
  • markdownCapable: outbound formatting 결정에서 이 채널이 markdown 가능함을 표시
  • showConfigured: false로 설정 시 configured-channel 목록 surface에서 채널을 숨김
  • quickstartAllowFrom: 채널을 표준 quickstart allowFrom flow에 opt-in
  • forceAccountBinding: account가 하나뿐이어도 명시적 account binding을 요구
  • preferSessionLookupForAnnounceTarget: announce target resolve 시 session 조회를 우선
OpenClaw는 외부 채널 catalog도 병합할 수 있습니다 (예: MPM registry export). 다음 위치 중 하나에 JSON 파일을 두세요.
  • ~/.openclaw/mpm/plugins.json
  • ~/.openclaw/mpm/catalog.json
  • ~/.openclaw/plugins/catalog.json
또는 OPENCLAW_PLUGIN_CATALOG_PATHS(또는 OPENCLAW_MPM_CATALOG_PATHS)를 하나 이상의 JSON 파일로 지정하세요 (쉼표/세미콜론/PATH 구분). 각 파일은 { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] } 를 포함해야 합니다. 파서는 "entries" 키의 레거시 별칭으로 "packages" 또는 "plugins"도 허용합니다.

컨텍스트 엔진 플러그인

컨텍스트 엔진 플러그인은 ingest, assemble, compaction에 대한 session context orchestration을 소유합니다. 플러그인에서 api.registerContextEngine(id, factory)로 등록한 뒤, 활성 엔진은 plugins.slots.contextEngine으로 선택하세요. 기본 context 파이프라인을 단순히 memory search나 hook으로 확장하는 것이 아니라 교체하거나 확장해야 할 때 이 기능을 사용하세요.
export default function (api) {
  api.registerContextEngine("lossless-claw", () => ({
    info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
    async ingest() {
      return { ingested: true };
    },
    async assemble({ messages }) {
      return { messages, estimatedTokens: 0 };
    },
    async compact() {
      return { ok: true, compacted: false };
    },
  }));
}
엔진이 compaction 알고리즘을 소유하지 않는다면, compact()를 구현된 상태로 두고 이를 명시적으로 위임하세요.
import { delegateCompactionToRuntime } from "openclaw/plugin-sdk/core";

export default function (api) {
  api.registerContextEngine("my-memory-engine", () => ({
    info: {
      id: "my-memory-engine",
      name: "My Memory Engine",
      ownsCompaction: false,
    },
    async ingest() {
      return { ingested: true };
    },
    async assemble({ messages }) {
      return { messages, estimatedTokens: 0 };
    },
    async compact(params) {
      return await delegateCompactionToRuntime(params);
    },
  }));
}

새 capability 추가

플러그인에 현재 API에 맞지 않는 동작이 필요하다면, 플러그인 시스템을 우회하는 비공개 직접 접근을 하지 마세요. 누락된 capability를 추가하세요. 권장 순서:
  1. core 계약 정의 core가 소유해야 하는 공유 동작을 결정합니다. 정책, fallback, config 병합, 라이프사이클, 채널 대상 의미론, 런타임 helper 형태가 여기에 포함됩니다.
  2. typed 플러그인 등록/런타임 surface 추가 OpenClawPluginApi 및/또는 api.runtime를 가장 작지만 유용한 typed capability surface로 확장합니다.
  3. core + 채널/기능 소비자 연결 채널과 기능 플러그인은 vendor 구현을 직접 import하지 말고 core를 통해 새 capability를 소비해야 합니다.
  4. vendor 구현 등록 이후 vendor 플러그인이 그 capability에 백엔드를 등록합니다.
  5. contract 커버리지 추가 시간이 지나도 소유권과 등록 형태가 명시적으로 유지되도록 테스트를 추가합니다.
이것이 OpenClaw가 한 provider의 세계관에 하드코딩되지 않으면서도 의견을 가진 구조를 유지하는 방법입니다. 구체적인 파일 체크리스트와 worked example은 Capability Cookbook를 참고하세요.

Capability 체크리스트

새 capability를 추가할 때 구현은 보통 다음 surface를 함께 수정해야 합니다.
  • src/<capability>/types.ts의 core 계약 타입
  • src/<capability>/runtime.ts의 core runner/runtime helper
  • src/plugins/types.ts의 plugin API 등록 surface
  • src/plugins/registry.ts의 plugin registry wiring
  • 기능/채널 플러그인이 이를 소비해야 한다면 src/plugins/runtime/*의 plugin 런타임 노출
  • src/test-utils/plugin-registration.ts의 capture/test helper
  • src/plugins/contracts/registry.ts의 소유권/contract assertion
  • docs/의 운영자/플러그인 문서
이 중 하나가 빠져 있다면, 일반적으로 그 capability가 아직 완전히 통합되지 않았다는 신호입니다.

Capability 템플릿

최소 패턴:
// core contract
export type VideoGenerationProviderPlugin = {
  id: string;
  label: string;
  generateVideo: (req: VideoGenerationRequest) => Promise<VideoGenerationResult>;
};

// plugin API
api.registerVideoGenerationProvider({
  id: "openai",
  label: "OpenAI",
  async generateVideo(req) {
    return await generateOpenAiVideo(req);
  },
});

// feature/channel plugin용 공유 런타임 helper
const clip = await api.runtime.videoGeneration.generate({
  prompt: "Show the robot walking through the lab.",
  cfg,
});
contract test 패턴:
expect(findVideoGenerationProviderIdsForPlugin("openai")).toEqual(["openai"]);
이렇게 하면 규칙은 단순해집니다.
  • core가 capability 계약 + orchestration을 소유합니다
  • vendor 플러그인이 vendor 구현을 소유합니다
  • 기능/채널 플러그인이 런타임 helper를 소비합니다
  • contract test가 소유권을 명시적으로 유지합니다