feat(cli): add explicit execution opt-in flag
This commit is contained in:
parent
1c94019fb7
commit
c17a293c60
@ -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"`.
|
||||
|
||||
12
README.md
12
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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
36
src/cli.js
36
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 <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.
|
||||
`);
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 = {}) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user