diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c25ba2..d0975bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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"`. diff --git a/README.md b/README.md index 0d1094a..363543c 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/examples/circleci-plugin-inspector.yml b/examples/circleci-plugin-inspector.yml index 8b8088d..ba7a857 100644 --- a/examples/circleci-plugin-inspector.yml +++ b/examples/circleci-plugin-inspector.yml @@ -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: diff --git a/examples/github-actions-code-scanning.yml b/examples/github-actions-code-scanning.yml index 8679d17..800e43a 100644 --- a/examples/github-actions-code-scanning.yml +++ b/examples/github-actions-code-scanning.yml @@ -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: diff --git a/examples/github-actions-plugin-inspector.yml b/examples/github-actions-plugin-inspector.yml index a18fff8..c0f4964 100644 --- a/examples/github-actions-plugin-inspector.yml +++ b/examples/github-actions-plugin-inspector.yml @@ -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: diff --git a/examples/gitlab-ci-plugin-inspector.yml b/examples/gitlab-ci-plugin-inspector.yml index 9c97273..877515c 100644 --- a/examples/gitlab-ci-plugin-inspector.yml +++ b/examples/gitlab-ci-plugin-inspector.yml @@ -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: diff --git a/examples/package-json-plugin-inspector.json b/examples/package-json-plugin-inspector.json index 007ee36..91615c6 100644 --- a/examples/package-json-plugin-inspector.json +++ b/examples/package-json-plugin-inspector.json @@ -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, diff --git a/src/api.js b/src/api.js index 89d6b43..f4a57d8 100644 --- a/src/api.js +++ b/src/api.js @@ -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 { diff --git a/src/cli.js b/src/cli.js index 71d8172..191e777 100755 --- a/src/cli.js +++ b/src/cli.js @@ -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 ] [--config ] [--out ] [--openclaw ] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json] + plugin-inspector check [--plugin-root ] [--config ] [--out ] [--openclaw ] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json] plugin-inspector config [--plugin-root ] [--config ] [--json] plugin-inspector init [--plugin-root ] [--config ] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--force] plugin-inspector report --config [--out ] [--check] [--json] - plugin-inspector inspect [--plugin-root ] [--config ] [--out ] [--check] [--json] [--sarif [path]] [--junit [path]] - plugin-inspector ci [--plugin-root ] [--config ] [--out ] [--openclaw ] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json] [--no-sarif] [--no-junit] - PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture [--mock-sdk|--real-sdk] [--plugin-root ] [--output ] + plugin-inspector inspect [--plugin-root ] [--config ] [--out ] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute] + plugin-inspector ci [--plugin-root ] [--config ] [--out ] [--openclaw ] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json] [--no-sarif] [--no-junit] + plugin-inspector capture [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root ] [--output ] 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. `); } diff --git a/src/init.js b/src/init.js index 80f2c9e..7a30cad 100644 --- a/src/init.js +++ b/src/init.js @@ -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: diff --git a/test/api.test.js b/test/api.test.js index ce7b1b5..ff7502e 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -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 = {}) { diff --git a/test/cli.test.js b/test/cli.test.js index 45972fd..6d04a76 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -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) {