Created
March 18, 2026 23:53
-
-
Save jalehman/6457ca0dfc7fa8cd30acb64d6c96b2fb to your computer and use it in GitHub Desktop.
OpenClaw Plugin Architecture — How extensions go from TypeScript source to running code #pagedrop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>OpenClaw Plugin Architecture</title> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/theme/black.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/plugin/highlight/monokai.css"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --r-background-color: #0a0a0f; | |
| --r-main-font: 'Space Grotesk', system-ui, sans-serif; | |
| --r-main-font-size: 32px; | |
| --r-heading-font: 'Space Grotesk', system-ui, sans-serif; | |
| --r-code-font: 'JetBrains Mono', 'Space Mono', monospace; | |
| --r-main-color: #e2e0d8; | |
| --r-heading-color: #f5f3eb; | |
| --r-link-color: #f59e42; | |
| --r-link-color-hover: #fbbf68; | |
| --r-selection-background-color: #f59e4244; | |
| --r-heading-text-transform: none; | |
| --accent: #f59e42; | |
| --accent2: #e05252; | |
| --accent3: #42b8f5; | |
| --accent4: #6dd672; | |
| --dim: #6b6a64; | |
| --surface: #16161e; | |
| --surface2: #1e1e28; | |
| } | |
| .reveal { | |
| font-weight: 400; | |
| } | |
| .reveal h1, .reveal h2, .reveal h3 { | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| line-height: 1.15; | |
| } | |
| .reveal h1 { font-size: 2.4em; } | |
| .reveal h2 { font-size: 1.6em; margin-bottom: 0.6em; } | |
| .reveal h3 { font-size: 1.1em; color: var(--accent); margin-bottom: 0.4em; } | |
| .reveal code { | |
| font-family: var(--r-code-font); | |
| font-size: 0.85em; | |
| background: var(--surface); | |
| padding: 0.1em 0.35em; | |
| border-radius: 4px; | |
| color: var(--accent3); | |
| } | |
| .reveal pre { | |
| width: 100%; | |
| box-shadow: none; | |
| font-size: 0.52em; | |
| } | |
| .reveal pre code { | |
| background: var(--surface); | |
| border: 1px solid #2a2a36; | |
| border-radius: 8px; | |
| padding: 1em 1.2em; | |
| line-height: 1.55; | |
| max-height: 520px; | |
| color: var(--r-main-color); | |
| } | |
| .reveal table { | |
| margin: 0 auto; | |
| border-collapse: collapse; | |
| font-size: 0.72em; | |
| } | |
| .reveal table th { | |
| background: var(--surface2); | |
| color: var(--accent); | |
| font-weight: 600; | |
| padding: 0.5em 0.8em; | |
| border-bottom: 2px solid var(--accent); | |
| text-align: left; | |
| } | |
| .reveal table td { | |
| padding: 0.4em 0.8em; | |
| border-bottom: 1px solid #2a2a36; | |
| text-align: left; | |
| } | |
| .reveal table tr:last-child td { border-bottom: none; } | |
| .reveal ul, .reveal ol { | |
| display: block; | |
| text-align: left; | |
| margin-left: 0; | |
| font-size: 0.88em; | |
| line-height: 1.7; | |
| } | |
| .reveal li { margin-bottom: 0.25em; } | |
| .reveal .slides section { | |
| text-align: left; | |
| padding: 0 40px; | |
| } | |
| /* Title slide */ | |
| .title-slide { | |
| text-align: center !important; | |
| } | |
| .title-slide h1 { | |
| background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 50%, var(--accent3) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-size: 2.6em !important; | |
| } | |
| .title-slide .subtitle { | |
| color: var(--dim); | |
| font-size: 0.9em; | |
| margin-top: 0.8em; | |
| font-weight: 400; | |
| } | |
| /* Section divider slides */ | |
| .section-divider { | |
| text-align: center !important; | |
| } | |
| .section-divider h2 { | |
| font-size: 2em !important; | |
| } | |
| .section-divider .section-num { | |
| display: inline-block; | |
| color: var(--accent); | |
| font-family: var(--r-code-font); | |
| font-size: 0.6em; | |
| border: 2px solid var(--accent); | |
| border-radius: 50%; | |
| width: 1.6em; | |
| height: 1.6em; | |
| line-height: 1.5em; | |
| text-align: center; | |
| margin-bottom: 0.4em; | |
| } | |
| /* Diagram styling */ | |
| .diagram { | |
| background: var(--surface); | |
| border: 1px solid #2a2a36; | |
| border-radius: 8px; | |
| padding: 1em 1.4em; | |
| font-family: var(--r-code-font); | |
| font-size: 0.48em; | |
| line-height: 1.6; | |
| white-space: pre; | |
| overflow-x: auto; | |
| color: var(--r-main-color); | |
| } | |
| /* Highlight spans */ | |
| .hl { color: var(--accent); font-weight: 600; } | |
| .hl2 { color: var(--accent2); font-weight: 600; } | |
| .hl3 { color: var(--accent3); font-weight: 600; } | |
| .hl4 { color: var(--accent4); font-weight: 600; } | |
| .dim { color: var(--dim); } | |
| /* Badge/pill */ | |
| .pill { | |
| display: inline-block; | |
| background: var(--surface2); | |
| border: 1px solid #2a2a36; | |
| border-radius: 999px; | |
| padding: 0.15em 0.65em; | |
| font-size: 0.75em; | |
| color: var(--accent); | |
| font-family: var(--r-code-font); | |
| margin: 0 0.15em; | |
| } | |
| /* Stat bars */ | |
| .stat-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.6em; | |
| margin: 0.35em 0; | |
| font-size: 0.78em; | |
| } | |
| .stat-bar .bar { | |
| height: 24px; | |
| border-radius: 4px; | |
| transition: width 0.8s ease; | |
| } | |
| .stat-bar .label { | |
| min-width: 120px; | |
| text-align: right; | |
| font-family: var(--r-code-font); | |
| font-size: 0.85em; | |
| color: var(--dim); | |
| } | |
| .stat-bar .value { | |
| font-family: var(--r-code-font); | |
| font-size: 0.85em; | |
| } | |
| /* Two-column layout */ | |
| .columns { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 1.5em; | |
| align-items: start; | |
| } | |
| .columns > div { min-width: 0; } | |
| /* Key-file badge */ | |
| .key-file { | |
| font-family: var(--r-code-font); | |
| font-size: 0.6em; | |
| color: var(--dim); | |
| margin-bottom: 0.8em; | |
| } | |
| /* Fragment custom: fade up with color */ | |
| .reveal .fragment.pop-in { | |
| opacity: 0; | |
| transform: translateY(12px); | |
| transition: all 0.4s ease; | |
| } | |
| .reveal .fragment.pop-in.visible { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| /* Progress bar color */ | |
| .reveal .progress span { background: var(--accent); } | |
| /* Slide number */ | |
| .reveal .slide-number { color: var(--dim); font-family: var(--r-code-font); font-size: 14px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="reveal"> | |
| <div class="slides"> | |
| <!-- ============================================================ --> | |
| <!-- TITLE --> | |
| <!-- ============================================================ --> | |
| <section class="title-slide" data-transition="zoom"> | |
| <svg class="lobster" width="80" height="80" viewBox="0 0 100 100" style="margin-bottom:-0.2em;filter:drop-shadow(0 0 12px rgba(245,158,66,0.35));"> | |
| <g fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <!-- body --> | |
| <ellipse cx="50" cy="52" rx="14" ry="18" fill="var(--accent)" opacity="0.15"/> | |
| <ellipse cx="50" cy="52" rx="14" ry="18"/> | |
| <!-- tail segments --> | |
| <path d="M42 68 Q50 78 58 68" fill="var(--accent)" opacity="0.1"/> | |
| <path d="M42 68 Q50 78 58 68"/> | |
| <path d="M44 74 Q50 84 56 74"/> | |
| <path d="M40 80 L50 90 L60 80" fill="var(--accent)" opacity="0.1"/> | |
| <path d="M40 80 L50 90 L60 80"/> | |
| <!-- head --> | |
| <ellipse cx="50" cy="36" rx="10" ry="7" fill="var(--accent)" opacity="0.15"/> | |
| <ellipse cx="50" cy="36" rx="10" ry="7"/> | |
| <!-- eyes --> | |
| <circle cx="45" cy="32" r="2.5" fill="var(--accent)"/> | |
| <circle cx="55" cy="32" r="2.5" fill="var(--accent)"/> | |
| <line x1="44" y1="28" x2="42" y2="23"/> | |
| <line x1="56" y1="28" x2="58" y2="23"/> | |
| <circle cx="42" cy="22" r="1.5" fill="var(--accent)"/> | |
| <circle cx="58" cy="22" r="1.5" fill="var(--accent)"/> | |
| <!-- antennae --> | |
| <path d="M44 30 Q35 18 28 12" stroke-width="2"/> | |
| <path d="M56 30 Q65 18 72 12" stroke-width="2"/> | |
| <!-- left claw --> | |
| <path d="M36 42 Q26 36 20 40" stroke-width="2.5"/> | |
| <path d="M20 40 Q14 34 10 36" stroke-width="2.5"/> | |
| <path d="M20 40 Q16 44 12 44" stroke-width="2.5"/> | |
| <ellipse cx="11" cy="40" rx="5" ry="6" transform="rotate(-15 11 40)" fill="var(--accent2)" opacity="0.2" stroke="var(--accent2)" stroke-width="2"/> | |
| <!-- right claw --> | |
| <path d="M64 42 Q74 36 80 40" stroke-width="2.5"/> | |
| <path d="M80 40 Q86 34 90 36" stroke-width="2.5"/> | |
| <path d="M80 40 Q84 44 88 44" stroke-width="2.5"/> | |
| <ellipse cx="89" cy="40" rx="5" ry="6" transform="rotate(15 89 40)" fill="var(--accent2)" opacity="0.2" stroke="var(--accent2)" stroke-width="2"/> | |
| <!-- legs --> | |
| <path d="M38 48 L28 54" stroke-width="1.8"/> | |
| <path d="M37 54 L26 58" stroke-width="1.8"/> | |
| <path d="M38 60 L28 66" stroke-width="1.8"/> | |
| <path d="M62 48 L72 54" stroke-width="1.8"/> | |
| <path d="M63 54 L74 58" stroke-width="1.8"/> | |
| <path d="M62 60 L72 66" stroke-width="1.8"/> | |
| </g> | |
| </svg> | |
| <h1>OpenClaw<br>Plugin Architecture</h1> | |
| <p class="subtitle">How extensions go from TypeScript source to running code</p> | |
| <p style="margin-top:1.5em;"> | |
| <span class="pill">tsdown</span> | |
| <span class="pill">jiti</span> | |
| <span class="pill">Plugin Registry</span> | |
| <span class="pill">Hooks</span> | |
| <span class="pill">Runtime Proxy</span> | |
| </p> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- PIPELINE OVERVIEW --> | |
| <!-- ============================================================ --> | |
| <section data-transition="slide"> | |
| <h2>The Pipeline: Three Layers</h2> | |
| <div class="diagram">┌───────────────┐ ┌──────────┐ ┌──────────────┐ ┌────────────────┐ | |
| │ Extension │ │ tsdown │ │ Discovery │ │ Plugin │ | |
| │ Source (.ts) │───>│ Build │───>│ + Manifest │───>│ Registry │ | |
| │ │ │ (unified)│ │ Loading │ │ (in-memory) │ | |
| └───────────────┘ └──────────┘ └──────┬───────┘ └───────┬────────┘ | |
| │ │ | |
| v v | |
| ┌──────────┐ ┌────────────────┐ | |
| │ jiti │ │ Hooks, Tools, │ | |
| │ (loader) │──────>│ Channels, │ | |
| └──────────┘ │ Providers │ | |
| └────────────────┘</div> | |
| <table style="margin-top:0.8em;"> | |
| <tr><th>Layer</th><th>Tool</th><th>Role</th></tr> | |
| <tr><td><span class="hl">Build</span></td><td>tsdown</td><td>Compile all extensions + SDK into <code>dist/</code></td></tr> | |
| <tr><td><span class="hl">Load</span></td><td>jiti</td><td>Import plugin modules at runtime (.js native or .ts transpiled)</td></tr> | |
| <tr><td><span class="hl">Wire</span></td><td>Plugin Registry</td><td>Collect hooks, tools, channels, providers for the core</td></tr> | |
| </table> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 1: BUILD --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">1</div> | |
| <h2>Build: tsdown</h2> | |
| <p class="dim" style="font-size:0.7em;">Unified Entry Graph</p> | |
| </section> | |
| <section> | |
| <h2>Single Build Graph</h2> | |
| <p class="key-file">tsdown.config.ts</p> | |
| <p>One root config compiles <strong>everything</strong> in a single invocation:</p> | |
| <pre><code class="language-plaintext">tsdown entry graph | |
| ├── Core entries (index.ts, entry.ts, CLI, infra, channels) | |
| ├── Plugin-SDK subpaths (122 exports) | |
| ├── Bundled extension entries (extensions/*/package.json) | |
| └── Bundled hooks (src/hooks/bundled/*/handler.ts)</code></pre> | |
| <h3 style="margin-top:0.8em;">Why a single graph?</h3> | |
| <ul> | |
| <li>Runtime singletons (config, registry, hook runner) emitted <strong>once</strong></li> | |
| <li>Separate builds → duplicate module instances → broken shared state</li> | |
| <li><code>import "openclaw/plugin-sdk"</code> must resolve to the same graph</li> | |
| </ul> | |
| </section> | |
| <section> | |
| <h2>Extension Entry Declaration</h2> | |
| <p>Extensions declare entries in <code>package.json</code>, not in a build config:</p> | |
| <pre><code class="language-json">{ | |
| "name": "@openclaw/discord", | |
| "openclaw": { | |
| "extensions": ["./index.ts"], | |
| "setupEntry": "./setup-entry.ts" | |
| } | |
| }</code></pre> | |
| <ul> | |
| <li><code class="hl">extensions[]</code> — runtime entry (full plugin with handlers)</li> | |
| <li><code class="hl">setupEntry</code> — optional pre-listen setup (config UI, onboarding)</li> | |
| </ul> | |
| <p style="font-size:0.78em; color:var(--dim); margin-top:0.6em;">tsdown discovers these dynamically at build time via <code>listBundledPluginBuildEntries()</code></p> | |
| </section> | |
| <section> | |
| <h2>Build Output: Three Shapes</h2> | |
| <p style="font-size:0.82em;">Each extension → single <code>index.js</code> (tree-shaken, no code-splitting)</p> | |
| <table> | |
| <tr><th>Shape</th><th>JS files</th><th>Examples</th></tr> | |
| <tr> | |
| <td><span class="hl4">Provider / utility</span></td> | |
| <td><code>index.js</code> only</td> | |
| <td>anthropic (16 KB), openai (22 KB), voice-call (179 KB)</td> | |
| </tr> | |
| <tr> | |
| <td><span class="hl3">Channel plugin</span></td> | |
| <td><code>index.js</code> + <code>setup-entry.js</code></td> | |
| <td>discord, telegram, slack, signal</td> | |
| </tr> | |
| <tr> | |
| <td><span class="hl2">Optional cluster</span></td> | |
| <td>0 JS files (metadata only)</td> | |
| <td>matrix, msteams, nostr, memory-lancedb</td> | |
| </tr> | |
| </table> | |
| <p style="font-size:0.72em; color:var(--dim); margin-top:0.6em;"> | |
| Optional clusters defined in <code>scripts/lib/optional-bundled-clusters.mjs</code><br> | |
| Excluded from default build via <code>shouldBuildBundledCluster()</code><br> | |
| Compile only when <code>OPENCLAW_INCLUDE_OPTIONAL_BUNDLED=1</code> | |
| </p> | |
| </section> | |
| <section> | |
| <h2>Disk Layout</h2> | |
| <pre><code class="language-plaintext">dist/ | |
| ├── plugin-sdk/ | |
| │ ├── root-alias.cjs ← CJS facade (Proxy) | |
| │ ├── compat.js ← monolithic SDK bundle | |
| │ ├── core.js ← scoped subpath | |
| │ ├── discord.js ← channel-specific SDK | |
| │ └── ... (122 subpaths) | |
| └── extensions/ | |
| ├── discord/ ← channel plugin (2 JS) | |
| │ ├── index.js ← 5.7 KB | |
| │ ├── setup-entry.js ← 5.7 KB | |
| │ ├── node_modules/ ← symlink → source | |
| │ ├── openclaw.plugin.json | |
| │ └── package.json ← rewritten (.ts → .js) | |
| ├── voice-call/ ← utility (1 JS, 179 KB) | |
| │ └── index.js | |
| └── matrix/ ← optional cluster (0 JS) | |
| ├── openclaw.plugin.json | |
| └── package.json</code></pre> | |
| </section> | |
| <section> | |
| <h2>Post-Build Staging</h2> | |
| <p class="key-file">scripts/runtime-postbuild.mjs</p> | |
| <ol> | |
| <li><strong>Copy <code>root-alias.cjs</code></strong> from <code>src/</code> → <code>dist/</code> <span class="dim">(plain CJS, no transpilation)</span></li> | |
| <li><strong>Rewrite extension <code>package.json</code></strong>: flip <code>.ts</code> → <code>.js</code> refs</li> | |
| <li><strong>Create <code>dist-runtime/</code></strong> overlay with re-export wrappers + symlinks</li> | |
| </ol> | |
| <pre><code class="language-plaintext">dist-runtime/extensions/discord/ | |
| ├── index.js ← re-export → dist/extensions/discord/index.js | |
| ├── package.json | |
| ├── openclaw.plugin.json | |
| └── node_modules/ ← symlink → extensions/discord/node_modules/</code></pre> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 2: DISCOVERY --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">2</div> | |
| <h2>Discovery</h2> | |
| <p class="dim" style="font-size:0.7em;">Finding Plugins at Runtime</p> | |
| </section> | |
| <section> | |
| <h2>Four Plugin Origins</h2> | |
| <p class="key-file">src/plugins/discovery.ts</p> | |
| <table> | |
| <tr><th>Origin</th><th>Location</th><th>Priority</th></tr> | |
| <tr><td><span class="hl">config</span></td><td><code>plugins.loadPaths[]</code> in user config</td><td>Highest</td></tr> | |
| <tr><td><span class="hl">workspace</span></td><td><code>extensions/</code> in project root</td><td>↑</td></tr> | |
| <tr><td><span class="hl">global</span></td><td><code>~/.openclaw/extensions/</code></td><td>↑</td></tr> | |
| <tr><td><span class="hl">bundled</span></td><td><code>~/.openclaw/stock/extensions/</code></td><td>Lowest</td></tr> | |
| </table> | |
| <div style="margin-top:0.8em; font-size:0.82em;"> | |
| <h3>Plugin Manifest</h3> | |
| <pre><code class="language-json">{ | |
| "id": "discord", | |
| "channels": ["discord"], | |
| "providers": [], | |
| "configSchema": {} | |
| }</code></pre> | |
| <p class="dim" style="font-size:0.85em;">Deduplicates by plugin ID — higher-origin wins. 1-second cache.</p> | |
| </div> | |
| </section> | |
| <section> | |
| <h2>Path Safety</h2> | |
| <p class="key-file">src/plugins/path-safety.ts</p> | |
| <p>Before loading any plugin file, the system validates:</p> | |
| <ul> | |
| <li><span class="hl2">Symlink rejection</span> — configurable per origin</li> | |
| <li><span class="hl2">World-writable directory</span> detection</li> | |
| <li><span class="hl2">Ownership validation</span> (Unix)</li> | |
| <li><span class="hl2">Escape detection</span> — realpath must stay inside plugin root</li> | |
| </ul> | |
| <p style="font-size:0.78em; color:var(--dim); margin-top:0.8em;">Prevents malicious plugin injection via crafted symlinks or path traversal</p> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 3: DEPENDENCIES --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">3</div> | |
| <h2>Dependency Handling</h2> | |
| <p class="dim" style="font-size:0.7em;">Three strategies for plugin dependencies</p> | |
| </section> | |
| <section> | |
| <h2>Dependency Strategies</h2> | |
| <table> | |
| <tr><th>Extension type</th><th>Dependencies</th><th>Resolution</th></tr> | |
| <tr> | |
| <td><span class="hl4">Bundled (no deps)</span></td> | |
| <td>Core only, shared chunks</td> | |
| <td><code>dist/*.js</code> relative imports</td> | |
| </tr> | |
| <tr> | |
| <td><span class="hl3">Bundled (with deps)</span></td> | |
| <td>Own + root <code>package.json</code></td> | |
| <td>External bare-specifier → root <code>node_modules/</code></td> | |
| </tr> | |
| <tr> | |
| <td><span class="hl2">Optional cluster</span></td> | |
| <td>Own <code>package.json</code></td> | |
| <td>Isolated <code>node_modules/</code> via <code>npm install --omit=dev</code></td> | |
| </tr> | |
| <tr> | |
| <td><span class="hl">Plugin-SDK subpaths</span></td> | |
| <td>Never bundled</td> | |
| <td>jiti alias map → <code>dist/plugin-sdk/*.js</code></td> | |
| </tr> | |
| </table> | |
| <div style="margin-top:0.8em;"> | |
| <h3>Shared chunks</h3> | |
| <p style="font-size:0.78em;">tsdown splits shared code into <strong>~668 chunks</strong> in <code>dist/</code>. Each plugin index.js is thin — imports from chunks via relative paths.</p> | |
| </div> | |
| </section> | |
| <section> | |
| <h2>External Deps in Practice</h2> | |
| <pre><code class="language-javascript">// Built dist/extensions/voice-call/index.js | |
| // deps kept as external bare-specifier imports: | |
| import { z } from "zod"; | |
| import WebSocket from "ws"; | |
| import { Type } from "@sinclair/typebox";</code></pre> | |
| <ul> | |
| <li>Deps declared in both extension + root <code>package.json</code></li> | |
| <li>tsdown treats root-level deps as external — not bundled</li> | |
| <li>At runtime: resolve from root <code>node_modules/</code></li> | |
| <li>Only <code>@lancedb/lancedb</code> explicitly marked <code>neverBundle</code> <span class="dim">(native bindings)</span></li> | |
| </ul> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 4: JITI --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">4</div> | |
| <h2>Loading: jiti</h2> | |
| <p class="dim" style="font-size:0.7em;">The Module Bridge</p> | |
| </section> | |
| <section> | |
| <h2>What is jiti?</h2> | |
| <p class="key-file">src/plugins/loader.ts</p> | |
| <p><a href="https://github.com/unjs/jiti" style="color:var(--accent3)">jiti</a> (Just-In-Time Import) — runtime module loader from the <a href="https://unjs.io" style="color:var(--accent3)">unjs</a> ecosystem.</p> | |
| <div style="margin-top:0.6em;"> | |
| <h3>The problem it solves</h3> | |
| <p style="font-size:0.82em;">Node.js cannot natively <code>import()</code> a <code>.ts</code> file. A plugin system needs a loader that:</p> | |
| <ol style="font-size:0.82em;"> | |
| <li><span class="hl">Transpiles <code>.ts</code></span> on-the-fly <span class="dim">(dev mode)</span></li> | |
| <li><span class="hl">Delegates to native ESM</span> for pre-built <code>.js</code> <span class="dim">(production)</span></li> | |
| <li><span class="hl">Resolves custom import paths</span> like <code>openclaw/plugin-sdk/core</code></li> | |
| <li><span class="hl">Handles CJS/ESM interop</span> since <code>root-alias.cjs</code> is CommonJS</li> | |
| </ol> | |
| </div> | |
| </section> | |
| <section> | |
| <h2>jiti Configuration</h2> | |
| <table> | |
| <tr><th>Option</th><th>Value</th><th>Purpose</th></tr> | |
| <tr><td><code>tryNative</code></td><td><code>true</code></td><td>Use Node's native ESM for <code>.js</code></td></tr> | |
| <tr><td><code>interopDefault</code></td><td><code>true</code></td><td>Unwrap CJS <code>module.exports</code> as ESM default</td></tr> | |
| <tr><td><code>alias</code></td><td><code>{...}</code></td><td>Map 122 plugin-SDK import paths</td></tr> | |
| </table> | |
| <pre style="margin-top:0.8em;"><code class="language-typescript">createJiti(modulePath, { | |
| interopDefault: true, | |
| tryNative: true, | |
| extensions: [".ts", ".tsx", ".mts", ".cts", | |
| ".js", ".mjs", ".cjs", ".json"], | |
| alias: { | |
| "openclaw/plugin-sdk": "dist/plugin-sdk/root-alias.cjs", | |
| "openclaw/plugin-sdk/core": "dist/plugin-sdk/core.js", | |
| "openclaw/plugin-sdk/discord": "dist/plugin-sdk/discord.js", | |
| // ... 122 scoped subpaths total | |
| } | |
| });</code></pre> | |
| </section> | |
| <section> | |
| <h2>How jiti Loads a Plugin</h2> | |
| <div class="diagram">Plugin entry: extensions/discord/index.ts (dev) | |
| or: dist/extensions/discord/index.js (prod) | |
| │ | |
| v | |
| createJiti(modulePath, options) | |
| │ | |
| ├── File is .js / .mjs / .cjs ? | |
| │ └── tryNative: true → <span class="hl4">Node native ESM</span> (fast, zero transpilation) | |
| │ | |
| └── File is .ts / .tsx ? | |
| └── <span class="hl">jiti transpiles via esbuild</span> → evaluates result</div> | |
| <div style="margin-top:0.8em;"> | |
| <h3>Alias Resolution Strategy</h3> | |
| <div class="diagram" style="font-size:0.52em;"><span class="hl4">Production</span> (running from dist/) → try dist/*.js first, fall back to src/*.ts | |
| <span class="hl3">Development</span> (running from src/) → try src/*.ts first, fall back to dist/*.js</div> | |
| </div> | |
| </section> | |
| <section> | |
| <h2>The root-alias.cjs Proxy</h2> | |
| <p class="key-file">src/plugin-sdk/root-alias.cjs</p> | |
| <div class="diagram"><span class="dim">import { definePlugin } from "openclaw/plugin-sdk"</span> | |
| │ | |
| v | |
| root-alias.cjs (<span class="hl">Proxy</span>) | |
| ┌───────────────────────┐ | |
| │ <span class="hl4">Fast exports:</span> │──> emptyPluginConfigSchema | |
| │ (no lazy load) │ resolveControlCommandGate | |
| ├───────────────────────┤ | |
| │ <span class="hl3">All other exports:</span> │──> Lazy-loads compat.js | |
| │ (Proxy get trap) │ on first property access | |
| └───────────────────────┘</div> | |
| <p style="font-size:0.78em; margin-top:0.6em;">Frequently-used helpers available immediately. Full SDK (type builders, channel helpers) defers until first access.</p> | |
| </section> | |
| <section> | |
| <h2>Why Not Alternatives?</h2> | |
| <table> | |
| <tr><th>Alternative</th><th>Problem</th></tr> | |
| <tr><td>Node <code>--loader</code> hooks</td><td>Process-global, async-only — affects all imports</td></tr> | |
| <tr><td><code>tsx</code></td><td>Spawns child process — jiti runs in-process, synchronous</td></tr> | |
| <tr><td><code>esbuild</code> alone</td><td>Requires a build step — jiti wraps it on-demand</td></tr> | |
| <tr><td>Custom resolver</td><td>jiti's alias map is simpler for 122 subpaths</td></tr> | |
| </table> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 5: REGISTRATION --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">5</div> | |
| <h2>Registration</h2> | |
| <p class="dim" style="font-size:0.7em;">Building the Plugin Registry</p> | |
| </section> | |
| <section> | |
| <h2>The Load Sequence</h2> | |
| <p class="key-file">src/plugins/registry.ts</p> | |
| <div class="diagram">loadOpenClawPlugins(options) | |
| │ | |
| ├── 1. Check cache (<span class="hl3">LRU, 128 entries</span>) | |
| │ | |
| ├── 2. <span class="hl">discoverOpenClawPlugins()</span> → PluginCandidate[] | |
| │ | |
| ├── 3. <span class="hl">loadPluginManifestRegistry()</span> → manifests + metadata | |
| │ | |
| ├── 4. <span class="hl">createPluginRegistry()</span> → empty registry | |
| │ | |
| ├── 5. For each candidate (sorted by origin rank): | |
| │ ├── Validate enable state (config allow/deny) | |
| │ ├── Validate config schema | |
| │ ├── Path safety check | |
| │ ├── Load module via <span class="hl3">jiti</span> | |
| │ ├── Create PluginApi wrapper | |
| │ └── Call <span class="hl2">register(api)</span> | |
| │ | |
| ├── 6. <span class="hl">setActivePluginRegistry()</span> [global Symbol] | |
| │ | |
| └── 7. Cache registry in LRU map</div> | |
| </section> | |
| <section> | |
| <h2>What Plugins Register</h2> | |
| <pre><code class="language-typescript">export function register(api: OpenClawPluginApi) { | |
| api.registerChannel({ ... }); | |
| api.registerHook("before_agent_start", async (event) => { | |
| return { systemContext: "Extra instructions..." }; | |
| }); | |
| api.registerTool((ctx) => ({ | |
| name: "discord-lookup", | |
| description: "Look up a Discord user", | |
| inputSchema: { ... }, | |
| handler: async (input) => { ... } | |
| })); | |
| api.registerCommand({ name: "ping", ... }); | |
| api.registerHttpRoute({ method: "POST", path: "/webhook", ... }); | |
| api.registerProvider({ ... }); | |
| api.registerService({ ... }); | |
| }</code></pre> | |
| </section> | |
| <section> | |
| <h2>The PluginRegistry Shape</h2> | |
| <pre><code class="language-typescript">type PluginRegistry = { | |
| plugins: PluginRecord[]; | |
| tools: PluginToolRegistration[]; | |
| hooks: Map<PluginHookName, HookEntry[]>; | |
| channels: PluginChannelRegistration[]; | |
| providers: ProviderPlugin[]; | |
| speechProviders: SpeechProviderPlugin[]; | |
| imageProviders: ImageGenerationProviderPlugin[]; | |
| mediaProviders: MediaUnderstandingProviderPlugin[]; | |
| webSearchProviders: WebSearchProviderPlugin[]; | |
| commands: OpenClawPluginCommandDefinition[]; | |
| services: OpenClawPluginService[]; | |
| httpRoutes: OpenClawPluginHttpRouteMatch[]; | |
| gatewayMethods: Record<string, GatewayRequestHandler>; | |
| diagnostics: PluginDiagnostic[]; | |
| };</code></pre> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 6: RUNTIME --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">6</div> | |
| <h2>Runtime</h2> | |
| <p class="dim" style="font-size:0.7em;">The PluginRuntime API</p> | |
| </section> | |
| <section> | |
| <h2>Lazy PluginRuntime Proxy</h2> | |
| <p class="key-file">src/plugins/runtime/index.ts</p> | |
| <div class="columns"> | |
| <div> | |
| <div class="diagram" style="font-size:0.44em;">PluginRuntime (<span class="hl">Proxy</span>) | |
| ├── version | |
| ├── config | |
| ├── agent | |
| ├── subagent | |
| ├── system | |
| ├── media | |
| ├── tts / stt | |
| ├── tools | |
| ├── channel | |
| ├── events | |
| ├── logging | |
| ├── state | |
| └── modelAuth</div> | |
| </div> | |
| <div style="font-size:0.82em;"> | |
| <h3>Pay only for what you use</h3> | |
| <p>Properties materialize <strong>only on first access</strong>.</p> | |
| <p style="margin-top:0.5em;">If a plugin never touches <code>runtime.media</code>, the media subsystem never initializes.</p> | |
| <h3 style="margin-top:0.8em;">Subagent Late-Binding</h3> | |
| <p><code>runtime.subagent</code> throws outside a gateway request. Binds via:</p> | |
| <pre style="font-size:0.75em;"><code class="language-typescript">Symbol.for("openclaw.gatewaySubagent")</code></pre> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 7: HOOKS --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">7</div> | |
| <h2>Hooks</h2> | |
| <p class="dim" style="font-size:0.7em;">The Event Pipeline</p> | |
| </section> | |
| <section> | |
| <h2>Hook Lifecycle</h2> | |
| <p class="key-file">src/plugins/hooks.ts</p> | |
| <div class="diagram">Inbound message arrives | |
| │ | |
| v | |
| ┌─ <span class="hl">message_received</span> ──────────── (observe) | |
| │ | |
| ├─ <span class="hl">before_agent_start</span> ─────── (inject system context) | |
| │ | |
| ├─ <span class="hl">before_model_resolve</span> ───── (override LLM selection) | |
| │ | |
| ├─ <span class="hl">before_prompt_build</span> ────── (modify prompt assembly) | |
| │ | |
| ├─ <span class="hl3">llm_input</span> ─────────────────── (observe request to LLM) | |
| │ | |
| ├─ <span class="hl2">before_tool_call</span> ────────── (intercept/modify tools) | |
| │ └─ <span class="hl2">after_tool_call</span> ─────── (observe tool results) | |
| │ | |
| ├─ <span class="hl3">llm_output</span> ────────────────── (observe LLM response) | |
| │ | |
| ├─ <span class="hl4">message_sending</span> ─────────── (intercept outbound) | |
| │ | |
| └─ <span class="hl4">message_sent</span> ──────────────── (observe delivery)</div> | |
| </section> | |
| <section> | |
| <h2>Hook Execution Model</h2> | |
| <pre><code class="language-typescript">// Registration | |
| api.registerHook("before_agent_start", handler, { priority: 10 }); | |
| // Execution (internal) | |
| const results = await runPluginHooks( | |
| registry, "before_agent_start", event, { | |
| mergeStrategy: "accumulate", | |
| errorHandling: "catch-log", | |
| } | |
| );</code></pre> | |
| <table style="margin-top:0.8em;"> | |
| <tr><th>Property</th><th>Behavior</th></tr> | |
| <tr><td><span class="hl">Priority ordering</span></td><td>Higher priority hooks execute first</td></tr> | |
| <tr><td><span class="hl3">Merge strategies</span></td><td><code>accumulate</code> (concat), <code>last-wins</code>, <code>first-wins</code></td></tr> | |
| <tr><td><span class="hl2">Error isolation</span></td><td>catch + log — one plugin cannot crash others</td></tr> | |
| <tr><td><span class="hl4">Permission gates</span></td><td><code>allowPromptInjection</code> restricts mutations</td></tr> | |
| </table> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 8: GLOBAL STATE --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">8</div> | |
| <h2>Global State</h2> | |
| <p class="dim" style="font-size:0.7em;">Symbol.for across CJS/ESM</p> | |
| </section> | |
| <section> | |
| <h2>Process-Global Registry</h2> | |
| <p class="key-file">src/plugins/runtime.ts</p> | |
| <pre><code class="language-typescript">const REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState"); | |
| // State shape | |
| { | |
| registry: PluginRegistry | null, | |
| httpRouteRegistry: PluginRegistry | null, | |
| version: number // incremented on changes | |
| }</code></pre> | |
| <ul style="margin-top:0.6em;"> | |
| <li><code>setActivePluginRegistry()</code> — swap + notify observers</li> | |
| <li><code>getActivePluginRegistry()</code> — read current</li> | |
| <li><code>pinActivePluginHttpRouteRegistry()</code> — lock routes at gateway startup</li> | |
| <li>Version number detects changes without diffing</li> | |
| </ul> | |
| <p style="font-size:0.78em; color:var(--dim); margin-top:0.6em;"> | |
| <code>Symbol.for</code> works across CJS + ESM module graph copies — the only reliable cross-boundary global in Node.js | |
| </p> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 9: COMPLETE FLOW --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">9</div> | |
| <h2>Complete Data Flow</h2> | |
| </section> | |
| <section> | |
| <h2>Build Time</h2> | |
| <div class="diagram" style="font-size:0.44em;"><span class="hl">═══════════════════════ BUILD TIME ═══════════════════════</span> | |
| extensions/discord/index.ts | |
| │ | |
| v | |
| tsdown.config.ts (unified graph) | |
| │ | |
| ├──> dist/extensions/discord/index.js <span class="dim">← tree-shaken bundle</span> | |
| ├──> dist/plugin-sdk/*.js <span class="dim">← 122 subpaths</span> | |
| └──> dist/plugin-sdk/root-alias.cjs <span class="dim">← CJS Proxy facade</span> | |
| │ | |
| scripts/runtime-postbuild.mjs | |
| │ | |
| ├──> dist-runtime/ <span class="dim">← re-export wrappers</span> | |
| └──> rewritten package.json <span class="dim">← .ts → .js + symlinks</span></div> | |
| </section> | |
| <section> | |
| <h2>Runtime</h2> | |
| <div class="diagram" style="font-size:0.42em;"><span class="hl3">═════════════════════════ RUNTIME ════════════════════════</span> | |
| loadOpenClawPlugins() | |
| │ | |
| ├──> discoverOpenClawPlugins() | |
| │ scan <span class="hl">bundled</span> / <span class="hl">global</span> / <span class="hl">workspace</span> / <span class="hl">config</span> dirs | |
| │ → PluginCandidate[] | |
| │ | |
| ├──> loadPluginManifestRegistry() | |
| │ read openclaw.plugin.json per candidate | |
| │ → manifests + dedup + cache | |
| │ | |
| ├──> For each enabled plugin: | |
| │ ┌──────────────────────────────────────┐ | |
| │ │ createJiti(alias: { ... }) │ | |
| │ │ │ │ | |
| │ │ v │ | |
| │ │ jiti(extensionEntry) │ | |
| │ │ │ │ | |
| │ │ v │ | |
| │ │ module.<span class="hl2">register(api)</span> │ | |
| │ │ api.registerHook(...) │ | |
| │ │ api.registerTool(...) │ | |
| │ │ api.registerChannel(...) │ | |
| │ └──────────────────────────────────────┘ | |
| │ | |
| ├──> <span class="hl4">setActivePluginRegistry(registry)</span> | |
| │ Symbol.for("openclaw.pluginRegistryState") | |
| │ | |
| └──> Cache in LRU (128 entries) | |
| ── Request arrives ── | |
| │ | |
| ├──> runPluginHooks("message_received", ...) | |
| ├──> runPluginHooks("before_agent_start", ...) | |
| ├──> resolvePluginTools(context) | |
| └──> ... (full hook lifecycle)</div> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 10: STATISTICS --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">10</div> | |
| <h2>Codebase Statistics</h2> | |
| <p class="dim" style="font-size:0.7em;">HEAD vs v2026.3.12 (March 13)</p> | |
| </section> | |
| <section> | |
| <h2>Code Distribution</h2> | |
| <div style="margin-bottom:1em;"> | |
| <p style="font-size:0.72em; color:var(--dim); margin-bottom:0.6em;">v2026.3.12 — 5 days ago</p> | |
| <div class="stat-bar"> | |
| <span class="label">Core</span> | |
| <div class="bar" style="width:400px; background:var(--accent3);"></div> | |
| <span class="value" style="color:var(--accent3);">517K LOC <span class="dim">(84%)</span></span> | |
| </div> | |
| <div class="stat-bar"> | |
| <span class="label">Extensions</span> | |
| <div class="bar" style="width:77px; background:var(--accent);"></div> | |
| <span class="value" style="color:var(--accent);">100K LOC <span class="dim">(16%)</span></span> | |
| </div> | |
| </div> | |
| <div> | |
| <p style="font-size:0.72em; color:var(--dim); margin-bottom:0.6em;">Current HEAD</p> | |
| <div class="stat-bar"> | |
| <span class="label">Core</span> | |
| <div class="bar" style="width:340px; background:var(--accent3);"></div> | |
| <span class="value" style="color:var(--accent3);">480K LOC <span class="dim">(71%)</span></span> | |
| </div> | |
| <div class="stat-bar"> | |
| <span class="label">Extensions</span> | |
| <div class="bar" style="width:140px; background:var(--accent);"></div> | |
| <span class="value" style="color:var(--accent);">197K LOC <span class="dim">(29%)</span></span> | |
| </div> | |
| </div> | |
| <p style="font-size:0.78em; margin-top:0.8em;">Extensions: <span class="hl">42 → 76</span> (+34). Core shrank 7% as code migrated to plugins.</p> | |
| </section> | |
| <section> | |
| <h2>Plugin Infrastructure Growth</h2> | |
| <table> | |
| <tr><th>Component</th><th>v2026.3.12</th><th>Current</th><th>Delta</th></tr> | |
| <tr> | |
| <td>Plugin-SDK</td> | |
| <td>6,410 LOC</td> | |
| <td>8,993 LOC</td> | |
| <td><span class="hl4">+40%</span></td> | |
| </tr> | |
| <tr> | |
| <td>Plugin subsystem</td> | |
| <td>9,278 LOC</td> | |
| <td>23,297 LOC</td> | |
| <td><span class="hl2">+151%</span></td> | |
| </tr> | |
| <tr> | |
| <td><strong>Total infra</strong></td> | |
| <td>15,688 LOC</td> | |
| <td>32,290 LOC</td> | |
| <td><span class="hl">+106%</span></td> | |
| </tr> | |
| </table> | |
| <div style="margin-top:1em;"> | |
| <h3>Test Coverage</h3> | |
| <table> | |
| <tr><th>Area</th><th>v2026.3.12</th><th>Current</th><th>Delta</th></tr> | |
| <tr><td>Core tests</td><td>432K LOC</td><td>426K LOC</td><td><span class="dim">−1%</span></td></tr> | |
| <tr><td>Extension tests</td><td>54K LOC</td><td>129K LOC</td><td><span class="hl4">+139%</span></td></tr> | |
| <tr><td><strong>Total</strong></td><td>486K LOC</td><td>555K LOC</td><td><span class="hl">+14%</span></td></tr> | |
| </table> | |
| </div> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 11: DESIGN DECISIONS --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">11</div> | |
| <h2>Key Design Decisions</h2> | |
| </section> | |
| <section> | |
| <h2>Architecture Choices</h2> | |
| <table style="font-size:0.68em;"> | |
| <tr><th>Decision</th><th>Rationale</th></tr> | |
| <tr><td>Single tsdown graph</td><td>Prevents duplicate singletons (config, registry)</td></tr> | |
| <tr><td>No per-extension build config</td><td>Entries in <code>package.json</code>; root discovers dynamically</td></tr> | |
| <tr><td>jiti with <code>tryNative: true</code></td><td>Native <code>.js</code> in prod; <code>.ts</code> in dev</td></tr> | |
| <tr><td>Proxy-based lazy runtime</td><td>Pay only for subsystems you use</td></tr> | |
| <tr><td>root-alias.cjs Proxy</td><td>Fast exports inline; heavy SDK deferred</td></tr> | |
| <tr><td><code>dist-runtime/</code> overlay</td><td>Canonical module resolution + symlinked <code>node_modules</code></td></tr> | |
| <tr><td><code>Symbol.for</code> global state</td><td>Works across CJS + ESM boundary</td></tr> | |
| <tr><td>1s discovery cache</td><td>Collapses bursty reloads at startup</td></tr> | |
| <tr><td>LRU registry cache (128)</td><td>Hot reloads reuse; old entries evict</td></tr> | |
| <tr><td>Path safety before load</td><td>Symlink rejection + ownership checks</td></tr> | |
| </table> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- SECTION 12: AUTHOR REFERENCE --> | |
| <!-- ============================================================ --> | |
| <section class="section-divider" data-transition="zoom"> | |
| <div class="section-num">12</div> | |
| <h2>Extension Author</h2> | |
| <p class="dim" style="font-size:0.7em;">Quick Reference</p> | |
| </section> | |
| <section> | |
| <h2>Minimal Extension</h2> | |
| <div class="columns"> | |
| <div> | |
| <pre><code class="language-plaintext">my-extension/ | |
| ├── package.json | |
| ├── openclaw.plugin.json | |
| └── index.ts</code></pre> | |
| <pre><code class="language-json">{ | |
| "name": "@openclaw/my-extension", | |
| "type": "module", | |
| "openclaw": { | |
| "extensions": ["./index.ts"] | |
| } | |
| }</code></pre> | |
| </div> | |
| <div> | |
| <pre><code class="language-typescript">import type { | |
| OpenClawPluginApi | |
| } from "openclaw/plugin-sdk"; | |
| export function register( | |
| api: OpenClawPluginApi | |
| ) { | |
| api.registerHook( | |
| "before_agent_start", | |
| async (event) => { | |
| return { | |
| systemContext: | |
| "Hello from my-extension" | |
| }; | |
| } | |
| ); | |
| }</code></pre> | |
| </div> | |
| </div> | |
| </section> | |
| <section> | |
| <h2>Available api.register*() Methods</h2> | |
| <table style="font-size:0.65em;"> | |
| <tr><th>Method</th><th>Registers</th></tr> | |
| <tr><td><code>registerHook(name, handler, opts?)</code></td><td>Lifecycle hook handler</td></tr> | |
| <tr><td><code>registerTool(factory)</code></td><td>Agent tool (created per request)</td></tr> | |
| <tr><td><code>registerChannel(channel)</code></td><td>Messaging channel</td></tr> | |
| <tr><td><code>registerProvider(provider)</code></td><td>LLM provider</td></tr> | |
| <tr><td><code>registerSpeechProvider(provider)</code></td><td>TTS/STT provider</td></tr> | |
| <tr><td><code>registerImageProvider(provider)</code></td><td>Image generation provider</td></tr> | |
| <tr><td><code>registerMediaProvider(provider)</code></td><td>Media understanding provider</td></tr> | |
| <tr><td><code>registerWebSearchProvider(provider)</code></td><td>Web search provider</td></tr> | |
| <tr><td><code>registerCommand(command)</code></td><td>CLI/chat command</td></tr> | |
| <tr><td><code>registerService(service)</code></td><td>Background service</td></tr> | |
| <tr><td><code>registerHttpRoute(route)</code></td><td>HTTP endpoint on gateway</td></tr> | |
| <tr><td><code>registerGatewayMethod(name, handler)</code></td><td>Gateway RPC method</td></tr> | |
| </table> | |
| </section> | |
| <!-- ============================================================ --> | |
| <!-- END --> | |
| <!-- ============================================================ --> | |
| <section class="title-slide" data-transition="zoom"> | |
| <h1>OpenClaw<br>Plugin Architecture</h1> | |
| <p class="subtitle" style="margin-top:1em;"> | |
| <span class="pill">tsdown</span> builds it · | |
| <span class="pill">jiti</span> loads it · | |
| <span class="pill">Registry</span> wires it | |
| </p> | |
| <p style="font-size:0.55em; color:var(--dim); margin-top:2em;"> | |
| docs/architecture/plugin-interface.md · March 2026 | |
| </p> | |
| </section> | |
| </div><!-- .slides --> | |
| </div><!-- .reveal --> | |
| <script src="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/plugin/highlight/highlight.js"></script> | |
| <script> | |
| Reveal.initialize({ | |
| hash: true, | |
| slideNumber: 'c/t', | |
| width: 1280, | |
| height: 720, | |
| margin: 0.06, | |
| center: false, | |
| transition: 'slide', | |
| transitionSpeed: 'fast', | |
| backgroundTransition: 'fade', | |
| plugins: [RevealHighlight], | |
| highlight: { | |
| beforeHighlight: (hljs) => {} | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment