<img src="docs/plugin-inspector-banner.jpg" alt="OpenClaw Plugin Inspector banner">
# OpenClaw Plugin Inspector
`@openclaw/plugin-inspector` is the offline compatibility checker for OpenClaw
plugin packages and plugin fixture suites.
It answers the questions that matter before a plugin reaches users:
- can OpenClaw discover the package metadata and `openclaw.plugin.json`
manifest?
- which hooks, registration calls, manifest contracts, and SDK imports does the
plugin use?
- does the plugin still look compatible without local OpenClaw internals?
- if CI finds a breakage, which JSON, Markdown, SARIF, JUnit, and summary
artifacts should downstream automation read?
- when a fixture-suite harness such as Crabpot runs many plugins, which findings
are hard breakages, known warnings, live issues, deprecations, or inspector
proof gaps?
The default path is static, offline, and credential-free. Runtime capture exists,
but it is opt-in because it imports plugin code.
## Requirements
- Node.js 22.13 or newer on the Node.js 22 line, or Node.js 24 or newer.
- A plugin package root with `package.json`.
- `openclaw.plugin.json` when the plugin uses the OpenClaw manifest contract.
- No OpenClaw checkout, credentials, network service, or live provider access for
default inspection.
Pass `--no-openclaw` when CI should not compare against a local OpenClaw
checkout. If an OpenClaw checkout is supplied with `--openclaw <path>`, the
inspector only reads public compatibility surfaces such as compat records, SDK
exports, hook names, manifest fields, and registrar metadata.
Source-target SDK analysis preserves inferred exports while omitting explicitly
typed function bodies and imports used only by those bodies. Imports with
declaration effects and inferred declarations remain intact.
This keeps runtime implementation dependencies out of the host type graph when
a facade mixes explicit signatures with exports that need inference.
Pass `--openclaw-version latest`, `--openclaw-version beta`, or an exact
published version to resolve an official OpenClaw npm target. Reports keep the
exact resolved version and npm source metadata. Prepared targets are cached, so
batch and repeated inspections do not download the same OpenClaw package for
each plugin.
Manifest contract checks recognize both inline contract types and canonical
contract-key tuples, including tuples in separate bundled declarations in
published OpenClaw targets.
## Quick Start
Run this from a plugin package root:
```bash
npx @openclaw/plugin-inspector inspect --no-openclaw
```
Equivalent one-off runners:
```bash
pnpm dlx @openclaw/plugin-inspector inspect --no-openclaw
yarn dlx @openclaw/plugin-inspector inspect --no-openclaw
bunx @openclaw/plugin-inspector inspect --no-openclaw
```
The command writes:
- `reports/plugin-inspector-report.json`
- `reports/plugin-inspector-report.md`
- `reports/plugin-inspector-issues.md`
`inspect` exits zero after writing the report, even when it finds hard
compatibility breakages. Add `--check`, or run `plugin-inspector check`, to exit
non-zero on hard breakages. Warnings, suggestions, issue classifications, and
logs stay visible in the report without failing the command.
## Install In A Plugin Repo
Install the package when you want repeatable local scripts and CI:
```bash
npm install --save-dev @openclaw/plugin-inspector
```
Add scripts:
```json
{
"scripts": {
"plugin:check": "plugin-inspector inspect --no-openclaw",
"plugin:ci": "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute"
}
}
```
Then run:
```bash
npm run plugin:check
```
The initializer can write the starter config, package scripts, and GitHub
Actions workflow:
```bash
npx @openclaw/plugin-inspector init --ci --scripts --dry-run
npx @openclaw/plugin-inspector init --ci --scripts
```
`init` detects `packageManager` and common lockfiles. Override that with
`--package-manager npm`, `--package-manager pnpm`, `--package-manager yarn`, or
`--package-manager bun`. Existing files are protected unless you pass `--force`.
Generated workflows enable Corepack for pnpm/Yarn or install Bun before running
the package manager. Only npm workflows enable setup-node's lockfile cache;
other workflows do not require an npm lockfile or a preinstalled package manager.
## Configuration
Small plugin repos can keep configuration in `package.json`:
```json
{
"scripts": {
"plugin:check": "plugin-inspector inspect --no-openclaw",
"plugin:ci": "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute"
},
"pluginInspector": {
"version": 1,
"plugin": {
"id": "weather",
"priority": "high",
"seams": ["dynamic-tool"],
"sourceRoot": "src",
"expect": {
"registrations": ["registerTool"]
}
},
"capture": {
"mockSdk": true
}
}
}
```
Use `plugin-inspector.config.json` for a standalone config file:
```json
{
"version": 1,
"plugin": {
"id": "weather",
"priority": "high",
"seams": ["dynamic-tool"],
"sourceRoot": "src",
"expect": {
"registrations": ["registerTool"]
}
},
"capture": {
"mockSdk": true
}
}
```
Plugin-owned entrypoints, `sourceRoot`, and `openclaw.defaultCheckoutPath` must
stay inside the plugin root. Absolute paths, UNC shares, and paths that escape
the root are rejected before filesystem access. An invalid `sourceRoot` stops
inspection; an invalid checkout setting produces `target-openclaw-rejected`.
Compare against a sibling OpenClaw checkout with `--openclaw ../openclaw` or
the API's `openclawPath` option. Operator-owned fixture-suite configurations
retain their existing sibling checkout and source paths.
These are lexical path checks. They do not follow or reject symlinks or Windows
junctions, and do not sandbox runtime capture.
Inspect the resolved config before wiring CI:
```bash
plugin-inspector config --json
```
Copy-ready examples live in:
- `examples/plugin-inspector.config.json`
- `examples/package-json-plugin-inspector.json`
## Commands
| Command | Purpose |
| --- | --- |
| `plugin-inspector` | Default alias for `check`. |
| `plugin-inspector check` | Script-friendly plugin-root check. |
| `plugin-inspector inspect` | Plugin-root check unless `--config` is supplied; with `--config`, runs a fixture report. |
| `plugin-inspector ci` | Compatibility report plus CI summary, SARIF, and JUnit outputs. |
| `plugin-inspector config` | Print resolved plugin-root config as text or JSON. |
| `plugin-inspector init` | Write starter config, scripts, and optional GitHub Actions workflow. |
| `plugin-inspector report` | Run a fixture-suite config with many plugins. |
| `plugin-inspector batch` | Discover plugin roots under a folder and write one aggregate impact report. |
| `plugin-inspector capture` | Runtime-capture one entrypoint directly. |
Common options:
| Option | Meaning |
| --- | --- |
| `--plugin-root <path>` / `--root <path>` | Check a plugin somewhere other than the current directory. |
| `--config <path>` | Read a standalone config file. Required for fixture-suite `report`. |
| `--out <dir>` | Write reports somewhere other than `reports/`. |
| `--openclaw <path>` | Compare against a local OpenClaw checkout. |
| `--openclaw-version latest\|beta\|<exact>` | Resolve and compare against an official OpenClaw npm release. |
| `--no-openclaw` | Disable OpenClaw checkout comparison. |
| `--runtime` / `--capture` | Add opt-in runtime registration capture. |
| `--no-runtime` / `--no-capture` | Disable runtime capture even when config enables it. |
| `--mock-sdk` / `--sdk mock` | Use generated SDK and external-package mocks for runtime capture. |
| `--real-sdk` / `--sdk real` | Use installed real SDK dependencies instead of mocks. |
| `--allow-execute` | Permit commands that import plugin code. |
| `--author-facing` | Limit `check`, `ci`, and `batch` reports to findings with `authorRemediation` guidance. |
| `--json` | Print machine-readable JSON to stdout. |
| `--sarif [path]` | Write SARIF from `check` or `inspect`; `ci` enables this by default. |
| `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
| `--no-sarif` / `--no-junit` | Disable default `ci` outputs. |
For `batch`, `--concurrency <n>` must be a finite number (default `4`, rounded
and clamped to `1`–`32` workers). Invalid or missing values fail before inspection
or report writes. `--keep-plugin-reports` retains individual reports under
`<out>/plugins/<relative-plugin-path>/`, preserving the corpus directory layout
and names so plugins such as `a/b`, `a-b`, and `a b` cannot overwrite each other's
reports. If the corpus root is itself a plugin, its reports go in `<out>/plugins/`.
Run the built-in help for the exact CLI surface:
```bash
plugin-inspector --help
```
## Runtime Capture
Static inspection recognizes direct plugin factory calls and compiled CommonJS
calls such as `(0, sdk.defineBundledChannelEntry)(...)` without importing them.
Runtime capture imports plugin entrypoints in an isolated subprocess and records
what `register(api)` does. Use it when static inspection cannot prove the actual
registrations made at runtime.
```bash
plugin-inspector inspect --no-openclaw --runtime --mock-sdk --allow-execute
```
`--allow-execute` is the deliberate safety switch. Without it, modes that import
plugin code fail closed. The older environment guard still works for custom
harnesses:
```bash
PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector inspect --no-openclaw --runtime --mock-sdk
```
By default, runtime capture uses generated mocks for `openclaw/plugin-sdk`
subpaths and unresolved external packages discovered in the plugin import graph.
Discovery includes executable `.setup` modules shipped inside published plugin
packages, including their retained-handler imports and CommonJS requirements.
Mocked Zod enums preserve their option values for composed schemas.
That keeps compatibility CI offline and credential-free. It does not call live
services, launch OpenClaw, run provider SDKs, or emulate service lifecycle side
effects.
Capture exposes asynchronous keyed-store descriptors so registration can bind
their public methods; every state operation rejects as unsupported. It records
runtime lifecycle registrations but does not invent an instance abort signal or
disposal hook. These descriptors do not prove persisted state or lifecycle execution.
Global singleton helpers share factory results across mocked SDK barrels within
one capture, including stored `undefined`. Prepopulated own global slots are read
without changing them. Reset callbacks are recorded without execution; explicit
lifecycle drains reject as unsupported. Runtime-store helpers retain supplied
values in capture-local slots and throw the configured error before initialization.
They do not emulate the host's managed plugin instances or persistent state.
The generated runtime's `config.current()` reads the same fixed snapshot as
`api.config`, including an explicit `apiOptions.config` supplied to capture or
retained probes. It does not load host configuration or simulate config reloads;
an explicitly supplied runtime remains authoritative.
CommonJS SDK mocking, including compiled `.cjs` entrypoints and lazy `require()`
calls in synthetic handlers, requires Node.js 22.15 or newer with
`module.registerHooks()`. On older Node versions, upgrade Node.js or use an
ESM/TypeScript entrypoint. The package supports Node.js `^22.13.0 || >=24`;
Node.js 22.13 and 22.14 support existing ESM/TypeScript capture but not CommonJS
SDK mocking.
Static inspection also discovers literal CommonJS SDK `require()` references.
Synchronous bundled-module namespace capture also requires `module.registerHooks()`
to keep SDK mocks active. Older Node.js versions report this requirement before
loading the module; existing named callable capture remains available.
TypeScript loaded through this synchronous namespace path uses Node's native
type stripping. On Node.js 22.15, enable `--experimental-strip-types` explicitly;
without it, TypeScript namespace imports fail with a syntax error. Named callable
TypeScript loading continues to use the existing asynchronous loader.
The default capture `api.runtime.modelAuth` passes synthetic provider IDs through
unchanged, returns fresh empty auth stores and profile lists, and reports no
configured API keys. Auth acquisition rejects with a mock-auth-unavailable error;
it never looks up host credentials. This supports registration and no-auth
callbacks, not provider alias validation or authenticated execution. An explicitly
supplied runtime is preserved unchanged, including an empty runtime.
Synthetic probes classify widget presenters as metadata-only. They record the
registration without calling its match, availability, or presentation callbacks,
including when channel, provider, or lifecycle execution is enabled.
Use `--real-sdk` only when the plugin workspace already has real SDK
dependencies installed and you intentionally want that path.
Real-SDK CLI capture runs in an owned child, including runtime capture enabled
by flags or plugin config. The parent bounds imports and registration, then
cleans up retained plugin timers after the child flushes its complete result.
It uses installed SDK dependencies without loading the mock SDK.
The real-SDK programmatic API stays in-process to preserve supplied runtime
objects and retained handler identity. Its 30-second default deadline reports
`capture-timeout`, stops later inspector-owned phases, and aborts supported
setup reads. Caller `signal` cancellation also stops later phases. Neither
mechanism can preempt a synchronous JavaScript loop, unload an import, stop
arbitrary plugin side effects, or clear plugin-owned timers in the caller's
process. Only owned-child capture provides that process-lifetime boundary.
Override the API budget with `timeoutMs` or `PLUGIN_INSPECTOR_CAPTURE_TIMEOUT_MS`.
Runtime capture writes:
- `reports/plugin-inspector-runtime-capture.json`
- `reports/plugin-inspector-runtime-capture.md`
Capture one entrypoint directly:
```bash
plugin-inspector capture ./dist/index.js --mock-sdk --allow-execute
```
CLI capture, mock-SDK API capture, and import-loop/runtime profiles give each
child a 30-second budget. Capture reports `capture-timeout`; timed-out profile samples
always have a nonzero `exitCode`, even if a SIGTERM handler exits zero.
Pass an `AbortSignal` as `signal` to cancel owned-child work. Cancellation is
never a successful capture or profile sample.
On POSIX, each child owns a separate process group. Completion waits for
stdout/stderr to close and cleans descendants, including after a successful
leader exit. Shutdown sends SIGTERM, then SIGKILL after a 1-second grace
period. A further 1-second close deadline fails the operation if pipes remain
open. Descendants that deliberately leave the group are not contained;
this is lifecycle supervision, not a sandbox. Windows retains direct-child
termination and the bounded close deadline, not POSIX group cleanup.
The API options `timeoutMs`, `killGraceMs`, and `maxOutputBytes` take precedence
over `PLUGIN_INSPECTOR_CAPTURE_TIMEOUT_MS`, `PLUGIN_INSPECTOR_CAPTURE_KILL_GRACE_MS`,
and `PLUGIN_INSPECTOR_CAPTURE_MAX_OUTPUT_BYTES` for owned-child capture. Profiles use
the corresponding `PLUGIN_INSPECTOR_PROFILE_*` variables. Values must be finite
positive numbers (zero does not disable limits); invalid values fall through
to the environment, then defaults. Durations/byte limits cannot exceed
2,147,483,647; grace cannot exceed 30,000 ms.
Each profiled stdout/stderr stream retains at most 1 MiB by default while
continuing to drain output. Owned-child capture retains at most 10 MiB per pipe and
fails if its JSON response is truncated; intercepted plugin stdout/stderr
inside that response retains at most 1 MiB each. The optional `ps` sampler
also has bounded output, execution, and cleanup.
Runtime-profile and import-loop samples from failed process outcomes (nonzero
exit, timeout, or cancellation) include `processOutput` in their JSON artifacts:
the bounded stdout/stderr prefix and an `outputTruncated` flag.
The existing two-line previews are unchanged. Samples also record the process
signal and timeout budget. Successful process outcomes omit `processOutput`.
This output is not automatically redacted; use credential-free workloads and
review raw diagnostics before sharing them publicly.
Default import-loop profiles launch the mock capture runner directly under one
profile budget, for both baseline and plugin samples. Their JSON artifacts
retain capture's 10 MiB default limit (`PLUGIN_INSPECTOR_CAPTURE_MAX_OUTPUT_BYTES`,
or an explicit `maxOutputBytes` override), separately from the profile's
1 MiB stdout/stderr limits. Artifacts are accepted only after a successful
current capture. RSS and CPU now measure the actual runner, and wall time no
longer includes intermediate CLI startup. Historical measurements from the
CLI-wrapper route are not directly comparable. Custom `captureCommand` and
`captureScript` launch contracts are unchanged; custom detached groups are
outside the owned process group.
These limits apply to owned child processes only. The public in-process
`captureEntrypoint` path preserves retained handler identity and does not
claim to cancel synchronous plugin code or retained callbacks.
Synthetic probe APIs give each invoked callback a 30-second default budget.
Set `timeoutMs` or `PLUGIN_INSPECTOR_PROBE_TIMEOUT_MS`; the same finite positive
API-then-environment validation applies, with no zero or infinite opt-out.
A timed-out callback becomes a failed row and remaining dependent probes are
blocked. Ordinary handler failures remain failed rows without stopping
independent probes. Caller `signal` cancellation rejects the API call.
Timeout and cancellation abort supported handler signal arguments and observe
late promise settlement, but cannot preempt synchronous JavaScript or arbitrary
plugin side effects. Programmatic probes stay in-process and preserve caller
runtime objects and retained callback identity.
Gateway method probes invoke each registered handler once, including positional
`(method, handler, options)` registrations; captured `handler`/`run`/`execute`
aliases do not create extra calls. The handler receives synthetic Gateway
options and a void `respond(ok, payload, error, meta)` callback. Existing
`registrationProbeInputs` overrides remain available.
For a method that needs unavailable host state or live credentials, pass
`gatewayMethodPrerequisites: { "fixture.account": "saved account required" }`
to `runCapturedSyntheticProbes` or `runEntrypointSyntheticProbes`. The named
method produces a `blocked` row with its method and reason before its handler
runs. Other methods retain normal response validation. Once a caller supplies
the required inputs and runtime, omit that method from the prerequisite map;
rejected or malformed responses still fail. This option never reports a
missing prerequisite as a passing runtime check.
For a known negative contract, pass `gatewayMethodExpectations` with exact method
keys and response values, for example `{ "fixture.account": { ok: false,
payload: { error: "not configured" } } }`. The actual serialized first response
must match `ok`, `payload`, and `error` exactly. A missing or unexecuted method,
conflicting prerequisite, malformed response, or mismatch fails. Reports add
bounded `gatewayResponse` method/boolean evidence without including payloads.
This proves the declared response contract, not successful live operations.
The first emitted response is authoritative, even when malformed. Probes check
its JSON-serialized response/error shape: by default `ok: true` passes, `ok: false` fails,
and later responses cannot overwrite the outcome. Logging `meta` is not a wire
field. Without an explicit response, a non-`undefined` return is adapted into a
successful payload, including `false`, `null`, and `{ ok: false }`, matching the
OpenClaw plugin registrar. A void handler can respond later within the existing
probe deadline; no response or return before that deadline fails. Handler throws,
rejections, and timeouts still fail under the ordinary probe rules.
This observes the initial RPC response plus ordinary handler settlement within
the existing deadline. It does not prove asynchronous operation completion,
transport delivery, or authorization. An accepted-only success is valid once the
handler settles; probes do not implicitly request `expectFinal` or reject legal
multi-frame methods. No Gateway connection or live host call is made.
`synthetic-probes-cli.js` runs capture and retained callbacks together in one
owned child. Its whole-child budget also defaults to 30 seconds, including
imports and registration, with `PLUGIN_INSPECTOR_PROBE_TIMEOUT_MS`,
`PLUGIN_INSPECTOR_PROBE_KILL_GRACE_MS`, and
`PLUGIN_INSPECTOR_PROBE_MAX_OUTPUT_BYTES` overrides. Shutdown uses the same
bounded grace and process-group cleanup as capture. The default report and
per-pipe limit is 10 MiB, matching capture; intercepted plugin stdout and stderr are each capped
at 1 MiB and kept separate from the report protocol.
Completed synthetic reports are still written even when they contain failed
probe rows; the CLI exits successfully after delivering them, and CI policy
evaluates those rows. Child timeout, cancellation, truncated output, or an
oversized or malformed report instead exits unsuccessfully without writing a
new output artifact. The CLI validates the report shape, row identities and
statuses, and summary counts before publication; valid empty and blocked
reports are preserved. This is protocol validation, not authenticated
completion or a security sandbox: same-process plugin code can still fabricate
a valid report. Healthy retained intervals cannot keep the child alive after
its complete report is flushed.
## CI
`plugin-inspector ci` writes the normal compatibility report plus CI-native
summary, SARIF, and JUnit artifacts.
Minimal GitHub Actions workflow:
```yaml
name: plugin-inspector
on:
pull_request:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.22.1
cache: npm
- run: npm ci
- run: npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
- uses: actions/upload-artifact@v7
if: always()
with:
name: plugin-inspector-reports
path: reports/plugin-inspector-*
```
Generated `ci` artifacts:
- `reports/plugin-inspector-report.json`
- `reports/plugin-inspector-report.md`
- `reports/plugin-inspector-issues.md`
- `reports/plugin-inspector-ci-summary.json`
- `reports/plugin-inspector-ci-summary.md`
- `reports/plugin-inspector.sarif`
- `reports/plugin-inspector.junit.xml`
CI examples:
- `examples/github-actions-plugin-inspector.yml`
- `examples/github-actions-code-scanning.yml`
- `examples/gitlab-ci-plugin-inspector.yml`
- `examples/circleci-plugin-inspector.yml`
## Report Surfaces
The compatibility report is the primary contract. Preserve field names and
finding codes because downstream CI and Crabpot reports may consume them.
Important report sections:
| Field | Meaning |
| --- | --- |
| `status` | `pass` unless hard breakages exist. |
| `summary` | Counts for fixtures, breakages, warnings, suggestions, issues, issue classes, and contract probes. |
| `targetOpenClaw` | Exact resolved version, source metadata, cache state, and public compatibility data for the selected OpenClaw target. |
| `fixtures` | Per-plugin metadata, hooks, registrations, manifest contracts, package data, SDK imports, and SDK deprecation evidence. |
| `sdkUsage` | Versioned static SDK inventory for a single-plugin report; every fixture also has its own `sdkUsage`. |
| `breakages` | Blocking compatibility failures. |
| `warnings` / `suggestions` | Non-blocking compatibility findings. |
| `issues` | Normalized issue rows with severity and class. |
| `contractProbes` | Suggested synthetic probes derived from observed contracts. |
| `logs` | Informational inventory and coverage rows. |
| `decisions` | Maintainer-facing follow-up or compatibility-policy decisions. |
Default `check`, `ci`, and `batch` reports include both author-facing and
internal findings. Pass `--author-facing` when producing plugin-author output;
that filtered view includes only findings with `authorRemediation.summary`.
Curated fixes include `authorRemediation.docsUrl`; dynamically discovered API
removals use generic remediation without inventing a documentation URL.
Current author-facing deprecation warnings include deprecated SDK helpers such
as `loadSessionStore(...)` when a plugin still depends on the legacy whole-store
session shape.
When inspecting a source checkout with `--openclaw`, the inspector also reads
hook names from its explicit name array or exhaustive hook metadata registry.
Computed or spread registry keys remain unparsed rather than reporting an incomplete surface.
It reads
deprecated records from the target's compatibility registry, including imported
literal record modules and shared record metadata. Individual SDK member imports,
re-exports, namespace accesses, and literal CommonJS/dynamic imports are matched
against those records, as are direct `api.runtime` member accesses; importing a
supported sibling member does not warn.
Registry findings use code `sdk-deprecated-api` and include `deprecatedApi`,
`replacement` (the target's migration guidance), and `removeAfter` when declared,
in `fixtures[].sdkDeprecations`, `warnings`, and `issues`. For example,
`removeAfter: "2026-12-31"` means the **final compatibility day**, not that the API
is already removed. These are non-blocking, author-facing deprecation warnings.
The replacement text is preserved verbatim because some migrations require more
than renaming a function. Packed npm targets, including ClawHub's compact target,
use the bundled December 31, 2026 retirement inventory in `src/sdk-retirements.json`.
Its upstream commit is exposed as `targetOpenClaw.compatDeprecationsSource`.
The full inventory stays on `compatRecords`. Each record lists
`replacementExports`, every call its guidance tells the author to use.
Author-facing `compatDeprecations` include that record only when each of
those names is reachable from a `./plugin-sdk` declaration export. A root
package export does not count. A public declaration may import the type
that declares the member. A name in a comment does not count. A missing
replacement is not prescribed.
An explicit source checkout uses its own registry instead of the snapshot.
Conditional prose surfaces and untyped instance-method calls are
not inferred from method names alone. The reader resolves literal data and
spreads, not computed registry builders.
### SDK usage inventory
`sdkUsage` is additive. Existing `warnings`, `issues`, deprecation codes,
remediation, and author-facing filtering retain their meanings. Inventory entries
include supported APIs as well as deprecated ones; filter
`entry.deprecationCodes.length > 0` for the deprecated subset.
The inventory records six identity kinds: `sdk-module`, `sdk-export`,
`api-member`, `runtime-member`, `hook`, and `manifest-contract`. For example,
`sdk-export:openclaw/plugin-sdk/core#OpenClawPluginApi` identifies the original
public export even when a plugin gives it a local alias. API/runtime identities
record the accessed member path, such as
`runtime-member:runtime.channel.reply.dispatchReplyFromConfig`.
Each entry includes its stable key, identity, classification (`runtime`, `type`,
`mixed`, or `declarative`), reference kinds, source evidence, and deprecation
codes. Imports count as source dependencies even when unused. A `call` means a
call expression exists in the inspected code; it is not execution telemetry.
The envelope includes `schemaVersion`, `analysisVersion`, `inputKind`, `status`,
file coverage, entries, and unresolved references in `unknowns`. Treat a missing,
failed, or partial inventory differently from a complete inventory with no
entries. A zero count describes the inspected population and cannot establish
that an API is safe to remove or that a security finding can be ignored.
Each entry retains at most 12 evidence sites with source excerpts of at most
500 UTF-16 code units. Long or minified lines are excerpted around the reference;
the path and line identify the full source. These bounds never truncate identities.
Complex provenance analysis has a deterministic limit of 2,000,000 steps per
plugin, including call indexing, alias resolution, member selection, literal
resolution and caller-context traversal. Exhaustion adds one source-located
unknown and makes the inventory `partial`. All supplied files remain in scope;
syntax scanning, literal SDK imports, and direct references to declared entry
APIs continue. Deep argument-flow references may be missing from a partial
inventory. The allowance resets for each plugin.
The public `pluginRoot.runCheck` and fixture-suite APIs accept
`sdkUsageInputKind: "source" | "artifact"`. The default is `"source"`, except
npm package fixtures default to `"artifact"`; an explicit option takes precedence.
Fixture inventories use the npm payload or a configured nested plugin package
as their root, retaining package-root metadata for source-only subdirectories.
Source analysis
includes explicit type syntax; artifact analysis describes the supplied
published files, including embedded vendor code and dependencies actually
bundled inside that artifact. Source scans exclude separately installed
`node_modules`; neither mode downloads or crawls external dependencies. Erased
types are not inferred back into JavaScript. Inspection does not execute code.
Declared `openclaw.extensions` entrypoints establish the host API parameter for
plain registration functions and default/CommonJS wrappers. Artifact mode uses
`openclaw.runtimeExtensions` when declared, including packages that omit source
files. A missing runtime peer does not fall back to source code. Source mode can
resolve a unique source equivalent of a compiled entrypoint. A missing or
ambiguous declared entrypoint is an explicit unknown, so the report is partial.
Callers that supply precomputed `targetOpenClaw` metadata can also provide
`sdkUsageTargetPath` pointing to its verified local package/source directory.
Targets returned directly by `openClawTargets.prepare` retain that path privately
and need no additional option. The filesystem path is not serialized.
Host capability-catalog context helpers have a small contract adapter mapping
selected injected helpers to their public export identities. This requires
provenance from the SDK context type; arbitrary matching function signatures do
not establish SDK implementation identity.
The inventory can use published `exports` type declarations without host
implementation files. This analysis input is not added to the report.
## CI Policy And Shared Reporting Primitives
`plugin-inspector` owns the shared CI policy and report rendering primitives.
Fixture-suite harnesses such as Crabpot should call these exports instead of
reimplementing scoring, summaries, Markdown, SARIF, or JUnit handling.
The root API exposes grouped helpers:
```js
import { ci } from "@openclaw/plugin-inspector";
const policyReport = ci.buildPolicyReport({
policy,
compatibilityReport,
executionResults,
strict: false,
});
await ci.writePolicyReport(policyReport);
```
CI policy reports default to:
- `reports/plugin-inspector-ci-policy.json`
- `reports/plugin-inspector-ci-policy.md`
A policy must use `version: 1` and define:
- `allowedBlocked`
- `expectedWarnings`
- `thresholds`
- `fixtureSets`
Policy scoring fails hard breakages, unknown blocked synthetic probes, hard ref
diff regressions, failed execution results, strict live P0 issues, and strict
classified blockers. Non-strict mode keeps classified blocked probes and live P0
issues visible as warnings.
CI summary helpers read the known report set from `reports/` and render one
machine-readable and one Markdown rollup:
- compatibility
- runtime capture
- synthetic probes
- cold import readiness
- workspace plan
- platform probes
- import-loop profile
- execution results
- runtime profile
- ref diff
- profile diff
- CI policy
## Fixture Suites
Most plugin authors should use the plugin-root workflow. Use fixture suites when
one repository intentionally checks many plugins or packages, as Crabpot does.
```bash
plugin-inspector report --config crabpot.config.json --out reports
plugin-inspector report --config crabpot.config.json --out reports --check
plugin-inspector ci --config crabpot.config.json --out reports --no-openclaw
```
Fixture-suite configs are loaded through the explicit fixture helpers. That keeps
normal plugin-root configuration simple while still supporting bulk compatibility
harnesses.
## Public API
Prefer the CLI for normal plugin repositories. Import the public API when a test
harness needs to compose workflows directly:
```js
import { pluginRoot } from "@openclaw/plugin-inspector";
const { report, paths } = await pluginRoot.runCheck({
pluginRoot: process.cwd(),
openclawPath: false,
outDir: "reports",
});
console.log(report.status, paths.jsonPath);
```
Stable grouped facades:
| Facade | Use |
| --- | --- |
| `pluginRoot` | Load config, inspect, run checks, capture entrypoints, or set up a plugin repo. |
| `fixtureSuites` | Load fixture-suite configs, run reports, and build fixture-suite readiness plans. |
| `staticInspection` | Inspect source text or fixture sets without the compatibility report layer. |
| `reports` | Render/write reports and classify issue findings. |
| `contracts` | Build, render, validate, and write contract captures and coverage. |
| `ci` | Build summaries, policy reports, execution results, SARIF, and JUnit outputs. |
| `batch` | Discover plugin roots and aggregate compatibility findings across a corpus. |
| `runtime` | Build runtime profiles, profile diffs, ref diffs, and import-loop profiles. |
| `synthetic` | Build and run synthetic probe plans. |
Named exports remain available for existing automation. Prefer the grouped
facades for new code because they show ownership and keep downstream wrappers
thin.
### In-process resource snapshots
Workload harnesses can capture phase boundaries inside the measured runtime:
```js
import {
captureProcessResources,
diffProcessResources,
} from "@openclaw/plugin-inspector/resource-profile";
const before = captureProcessResources();
await runWorkload();
const after = captureProcessResources();
const phase = diffProcessResources(before, after);
```
The same functions are available on the `runtime` facade. The dedicated subpath
loads only Node built-ins and avoids importing the compatibility inspector into
the measured host. Capture does not start timers, retain samples, or force GC.
Import it before the empty-host baseline, and account for capture overhead.
Snapshots include PID, thread ID, process time origin, monotonic elapsed time,
CPU counters in microseconds, memory in bytes, event-loop counters, and an active
resource histogram. JSON-transported snapshots can be compared, but different
process lifetimes/threads and reversed wall/CPU counters are rejected.
- `cpuMs` reports actual process user/system/total deltas. It includes worker
threads, excludes child processes, and can exceed wall time on multiple cores.
- `memoryDeltaBytes` preserves signed endpoint changes, not phase peaks. RSS
covers the process; heap, external, and ArrayBuffer fields cover the current
thread. ArrayBuffers are included in external memory; do not add them together.
- `eventLoop` reports the current loop's active/idle deltas and utilization,
independently of CPU. No elapsed loop time produces `null`, not an idle verdict.
- `activeResourceDelta` counts resources keeping the current loop alive. It does
not inventory arbitrary listeners or unreferenced resources, prove ownership,
or establish complete cleanup.
Use equivalent warmup/drain/GC boundaries for retention comparisons. A single
RSS or heap delta is not a leak verdict. Collect child-process usage separately;
do not add process-wide counters from multiple worker snapshots. Existing
`cpuMsEstimate` and `harnessHeapDeltaMb` reports retain their separate meaning:
sampled immediate-child CPU estimates and the Inspector supervisor's heap.
Contracts: [Node process counters](https://nodejs.org/api/process.html) and
[event-loop utilization](https://nodejs.org/api/perf_hooks.html).
## Development
Repository checks are intentionally small and offline:
```bash
npm test
npm run release:contents
npm run check
```
`npm run check` runs the Node test suite and the package-contents guard. The
test runner limits parallel test files to four so process-supervision tests do
not contend with a machine-wide burst of child processes for their deadlines. The
contents guard shells through `npm pack --dry-run --json` and verifies the npm
tarball includes package entrypoints, examples, README assets, and no private
`test/`, `scripts/`, or `.github/` paths.
Useful release-prep commands:
```bash
npm run release:local
npm run release:readiness
npm run release:notes
npm run release:plan
npm run release:crabpot -- --crabpot ../crabpot
```
`release:readiness` proves the local package and verifies Crabpot follow-through.
It does not publish.
Keep this package dependency-light. Do not add runtime dependencies unless they
remove real complexity. Default checks must stay offline and credential-free.
## Release Notes
The package publishes from annotated `v*` tags through GitHub Actions. The
release workflow runs the test suite, verifies the npm tarball, publishes the
GitHub release, and publishes the public npm package through npm trusted
publishing.
Before tagging a release:
1. Move `CHANGELOG.md` `Unreleased` notes into a versioned section.
2. Update `package.json` to the same version.
3. Update Crabpot's `pluginInspectorRef` to the release commit.
4. Run `npm run release:readiness`.
5. Run the Crabpot plugin-inspector smoke commands printed by
`npm run release:crabpot -- --crabpot ../crabpot`.
After npm publish, update Crabpot's package pin and run:
```bash
npm run release:crabpot -- --crabpot ../crabpot --published
```
Do not publish npm packages without explicit owner approval.
## Contribution Notes
There is no `CONTRIBUTING.md` in this repository. Until one exists, use the repo
scripts above as the local contract and follow these project rules:
- preserve stable report field names and finding codes;
- prefer public OpenClaw plugin contracts over core internals;
- isolate any OpenClaw source parsing behind explicit helpers;
- keep runtime execution behind `--allow-execute` or
`PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1`;
- when behavior, entrypoints, release metadata, or the npm package version
change, update Crabpot's `@openclaw/plugin-inspector` pin/docs/smoke path and
run the Crabpot plugin-inspector smoke before calling the work done.