feat(cli): add explicit execution opt-in flag

This commit is contained in:
Vincent Koc 2026-04-27 15:16:12 -07:00
parent 1c94019fb7
commit c17a293c60
No known key found for this signature in database
12 changed files with 63 additions and 57 deletions

View File

@ -8,12 +8,14 @@
- Add `plugin-inspector config` for resolved plugin-root config summaries.
- Add author-facing `plugin-inspector inspect` plugin-root flow.
- Add CI-native SARIF and JUnit outputs; `plugin-inspector ci` writes them by default.
- Add `--allow-execute` as a cross-platform runtime capture opt-in flag.
- Add `plugin-inspector init --scripts` for `plugin:check` and `plugin:ci` package scripts.
- Add public synthetic probe suite helpers for building probe plans from compatibility reports.
### Changed
- Make generated CI workflows use one `plugin-inspector ci --no-openclaw --runtime --mock-sdk` command.
- Make generated runtime CI commands use `--allow-execute` instead of shell-specific inline environment syntax.
- Make `plugin-inspector init --ci` detect `packageManager` and common lockfiles before generating CI install/run commands.
- Make `plugin-inspector init` output repo-relative file paths and preflight generated files before writing.
- Make `plugin-inspector init` infer `sourceRoot: "src"` from package export maps like `"./src/index.js"`.

View File

