Skip to content

feat: add OpenClaw plugin for seamless maple-proxy integration#13

Merged
AnthonyRonning merged 29 commits intomasterfrom
feat/openclaw-plugin
Feb 17, 2026
Merged

feat: add OpenClaw plugin for seamless maple-proxy integration#13
AnthonyRonning merged 29 commits intomasterfrom
feat/openclaw-plugin

Conversation

@AnthonyRonning
Copy link
Contributor

@AnthonyRonning AnthonyRonning commented Feb 15, 2026

Summary

Adds an OpenClaw plugin (@opensecret/maple-proxy-openclaw-plugin) that automatically downloads, manages, and runs the maple-proxy binary as a background service within OpenClaw. Users install the plugin, provide an API key, and get immediate access to Maple's TEE-backed AI models -- no manual setup needed.

Closes #12

What's included

openclaw-plugin/ directory (new)

  • index.ts -- Plugin entry point; registers a background service that starts/stops maple-proxy
  • lib/platform.ts -- OS/arch detection, maps to the correct GitHub Release artifact name
  • lib/downloader.ts -- Downloads the binary from GitHub Releases, verifies SHA256 checksums, extracts and caches in ~/.openclaw/tools/maple-proxy/
  • lib/process.ts -- Spawns maple-proxy as a child process, finds free ports, waits for health check
  • openclaw.plugin.json -- Plugin manifest with config schema (apiKey required, port/backendUrl/debug optional) and UI hints
  • package.json -- npm package @opensecret/maple-proxy-openclaw-plugin
  • skills/maple-proxy-skill/SKILL.md -- AgentSkills-compatible skill, gated on plugin being enabled

Dev tooling updates

  • flake.nix -- Added nodejs_22 to commonInputs so TypeScript plugin dev works in the Nix devShell
  • justfile -- Added plugin-install, plugin-build, plugin-lint, plugin-test, check-all, plugin-link, plugin-pack, plugin-publish commands

Naming convention

maple-proxy is reserved for the Rust binary. All plugin/skill names use a maple-proxy- prefix:

  • Plugin id: maple-proxy-openclaw-plugin
  • npm package: @opensecret/maple-proxy-openclaw-plugin
  • Skill name: maple-proxy-skill

User experience

openclaw plugins install @opensecret/maple-proxy-openclaw-plugin

Then in openclaw.json:

{
  "plugins": {
    "entries": {
      "maple-proxy-openclaw-plugin": {
        "enabled": true,
        "config": { "apiKey": "sk-..." }
      }
    }
  }
}

Restart the gateway -- the plugin downloads the binary, starts it, and the skill becomes available to the agent.

No changes to Rust source

The src/ directory is completely untouched. The existing release CI (release.yml) already builds binaries for all 4 platforms that the plugin downloads from.


Open with Devin

Summary by CodeRabbit

Release Notes

  • New Features

    • Maple Proxy plugin now available for OpenClaw, enabling integration with Maple TEE-backed AI models.
    • New maple_proxy_status tool to monitor proxy health and system metrics.
    • Plugin auto-manages binary downloads, versioning, and platform compatibility.
  • Documentation

    • Added comprehensive plugin setup, configuration, and troubleshooting guides.
    • Included skill documentation for Maple model integration with memory search capabilities.

Adds openclaw-plugin/ directory with a TypeScript OpenClaw plugin
(@opensecret/maple-proxy-openclaw-plugin) that automatically downloads,
manages, and runs the maple-proxy binary as a background service.

Includes:
- Background service: binary download/cache, process lifecycle, health checks
- Plugin manifest with config schema (apiKey, port, backendUrl, debug)
- maple-proxy-skill: AgentSkills-compatible skill gated on plugin config
- flake.nix: adds nodejs_22 to devShell
- justfile: adds plugin-install/build/lint/test/link/pack/publish commands

Closes #12

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR introduces an OpenClaw plugin for maple-proxy that automates binary download, caching, and lifecycle management. It includes platform-specific artifact resolution, background service orchestration, health checks, a proxy status tool, build configuration updates, and comprehensive documentation.

Changes

