Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,32 @@ jobs:
- name: Run Public Compatibility Smoke
run: dotnet test --no-build -c Release --filter "Category=PublicSmoke" --verbosity normal --logger "trx;LogFileName=public-smoke.trx" src/OpenClaw.Tests

- name: Probe latest public plugin packages
id: latest_plugin_canary
continue-on-error: true
env:
OPENCLAW_LATEST_CANARY: "1"
run: dotnet test --no-build -c Release --filter "Category=LatestCanary" --verbosity normal --logger "trx;LogFileName=latest-plugin-canary.trx" src/OpenClaw.Tests

- name: Report latest-package compatibility canary
if: always()
shell: bash
run: |
if [[ "${{ steps.latest_plugin_canary.outcome }}" == "success" ]]; then
echo "### Latest plugin compatibility canary: passing" >> "$GITHUB_STEP_SUMMARY"
echo "Current npm releases still match the pinned compatibility expectations." >> "$GITHUB_STEP_SUMMARY"
else
result="${{ steps.latest_plugin_canary.outcome }}"
echo "::warning title=Latest plugin compatibility canary failed::The canary result was '${result}'. This may indicate package compatibility drift or a Node, npm, process, or test-infrastructure failure. Review the latest-plugin-canary artifact; the pinned release gate remains authoritative."
echo "### Latest plugin compatibility canary: ${result}" >> "$GITHUB_STEP_SUMMARY"
echo "Review the latest-plugin-canary test artifact to classify the failure. This signal is intentionally non-blocking; pinned public-smoke scenarios remain the release gate." >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload smoke results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: public-smoke-results
path: "**/public-smoke.trx"
path: |
**/public-smoke.trx
**/latest-plugin-canary.trx
6 changes: 3 additions & 3 deletions compat/public-smoke.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@
},
{
"id": "supermemory",
"category": "unsupported-surface-plugin",
"category": "cli-plugin",
"kind": "npm-plugin",
"spec": "@supermemory/openclaw-supermemory@2.0.2",
"packageName": "@supermemory/openclaw-supermemory",
"pluginId": "openclaw-supermemory",
"installExtraPackages": ["jiti"],
"expectedStatus": "incompatible",
"expectedStatus": "compatible",
"configJson": "{}",
"expectedDiagnosticCodes": ["unsupported_cli_registration"]
"expectedCliCommandNames": ["supermemory"]
}
]
}
39 changes: 30 additions & 9 deletions docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,24 @@ OpenClaw.NET keeps plugin compatibility explicit by runtime mode. The goal is to
| Surface | Status | Notes |
| --- | --- | --- |
| `api.registerTool()` | Supported | Available in both `aot` and `jit`. Covered by hermetic bridge tests. |
| Tool `outputSchema` and structured `details` | Supported | Output schemas are exposed to the model tool contract and structured results remain JSON instead of being flattened to text. |
| `api.registerService()` | Supported | Available in both `aot` and `jit`, including `start` / `stop` lifecycle coverage. |
| `api.registerChannel()` | Supported with caveats | `jit` only. `aot` fails fast with `jit_mode_required`. |
| `api.registerCommand()` | Supported with caveats | `jit` only. Registered as dynamic chat commands. |
| `api.on(...)` | Supported with caveats | `jit` only. `tool:before` / `tool:after` hooks are bridged with timeout protections. |
| `api.registerProvider()` | Supported with caveats | `jit` only. Plugin-provided LLMs are wired through the dynamic provider seam. |
| `OpenClaw.Providers.MicrosoftExtensionsAI` | Supported with caveats | `jit` only through native dynamic plugins. Use this to bring an arbitrary `IChatClient`; AOT users should use built-in providers or OpenAI-compatible endpoints. |
| Standalone `.js`, `.mjs`, `.ts` in `.openclaw/extensions` | Supported with caveats | `.ts` requires local `jiti`. |
| Manifest/package discovery via `Plugins:Load:Paths` | Supported | Includes `openclaw.plugin.json` and `package.json` `openclaw.extensions`. |
| `openclaw plugins install --dry-run` trust inspection | Supported | Prints trust level, declared surface, diagnostics, and blocks install when compatibility errors are present. |
| Standalone `.js`, `.mjs`, `.cjs`, `.ts` in `.openclaw/extensions` | Supported with caveats | CLI installs add local `jiti` when a TypeScript entry needs it; manually configured TypeScript paths still require `jiti` in their dependency tree. |
| Manifest/package discovery via `Plugins:Load:Paths` | Supported | Includes `openclaw.plugin.json`; prefers `package.json` `openclaw.runtimeExtensions` and retains `openclaw.extensions` compatibility. Built JavaScript entries are preferred over TypeScript source. |
| Package compatibility metadata | Supported with caveats | `openclaw.compat.pluginApi`, `compat.minGatewayVersion`, and `install.minHostVersion` are enforced before load. `install.expectedIntegrity` is discovered but fails closed because an extracted directory cannot yet be verified against package-manager integrity metadata. |
| `openclaw plugins install --dry-run` trust inspection | Supported | Prints manifest/package/static compatibility rather than claiming runtime verification. Known unsupported registration APIs block installation. |
| Staged plugin installation | Supported | Dependencies and any required `jiti` runtime are installed in a sibling staging directory with npm lifecycle scripts disabled, the bridge initializes the staged plugin, and only then is the existing install replaced atomically. A failed update preserves the previous plugin. |
| Codex compatible bundles | Supported with caveats | Detects `.codex-plugin/plugin.json`; maps `skills/` into plugin skills. Hook packs, MCP metadata, and app metadata are reported as detected-only because OpenClaw.NET does not yet execute those bundle surfaces. |
| Claude compatible bundles | Supported with caveats | Detects `.claude-plugin/plugin.json` and manifestless Claude layouts. Maps `skills/` and Markdown `commands/`; agents, hook automation, MCP, LSP, settings, and output styles are reported as detected-only. |
| Cursor compatible bundles | Supported with caveats | Detects `.cursor-plugin/plugin.json` and `.cursor/` layouts. Maps `skills/` and `.cursor/commands/`; agents, rules, hooks, and MCP metadata are reported as detected-only. |
| Bundle trust boundary | Supported | Native plugin detection has precedence. Bundle JavaScript is never loaded as a native plugin, bundle installs do not run npm dependency/lifecycle scripts, and all mapped content paths remain constrained to the bundle root. |
| Plugin config validation | Supported with caveats | Validated against the documented JSON Schema subset below before startup. |
| `api.registerCli()` | Supported with caveats | Root commands are discovered lazily and executed in a one-shot Node bridge with inherited terminal streams. Built-in CLI roots win; names are validated and duplicate plugin roots fail closed. The bridge implements the common Commander registration subset used by upstream plugins. |
| Plugin diagnostics in `/doctor` | Supported | Discovery, load, config, and compatibility failures are reported explicitly. |
| Plugin bridge runtime budgets | Supported | `OpenClaw:Plugins:RuntimeBudget` can auto-quarantine bridge plugins by restart count, working set, and compatibility error thresholds. |
| `Plugins:Transport:Mode=stdio` | Supported | JSON-RPC over child process stdin/stdout. |
Expand All @@ -71,12 +79,11 @@ OpenClaw.NET keeps plugin compatibility explicit by runtime mode. The goal is to