@ -146,9 +146,13 @@ the registrations made during `register(api)`. It is opt-in because it executes
plugin code:
```bash
PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector check --runtime --mock-sdk
npx @openclaw/plugin-inspector check --runtime --mock-sdk --allow-execute
```
`--allow-execute` is the explicit guard for modes that import plugin code. The
older `PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1` environment guard still works for
custom harnesses.
By default, runtime capture uses a generated mock for `openclaw/plugin-sdk` and
common external packages so plugin code can load in clean CI without OpenClaw
installed. Use `--real-sdk` only when the plugin workspace already has real SDK
@ -162,7 +166,7 @@ Runtime capture writes:
You can also capture one entrypoint directly:
```bash
PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture ./dist/index.js --mock-sdk
plugin-inspector capture ./dist/index.js --mock-sdk --allow-execute
```
## CI
@ -173,7 +177,7 @@ Minimal package scripts:
{
"scripts": {
"plugin:check": "plugin-inspector inspect --no-openclaw",
"plugin:ci": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector ci --no-openclaw --runtime --mock-sdk"
"plugin:ci": "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute"
}
}
```
@ -198,7 +202,7 @@ jobs:
node-version: 24
cache: npm
- run: npm ci
- run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
- run: npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
- uses: actions/upload-artifact@v5
if: always()
with:

View File

@ -7,7 +7,7 @@ jobs:
steps:
- checkout
- run: npm ci
- run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
- run: npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
- store_test_results:
path: reports
- store_artifacts:

View File

@ -19,7 +19,7 @@ jobs:
node-version: 24
cache: npm
- run: npm ci
- run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
- run: npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:

View File

@ -15,7 +15,7 @@ jobs:
node-version: 24
cache: npm
- run: npm ci
- run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
- run: npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
- uses: actions/upload-artifact@v5
if: always()
with:

View File

@ -2,7 +2,7 @@ plugin_inspector:
image: node:24
script:
- npm ci
- PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
- npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
artifacts:
when: always
paths:

View File

@ -1,7 +1,7 @@
{
"scripts": {
"plugin:check": "plugin-inspector inspect --no-openclaw",
"plugin:ci": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector ci --no-openclaw --runtime --mock-sdk"
"plugin:ci": "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute"
},
"pluginInspector": {
"version": 1,

View File

@ -97,8 +97,8 @@ export async function runPluginCheck(options = {}) {
const mockSdk = options.mockSdk ?? config.capture?.mockSdk ?? true;
if (capture === true) {
if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
throw new Error("runtime capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
if (!executionAllowed(options)) {
throw new Error("runtime capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 or --allow-execute in an isolated workspace");
}
const runtimeCapture = await buildRuntimeCaptureReport({
mockSdk,
@ -133,6 +133,10 @@ export async function setupPluginInspector(options = {}) {
export { createCaptureApi, renderTextSummary, writeCiOutputArtifacts };
function executionAllowed(options) {
return options.allowExecution === true || process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED === "1";
}
async function loadFixtureSetConfig(options) {
if (options.config) {
return {

View File

@ -72,8 +72,17 @@ async function runCheck(commandArgs) {
const json = commandArgs.includes("--json");
const capture = readRuntimeFlag(commandArgs);
const mockSdk = readMockSdkFlag(commandArgs);
const allowExecution = readAllowExecutionFlag(commandArgs);
const ciOutputs = readCiOutputFlags(commandArgs);
const { report, paths } = await runPluginCheck({ configPath, pluginRoot, outDir, openclawPath, capture, mockSdk });
const { report, paths } = await runPluginCheck({
allowExecution,
capture,
configPath,
mockSdk,
openclawPath,
outDir,
pluginRoot,
});
await writeCiOutputArtifacts(report, {
...ciOutputs,
cwd: path.dirname(paths.jsonPath),
@ -146,8 +155,10 @@ async function runCi(commandArgs) {
const json = commandArgs.includes("--json");
const capture = readRuntimeFlag(commandArgs);
const mockSdk = readMockSdkFlag(commandArgs);
const allowExecution = readAllowExecutionFlag(commandArgs);
const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
const { report, reportDir } = await runCiCompatibilityReport({
allowExecution,
capture,
configPath,
mockSdk,
@ -186,7 +197,7 @@ async function runCi(commandArgs) {
}
}
async function runCiCompatibilityReport({ capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
async function runCiCompatibilityReport({ allowExecution, capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
if (configPath) {
const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
@ -197,7 +208,7 @@ async function runCiCompatibilityReport({ capture, configPath, mockSdk, openclaw
};
}
const { report } = await runPluginCheck({ pluginRoot, outDir, openclawPath, capture, mockSdk });
const { report } = await runPluginCheck({ allowExecution, capture, mockSdk, openclawPath, outDir, pluginRoot });
return {
report,
reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
@ -209,11 +220,12 @@ async function runCapture(commandArgs) {
const outputPath = readFlag(commandArgs, "--output");
const pluginRoot = readFlag(commandArgs, "--plugin-root");
const mockSdk = readMockSdkFlag(commandArgs) ?? commandArgs.includes("--mock-sdk");
const allowExecution = readAllowExecutionFlag(commandArgs);
if (!entrypoint) {
throw new Error("capture requires an entrypoint path");
}
if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
if (!allowExecution && process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 or --allow-execute in an isolated workspace");
}
const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot });
@ -283,6 +295,10 @@ function readMockSdkFlag(commandArgs) {
return undefined;
}
function readAllowExecutionFlag(commandArgs) {
return commandArgs.includes("--allow-execute");
}
function renderCiTextSummary(summary) {
return [
`Status: ${summary.status.toUpperCase()}`,
@ -310,16 +326,16 @@ function printHelp() {
Usage:
plugin-inspector
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json]
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json]
plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--force]
plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]]
plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json] [--no-sarif] [--no-junit]
PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--plugin-root <path>] [--output <path>]
plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json] [--no-sarif] [--no-junit]
plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
Default check runs from the current plugin root and writes reports/ unless --out is set.
CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
Runtime capture is opt-in because it imports plugin code; use --runtime with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
`);
}

View File