Cohort / File(s) Summary
Build Configuration
flake.nix, justfile
Added Node.js 22 to devShell inputs; introduced 8 new plugin-focused targets (install, build, lint, test, link, pack, publish) and a composite check-all target for CI integration.
Plugin Manifest & Package
openclaw-plugin/openclaw.plugin.json, openclaw-plugin/package.json, openclaw-plugin/tsconfig.json
Defined plugin metadata (id, name, config schema with apiKey, port, debug); declared npm package as @opensecret/maple-proxy-openclaw-plugin with TypeScript build and test scripts; configured TypeScript compiler for strict mode and source maps.
Plugin Entry Point
openclaw-plugin/index.ts
Implemented plugin registration with background service (start/stop lifecycle, binary validation, proxy spawning), status tool for agent queries, and public API exports (id, name, register function, config interfaces).
Binary Download & Platform Logic
openclaw-plugin/lib/downloader.ts, openclaw-plugin/lib/platform.ts
Created download orchestration with caching, SHA-256 verification, extraction (tar.gz/zip), and cleanup; platform detection for linux/darwin/win32 with artifact name and URL construction.
Process Management
openclaw-plugin/lib/process.ts
Implemented child process spawning, environment variable mapping, startup health checks, automatic restart with exponential backoff (up to 3 retries), and graceful termination.
Testing & Documentation
openclaw-plugin/lib/downloader.test.ts, openclaw-plugin/README.md, openclaw-plugin/skills/maple-proxy-skill/SKILL.md
Added comprehensive version comparison tests; provided quick-start and troubleshooting guides; documented skill configuration for Maple model access via local proxy endpoint.
Project Scaffolding
openclaw-plugin/.gitignore
Added ignore patterns for node_modules, dist, and tgz archives.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Gateway
    participant Plugin as Plugin Service
    participant DL as Downloader
    participant Platform as Platform Detect
    participant GH as GitHub Releases
    participant Process as Process Manager
    participant Proxy as maple-proxy<br/>Process
    participant Health as Health Check

    User->>Plugin: start()
    activate Plugin
    Plugin->>Plugin: validate apiKey
    Plugin->>DL: ensureBinary(logger)
    activate DL
    DL->>Platform: getArtifact()
    activate Platform
    Platform-->>DL: {name, archiveType}
    deactivate Platform
    DL->>Platform: resolveVersion(ttl cache)
    DL->>GH: fetch latest tag
    activate GH
    GH-->>DL: version string
    deactivate GH
    DL->>GH: download artifact
    DL->>GH: download & verify checksum
    DL->>DL: extract (tar.gz/zip)
    DL->>DL: set permissions
    DL-->>Plugin: {binaryPath, version}
    deactivate DL
    Plugin->>Process: startProxy(config, version)
    activate Process
    Process->>Process: verify port available
    Process->>Proxy: spawn with env vars
    activate Proxy
    Proxy-->>Process: process started
    Process->>Health: GET /health (race)
    activate Health
    Health-->>Process: {status, port, ...}
    deactivate Health
    Process-->>Plugin: RunningProxy object
    deactivate Process
    Plugin-->>User: service running
    deactivate Plugin
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 A proxy springs to life with care,
Binaries dance through platform air,
No manual toil, no setup pain—
The plugin reigns, let Maple reign! 🍁

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/openclaw-plugin

Comment @coderabbitai help to get the list of available commands and usage tips.

devin-ai-integration[bot]

This comment was marked as resolved.

- Add optional version field to plugin config (defaults to latest)
- Cache latest-version GitHub API response for 24h to avoid rate limits
- Auto-cleanup old cached binaries, keeping current + one previous
- Pass pinned version from config through to ensureBinary

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

…sh recovery

Addresses PR review feedback:

- Rename plugin id: maple-proxy-openclaw-plugin -> maple-openclaw-plugin
- Default port changed to 8000 (vLLM default) for auto-discovery
- No port fallback: errors clearly if 8000 is occupied
- Add maple_proxy_status agent tool (port, version, health)
- Crash recovery: auto-restart with exponential backoff (max 3 attempts)
- Graceful shutdown: SIGINT first, SIGTERM after 3s
- SKILL.md: documents vLLM auto-discovery, explicit provider config,
  port override, and status tool usage

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

- Race waitForHealth against child spawn errors so missing/broken binaries
  fail fast instead of waiting the full 10s health timeout. Kill and clean
  up child process on failure.