## Unsupported Today

These APIs are not bridged. If a plugin uses them, initialization fails fast with structured diagnostics instead of loading partially:
This API is not bridged. Static install inspection and bridge initialization fail with structured diagnostics instead of loading the plugin partially:

| Surface | Status | Failure code |
| --- | --- | --- |
| `api.registerGatewayMethod()` | Not supported | `unsupported_gateway_method` |
| `api.registerCli()` | Not supported | `unsupported_cli_registration` |

## Canvas and A2UI Compatibility

Expand Down Expand Up @@ -112,9 +119,9 @@ The messaging channels below now share the same operator model for DM policy, re

## TypeScript Requirements

TypeScript plugins are supported when `jiti` is available in the plugin dependency tree.
TypeScript plugins are supported when `jiti` is available in the plugin dependency tree. `openclaw plugins install` installs it into the staged plugin automatically when necessary.

Install it in the plugin directory or its parent workspace:
For plugins loaded directly from manually configured paths, install it in the plugin directory or its parent workspace:

```bash
npm install jiti
Expand Down Expand Up @@ -174,11 +181,13 @@ The catalog is scenario-based rather than marketing-based:
- negative scenarios show pinned configs or packages expected to fail with explicit diagnostics
- each entry includes install guidance, required config examples where relevant, and expected tools, skills, or diagnostics

Pinned scenarios remain the release gate. The scheduled/manual `LatestCanary` lane separately installs the current npm release for each distinct catalog package and compares it with the pinned expectation. That moving probe is intentionally non-blocking: drift produces a workflow warning, test artifact, resolved package version, and diagnostics without making a known-good pinned release fail.

## Known Limitations

- Public-bind setup defaults intentionally disable bridge plugins and shell until you opt into the relevant trust settings.
- JIT-only capabilities remain JIT-only; `aot` does not attempt partial dynamic fallback.
- TypeScript plugin loading depends on `jiti`; OpenClaw.NET does not bundle a TypeScript runtime automatically.
- TypeScript plugin loading depends on `jiti`; CLI installs provision it when needed, while plugins loaded directly from configured paths must provide it locally.
- Out-of-root plugin entry files, manifests, and native dynamic assemblies fail explicitly instead of being resolved elsewhere on disk.
- Tool-name collisions are deterministic: the first tool wins, later duplicates are skipped and reported.

Expand All @@ -195,6 +204,18 @@ The compatibility claim is backed by automated validation in `src/OpenClaw.Tests
- plugin-packaged skills
- config validation, including `oneOf`
- unsupported-surface failure modes
- bridge restart readiness across stdio, socket, and hybrid transports
- structured output-schema and result preservation
- `PluginCommandsTests.cs`
- dry-run compatibility status and unsupported-surface rejection
- package API-floor rejection
- isolated runtime inspection diagnostics
- compatible bundle inspection and mapped/detected-only surface reporting
- `PluginTests.cs` / `SkillTests.cs`
- Codex, Claude, and Cursor bundle detection
- native-plugin precedence over dual-format bundle markers
- bundle loading without bridge execution
- Claude/Cursor Markdown command mapping into user-invocable skills
- `NativeDynamicPluginHostTests.cs`
- JIT-mode in-process plugin loading
- command and service lifecycle
Expand All @@ -207,4 +228,4 @@ The compatibility claim is backed by automated validation in `src/OpenClaw.Tests
- pinned config-schema rejection case
- pinned unsupported-surface plugin case

The nightly/manual CI smoke lane runs those public packages with `OPENCLAW_PUBLIC_SMOKE=1`.
The nightly/manual CI smoke lane runs pinned public packages with `OPENCLAW_PUBLIC_SMOKE=1`, then runs the non-blocking moving probe with `OPENCLAW_LATEST_CANARY=1`.
27 changes: 27 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,33 @@ OpenClaw.NET is designed to be compatible with the original [OpenClaw](https://g

OpenClaw.NET spawns a Node.js bridge process to run upstream plugins over JSON-RPC. For runtime requirements, the compatibility matrix, and install paths, see the **Bridged Tools** section of the [Tool Guide](TOOLS_GUIDE.md). For the full supported-feature breakdown, see the [Compatibility Guide](COMPATIBILITY.md).

Inspect a package without changing the workspace:

```bash
openclaw plugins install <package-or-path> --dry-run
```

Dry-run validates the manifest, package compatibility metadata, entry containment, config schema, skill paths, and known unsupported registration APIs. It reports `manifest-valid` only for that pre-execution evidence. A real install then installs dependencies and initializes the plugin in an isolated bridge process inside a staging directory before atomically replacing any existing copy, so failed updates preserve the working plugin.

Codex, Claude, and Cursor compatible bundles use the same install path. They appear as `Format: bundle` with a `Bundle format` value, but they keep a narrower execution boundary: OpenClaw.NET maps bundle skill roots and Claude/Cursor Markdown command roots without executing arbitrary bundle modules. Other detected surfaces are shown as `bundle_capability_detected_only` diagnostics so users can distinguish reusable content from runtime gaps.

Inspect an installed bundle or native plugin directly:

```bash
openclaw plugins inspect <plugin-id>
openclaw plugins inspect <plugin-id> --runtime
```

For bundles, `--runtime` confirms that no arbitrary module was executed. For native plugins, it initializes the bridge and reports registered tool, channel, chat-command, root-CLI-command, and provider counts.

Upstream plugins can also add root commands with `api.registerCli()`. OpenClaw.NET discovers those commands only after a built-in root does not match, then executes the selected plugin in a fresh Node process with the terminal attached:

```bash
openclaw <plugin-command> [arguments]
```

Built-in command names always take precedence. Disabled or quarantined plugins are not eligible, duplicate plugin command roots fail closed, and CLI discovery honors `OPENCLAW_CONFIG_PATH`, `OPENCLAW_WORKSPACE`, plugin allow/deny settings, per-plugin enablement, slots, package compatibility metadata, and plugin config validation. The bridge supports the common Commander-style `command`, `description`, `argument`, `option`, `requiredOption`, and `action` registration flow. Plugin commands that directly edit upstream-specific config files remain responsible for whether those files match the OpenClaw.NET deployment configuration.

---

## Breaking Changes
Expand Down
2 changes: 1 addition & 1 deletion docs/zh-CN/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
| `api.registerCommand()` | jit only |
| `api.on(...)` | jit only |
| `api.registerProvider()` | jit only |
| `api.registerCli()` | Supported with caveats(内置根命令优先;重复插件根命令会失败关闭;仅启用且通过配置验证的插件参与惰性发现;通过一次性 Node 桥接进程执行,并支持上游常用的 Commander 子集) |
| 独立 `.js`/`.mjs`/`.ts` | `.ts` 需 `jiti` |
| 原生动态 .NET 插件 | jit only |
| 上游 TypeScript `payment` 插件 | Not supported(使用原生支付运行时) |
Expand All @@ -42,7 +43,6 @@
| 接口 | 失败码 |
| --- | --- |
| `api.registerGatewayMethod()` | `unsupported_gateway_method` |
| `api.registerCli()` | `unsupported_cli_registration` |

## Canvas 和 A2UI 兼容性

Expand Down
17 changes: 16 additions & 1 deletion src/OpenClaw.Agent/OpenClawToolExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1242,11 +1242,26 @@ private static ValueTask<string> InvokeToolAsync(
internal static AIFunctionDeclaration CreateDeclaration(ITool tool)
{
using var doc = JsonDocument.Parse(tool.ParameterSchema);
JsonElement? returnSchema = null;
if (tool is IToolOutputSchema { OutputSchema: { Length: > 0 } outputSchema })
{
try
{
using var returnSchemaDocument = JsonDocument.Parse(outputSchema);
returnSchema = returnSchemaDocument.RootElement.Clone();
}
catch (JsonException)
{
// A malformed optional return schema must not hide an otherwise valid tool.
returnSchema = null;
}
}

return AIFunctionFactory.CreateDeclaration(
tool.Name,
tool.Description,
doc.RootElement.Clone(),
returnJsonSchema: null);
returnJsonSchema: returnSchema);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private static string NormalizeApprovalToolName(string toolName) =>
Expand Down
4 changes: 3 additions & 1 deletion src/OpenClaw.Agent/Plugins/BridgedPluginTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ namespace OpenClaw.Agent.Plugins;
/// An ITool implementation that bridges to a tool registered by an OpenClaw
/// TypeScript/JavaScript plugin running in a Node.js child process.
/// </summary>
public sealed class BridgedPluginTool : ITool
public sealed class BridgedPluginTool : ITool, IToolOutputSchema
{
private readonly PluginBridgeProcess _bridge;
private readonly string _pluginId;

public string Name { get; }
public string Description { get; }
public string ParameterSchema { get; }
public string? OutputSchema { get; }

/// <summary>Whether this tool is optional (opt-in only).</summary>
public bool Optional { get; }
Expand All @@ -29,6 +30,7 @@ public BridgedPluginTool(
Name = registration.Name;
Description = registration.Description;
ParameterSchema = registration.Parameters.GetRawText();
OutputSchema = registration.OutputSchema?.GetRawText();
Optional = registration.Optional;
}

Expand Down
Loading