@ -7,7 +7,7 @@ export const defaultInitConfigPath = "plugin-inspector.config.json";
export const defaultInitWorkflowPath = ".github/workflows/plugin-inspector.yml";
export const defaultInitPackageScripts = {
"plugin:check": "plugin-inspector inspect --no-openclaw",
"plugin:ci": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector ci --no-openclaw --runtime --mock-sdk",
"plugin:ci": "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute",
};
export async function writePluginInspectorInit(options = {}) {
@ -106,7 +106,7 @@ jobs:
node-version: 24
cache: ${setup.cache}
${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.install}
- run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
- run: ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
- uses: actions/upload-artifact@v5
if: always()
with:

View File

@ -140,8 +140,8 @@ test("public API can initialize plugin inspector files", async () => {
assert.equal(config.plugin.id, "weather");
assert.equal(config.capture.mockSdk, true);
assert.equal(packageJson.scripts["plugin:check"], "plugin-inspector inspect --no-openclaw");
assert.equal(packageJson.scripts["plugin:ci"], "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector ci --no-openclaw --runtime --mock-sdk");
assert.match(workflow, /npx @openclaw\/plugin-inspector ci --no-openclaw --runtime --mock-sdk/);
assert.equal(packageJson.scripts["plugin:ci"], "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute");
assert.match(workflow, /npx @openclaw\/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute/);
});
test("public API initializes source root from package export maps", async () => {
@ -197,18 +197,8 @@ test("public API honors config-driven runtime capture", async () => {
"utf8",
);
const previous = process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED;
process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED = "1";
try {
const result = await runPluginCheck({ pluginRoot, outDir: "reports", openclawPath: false });
assert.equal(result.runtimeCapture.summary.registrationCount, 1);
} finally {
if (previous === undefined) {
delete process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED;
} else {
process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED = previous;
}
}
const result = await runPluginCheck({ pluginRoot, outDir: "reports", openclawPath: false, allowExecution: true });
assert.equal(result.runtimeCapture.summary.registrationCount, 1);
});
async function createPluginRoot(options = {}) {

View File

@ -55,13 +55,11 @@ test("check command runs from a plugin root without fixture config", async () =>
assert.ok(report.fixtures[0].package.openclaw.entrypoints.some((entrypoint) => entrypoint.exists));
assert.match(issues, /# OpenClaw Plugin Issue Findings/);
await execFileAsync(process.execPath, [cliPath, "check", "--out", "capture-reports", "--no-openclaw", "--capture"], {
cwd: rootDir,
env: {
...process.env,
PLUGIN_INSPECTOR_EXECUTE_ISOLATED: "1",
},
});
await execFileAsync(
process.execPath,
[cliPath, "check", "--out", "capture-reports", "--no-openclaw", "--capture", "--allow-execute"],
{ cwd: rootDir },
);
const capture = JSON.parse(
await readFile(path.join(rootDir, "capture-reports", "plugin-inspector-runtime-capture.json"), "utf8"),
);
@ -75,13 +73,9 @@ test("check command can target a plugin root and use runtime aliases", async ()
await execFileAsync(
process.execPath,
[cliPath, "--plugin-root", rootDir, "--out", "reports", "--no-openclaw", "--runtime", "--mock-sdk"],
[cliPath, "--plugin-root", rootDir, "--out", "reports", "--no-openclaw", "--runtime", "--mock-sdk", "--allow-execute"],
{
cwd: os.tmpdir(),
env: {
...process.env,
PLUGIN_INSPECTOR_EXECUTE_ISOLATED: "1",
},
},
);
@ -126,12 +120,8 @@ test("check command can enable runtime capture from plugin config", async () =>
);
const cliPath = path.resolve("src/cli.js");
await execFileAsync(process.execPath, [cliPath, "check", "--out", "reports", "--no-openclaw"], {
await execFileAsync(process.execPath, [cliPath, "check", "--out", "reports", "--no-openclaw", "--allow-execute"], {
cwd: rootDir,
env: {
...process.env,
PLUGIN_INSPECTOR_EXECUTE_ISOLATED: "1",
},
});
const capture = JSON.parse(
@ -211,7 +201,7 @@ test("init command writes plugin config and CI workflow", async () => {
assert.equal(config.plugin.id, "weather");
assert.equal(config.plugin.sourceRoot, "src");
assert.equal(config.capture.mockSdk, true);
assert.match(workflow, /pnpm dlx @openclaw\/plugin-inspector ci --no-openclaw --runtime --mock-sdk/);
assert.match(workflow, /pnpm dlx @openclaw\/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute/);
});
test("init command detects plugin package managers", async () => {
@ -225,7 +215,7 @@ test("init command detects plugin package managers", async () => {
assert.match(workflow, /cache: pnpm/);
assert.match(workflow, /corepack enable/);
assert.match(workflow, /pnpm install --frozen-lockfile/);
assert.match(workflow, /pnpm dlx @openclaw\/plugin-inspector ci --no-openclaw --runtime --mock-sdk/);
assert.match(workflow, /pnpm dlx @openclaw\/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute/);
});
test("init command can add package scripts", async () => {
@ -236,7 +226,7 @@ test("init command can add package scripts", async () => {
const packageJson = JSON.parse(await readFile(path.join(rootDir, "package.json"), "utf8"));
assert.equal(packageJson.scripts["plugin:check"], "plugin-inspector inspect --no-openclaw");
assert.equal(packageJson.scripts["plugin:ci"], "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector ci --no-openclaw --runtime --mock-sdk");
assert.equal(packageJson.scripts["plugin:ci"], "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute");
});
async function createCliPluginRoot(prefix) {