- Guard against duplicate start(): kill existing proxy before spawning a
  new one to prevent leaked processes on config reload.
- Log warning when checksum file is unavailable instead of silently skipping.
- Fix PowerShell injection: pass archive/dest paths as separate arguments.
- Extract compareVersionsDesc for semver-aware version sorting (fixes
  v0.9.0 vs v0.10.0 lexicographic bug).
- Add 9 unit tests for version sort covering edge cases.
- Distinguish EADDRINUSE from other port errors in checkPortAvailable.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
openclaw-plugin/lib/downloader.ts (1)

136-138: Consider cleaning non‑v version directories too.

Line 136-138 only tracks directories starting with "v". If a user pins "0.1.0" (no v), older non‑v folders won’t be cleaned.

♻️ Suggested tweak
-  const versionDirs = entries
-    .filter((e) => e.startsWith("v"))
+  const versionDirs = entries
+    .filter((e) => /^v?\d+\.\d+\.\d+/.test(e))
     .sort(compareVersionsDesc);

- Fix SIGTERM escalation dead code: track actual exit state instead of
  relying on child.killed (which is true immediately after kill() call).
  Now SIGTERM correctly fires after 3s if SIGINT did not terminate the
  process.
- Fix crash recovery for signal-killed processes: handle SIGSEGV, SIGABRT,
  SIGBUS etc. Previously code === null (signal death) skipped the entire
  recovery block silently. Now any non-intentional crash triggers restart.
- Fix checksum verification: only skip on 404 (file not found). Other HTTP
  errors (403 rate limit, 500 server error) now throw instead of silently
  using an unverified binary.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
openclaw-plugin/lib/downloader.ts (1)

30-37: Consider adding a timeout to prevent indefinite hangs.

The fetch call has no timeout. If GitHub is unreachable or slow, this could block indefinitely. Consider using AbortController with a reasonable timeout.

♻️ Optional: Add fetch timeout
 async function downloadFile(url: string, dest: string): Promise<void> {
-  const res = await fetch(url, { redirect: "follow" });
+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), 60000);
+  try {
+    const res = await fetch(url, { redirect: "follow", signal: controller.signal });
+    if (!res.ok) {
+      throw new Error(`Download failed: ${res.status} ${res.statusText} (${url})`);
+    }
+    const buffer = Buffer.from(await res.arrayBuffer());
+    await fsp.writeFile(dest, buffer);
+  } finally {
+    clearTimeout(timeout);
+  }
-  if (!res.ok) {
-    throw new Error(`Download failed: ${res.status} ${res.statusText} (${url})`);
-  }
-  const buffer = Buffer.from(await res.arrayBuffer());
-  await fsp.writeFile(dest, buffer);
 }
openclaw-plugin/lib/process.ts (2)

130-143: Unhandled promise in setTimeout async callback.

If waitForHealth throws during restart, the error is logged but the resulting rejected promise from the async callback is silently ignored. While the error handling inside is fine, consider explicitly handling the returned promise to avoid potential unhandled rejection warnings in future Node.js versions.

♻️ Optional: Explicitly void the async callback
-          setTimeout(async () => {
+          setTimeout(() => {
+            void (async () => {
               if (stopped) return;
               try {
                 child = spawnProxy(config, port, logger);
                 setupCrashRecovery(child);
                 await waitForHealth(port);
                 logger.info(`maple-proxy restarted on http://127.0.0.1:${port}`);
                 restartAttempts = 0;
               } catch (err) {
                 logger.error(
                   `Failed to restart maple-proxy: ${err instanceof Error ? err.message : err}`
                 );
               }
-          }, delay);
+            })();
+          }, delay);

194-203: Signal ordering: SIGINT→SIGTERM is unconventional for daemon shutdown.

The kill() method sends SIGINT first, then escalates to SIGTERM after 3 seconds. Typically, SIGTERM is the standard "please terminate gracefully" signal, while SIGINT is for interactive interrupts (Ctrl+C). If the proxy doesn't respond to either, consider a final SIGKILL escalation.

This likely works fine in practice, but if the maple-proxy binary follows standard signal conventions, swapping the order might be more idiomatic.

♻️ Optional: Consider SIGTERM→SIGKILL ordering
     kill: () => {
       stopped = true;
       if (exited) return;
-      child.kill("SIGINT");
+      child.kill("SIGTERM");
       setTimeout(() => {
         if (!exited) {
-          child.kill("SIGTERM");
+          child.kill("SIGKILL");
         }
       }, 3000);
     },

devin-ai-integration[bot]

This comment was marked as resolved.

Move exited flag and its exit listener to the same scope as crash
recovery. Reset exited=false and attach a new listener after each
respawn so kill() correctly targets the current child process, not
a stale reference from a previous crash.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

AnthonyRonning and others added 2 commits February 15, 2026 20:22
- Make RunningProxy.process a getter so it always returns the current
  child process, not a stale reference from before crash recovery.
- Escalate from SIGINT to SIGKILL (not SIGTERM) after 3s timeout.
  SIGTERM is equally catchable/ignorable as SIGINT; SIGKILL guarantees
  termination and prevents orphaned processes on shutdown.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Add a starting flag with try/finally so concurrent start() calls
(e.g. during rapid config reloads) do not race on ensureBinary,
preventing duplicate downloads and file corruption during extraction.
Also flatten the nested try blocks for clarity.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

AnthonyRonning and others added 8 commits February 15, 2026 20:44
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
When crash recovery spawns a new child but waitForHealth times out,
the child was left running. Now SIGKILL it in the catch block, matching
the initial startup error handling path.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
maple-proxy now serves /v1/embeddings with nomic-embed-text. Document
how to configure OpenClaw memorySearch to use maple-proxy as the
embedding provider, keeping all vector operations inside the TEE.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The apiKey in memorySearch.remote is forwarded as a Bearer token to
the TEE backend, so it must be the real Maple API key, not an
arbitrary placeholder.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Add "type": "module" (required by all OpenClaw extensions)
- Point openclaw.extensions at ./index.ts instead of ./dist/index.js
  (OpenClaw uses jiti to load TypeScript directly at runtime)
- Ship .ts source files instead of dist/ in the npm package
- Add openclaw peerDependency (>=2026.1.0, optional)
- Remove prepublishOnly build step (no longer needed)
- Remove main/types fields (jiti handles resolution)

Based on conventions from all official OpenClaw extensions.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Based on real-world OpenClaw setup feedback:
- Lead with explicit maple provider config (not vLLM auto-discovery)
- Change default port from 8000 to 8787 to avoid vLLM conflicts
- Document the model allowlist step (agents.defaults.models)
- Document subagent usage with maple/ prefix
- Fix auth docs: apiKey must be real Maple key everywhere
- Update all port references in examples and manifests

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

AnthonyRonning and others added 2 commits February 16, 2026 16:07
SIGKILL is only sent intentionally by our own code (cleanup on failed
restart, escalation on shutdown). Treating it as a crash wastes restart
attempts when a respawned child fails health checks and gets cleaned up.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
When the API key is not set, the tool now returns step-by-step setup
guidance instead of just "not running". This bridges the gap between
plugin install and configuration since there is no post-install hook.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

AnthonyRonning and others added 5 commits February 16, 2026 21:53
Both the SIGKILL fallback timer and crash-recovery restart timer were
holding the event loop open, delaying gateway shutdown by up to 6s.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Rewrote the Embeddings & Memory Search section based on real-world
setup experience. Key additions:

- Step-by-step: enable memory-core plugin, configure memorySearch,
  restart, reindex, and test
- Document that model must be 'nomic-embed-text' (no maple/ prefix)
  or the proxy returns 400 errors
- Document that memory-core must be explicitly added to plugins.allow
  and plugins.entries (not loaded by default despite docs saying so)
- Add troubleshooting section covering the 5 most common failure modes
- Add verification steps (openclaw memory status --deep, CLI search)
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

AnthonyRonning and others added 5 commits February 16, 2026 23:19
…etup paths

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
When multiple restart attempts overlap, the shared child variable could
be reassigned by a later attempt before an earlier one finishes. The
stale catch block would then kill the wrong process. Now each restart
captures its own spawned reference for cleanup.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@AnthonyRonning AnthonyRonning merged commit 590d67e into master Feb 17, 2026
12 of 13 checks passed
@AnthonyRonning AnthonyRonning deleted the feat/openclaw-plugin branch February 17, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenClaw plugin + skill for seamless maple-proxy integration

2 participants