From c12fcbaa5cac47905a79f7bd0e7af596ee5be29d Mon Sep 17 00:00:00 2001 From: kunaldhongade Date: Thu, 6 Aug 2026 17:50:07 +0530 Subject: [PATCH] feat(security): wire capabilityPolicy through CLI, adapters, MCP, and loop Pass the loaded capability policy into configured command execution across product, tool adapters, loop, and MCP surfaces, and document that MCP confirmations are operation-scoped. Refs #690. Co-authored-by: Cursor --- packages/adapters/src/command-adapter.ts | 10 +++-- packages/cli/src/commands/loop.ts | 3 +- .../cli/src/product/generated-tests/runner.ts | 10 +++-- .../product/generated-tests/runner/retry.ts | 10 +++-- packages/cli/src/product/runtime/service.ts | 24 +++++++++-- packages/execution/src/index.ts | 1 + packages/execution/src/safe-policy.ts | 20 +++++++++ .../execution/test/capability-policy.test.ts | 35 ++++++++++++++++ packages/mcp/src/execution/safety.ts | 4 +- packages/mcp/src/product/safety.ts | 3 +- .../src/agent-process/configured.ts | 5 ++- .../tool-adapters/src/agent-process/run.ts | 6 +-- .../tool-adapters/src/coverage/configured.ts | 5 ++- packages/tool-adapters/src/coverage/run.ts | 6 +-- packages/tool-adapters/src/index.ts | 18 ++++---- packages/tool-adapters/src/pact/configured.ts | 5 ++- packages/tool-adapters/src/pact/harness.ts | 6 +-- .../src/playwright/configured.ts | 5 ++- .../tool-adapters/src/playwright/harness.ts | 6 +-- packages/tool-adapters/src/safety.ts | 11 +++++ .../src/schemathesis/configured.ts | 5 ++- .../tool-adapters/src/schemathesis/harness.ts | 6 +-- .../tool-adapters/src/semgrep/configured.ts | 5 ++- packages/tool-adapters/src/semgrep/run.ts | 6 +-- .../tool-adapters/src/stryker/configured.ts | 5 ++- packages/tool-adapters/src/stryker/run.ts | 6 +-- packages/tool-adapters/src/types.ts | 41 +++++++------------ 27 files changed, 173 insertions(+), 94 deletions(-) create mode 100644 packages/execution/src/safe-policy.ts create mode 100644 packages/tool-adapters/src/safety.ts diff --git a/packages/adapters/src/command-adapter.ts b/packages/adapters/src/command-adapter.ts index 813e7165..84260e9f 100644 --- a/packages/adapters/src/command-adapter.ts +++ b/packages/adapters/src/command-adapter.ts @@ -1,5 +1,5 @@ import type { Finding } from "@submuxhq/codedecay-core"; -import { runConfiguredCommand, type CommandExecutionResult } from "@submuxhq/codedecay-execution"; +import { createSafeCommandPolicy, runConfiguredCommand, type CommandExecutionResult } from "@submuxhq/codedecay-execution"; import type { AdapterContext, AdapterResult, CodeDecayAdapter, CommandAdapterOptions } from "./types"; import { validateCommandAdapterOptions } from "./validation"; @@ -21,9 +21,11 @@ async function runCommandAdapter( command: options.command, cwd: context.rootDir, timeoutMs: options.timeoutMs ?? context.config.safety.commandTimeoutMs, - safety: { - allowCommands: options.requiresCommandAllowlist ? context.config.safety.allowCommands : true - } + safety: createSafeCommandPolicy({ + allowCommands: options.requiresCommandAllowlist ? context.config.safety.allowCommands : true, + capabilityPolicy: context.config.safety.capabilityPolicy + }), + capabilityIntentSource: "user-config" }); return adapterResultFromExecution(options, result); diff --git a/packages/cli/src/commands/loop.ts b/packages/cli/src/commands/loop.ts index c5877133..b809e3bc 100644 --- a/packages/cli/src/commands/loop.ts +++ b/packages/cli/src/commands/loop.ts @@ -68,7 +68,8 @@ export async function runLoopCommand( securityScoreThreshold: options.securityScoreThreshold, agentTimeoutMs: loadedConfig.config.safety.commandTimeoutMs, commandSafety: { - allowCommands: loadedConfig.config.safety.allowCommands + allowCommands: loadedConfig.config.safety.allowCommands, + capabilityPolicy: loadedConfig.config.safety.capabilityPolicy }, createRedteamReport: async () => await createRedteamReportForCli(rootDir, { diff --git a/packages/cli/src/product/generated-tests/runner.ts b/packages/cli/src/product/generated-tests/runner.ts index 22ffbc0e..44528b74 100644 --- a/packages/cli/src/product/generated-tests/runner.ts +++ b/packages/cli/src/product/generated-tests/runner.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import type { CodeDecayProductTarget, LoadedCodeDecayConfig } from "@submuxhq/codedecay-config"; -import { runConfiguredCommand } from "@submuxhq/codedecay-execution"; +import { createSafeCommandPolicy, runConfiguredCommand } from "@submuxhq/codedecay-execution"; import { generatedProductBaseUrl } from "./manifest"; import { elapsed } from "./strings"; import type { @@ -99,9 +99,11 @@ export async function runGeneratedProductTests( env: { CODEDECAY_PRODUCT_BASE_URL: generatedProductBaseUrl(rootDir, generatedTests) }, - safety: { - allowCommands: loadedConfig.config.safety.allowCommands - } + safety: createSafeCommandPolicy({ + allowCommands: loadedConfig.config.safety.allowCommands, + capabilityPolicy: loadedConfig.config.safety.capabilityPolicy + }), + capabilityIntentSource: "user-config" }); const testSource = readFileSync(join(rootDir, generatedTests.sourcePath), "utf8"); const impactedFiles = dependencies.findImpactedProductFiles(rootDir); diff --git a/packages/cli/src/product/generated-tests/runner/retry.ts b/packages/cli/src/product/generated-tests/runner/retry.ts index bad7b7c4..1eed110b 100644 --- a/packages/cli/src/product/generated-tests/runner/retry.ts +++ b/packages/cli/src/product/generated-tests/runner/retry.ts @@ -1,5 +1,5 @@ import type { LoadedCodeDecayConfig, CodeDecayProductTarget } from "@submuxhq/codedecay-config"; -import { runConfiguredCommand } from "@submuxhq/codedecay-execution"; +import { createSafeCommandPolicy, runConfiguredCommand } from "@submuxhq/codedecay-execution"; import { generatedProductBaseUrl } from "../manifest"; import type { ProductGeneratedTestCase, @@ -76,9 +76,11 @@ export async function attachGeneratedFailureRetryEvidence(input: { env: { CODEDECAY_PRODUCT_BASE_URL: generatedProductBaseUrl(input.rootDir, input.generatedTests) }, - safety: { - allowCommands: input.loadedConfig.config.safety.allowCommands - } + safety: createSafeCommandPolicy({ + allowCommands: input.loadedConfig.config.safety.allowCommands, + capabilityPolicy: input.loadedConfig.config.safety.capabilityPolicy + }), + capabilityIntentSource: "user-config" }); const parsed = parsePlaywrightTestRun({ stdout: execution.stdout, diff --git a/packages/cli/src/product/runtime/service.ts b/packages/cli/src/product/runtime/service.ts index 4bf8cc44..bfde8e14 100644 --- a/packages/cli/src/product/runtime/service.ts +++ b/packages/cli/src/product/runtime/service.ts @@ -2,6 +2,8 @@ import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:chil import type { LoadedCodeDecayConfig } from "@submuxhq/codedecay-config"; import { checkCommandSafety, + createSafeCommandPolicy, + detectShellSubstitution, runConfiguredCommand, type CommandExecutionResult } from "@submuxhq/codedecay-execution"; @@ -18,9 +20,11 @@ export async function runProductOneShotCommand( command, cwd: rootDir, timeoutMs, - safety: { - allowCommands: loadedConfig.config.safety.allowCommands - } + safety: createSafeCommandPolicy({ + allowCommands: loadedConfig.config.safety.allowCommands, + capabilityPolicy: loadedConfig.config.safety.capabilityPolicy + }), + capabilityIntentSource: "user-config" }); } @@ -42,6 +46,20 @@ export async function startManagedProductProcess( }; } + const substitution = detectShellSubstitution(command); + if (substitution) { + const message = `Command was blocked by CodeDecay capability policy: command rejected: ${substitution}.`; + return { + command, + status: "blocked", + durationMs: 0, + stdout: "", + stderr: message, + error: message, + blockedReason: `command rejected: ${substitution}` + }; + } + const safety = checkCommandSafety(command); if (!safety.safe) { const message = `Command was blocked by CodeDecay safety policy: ${safety.reason}.`; diff --git a/packages/execution/src/index.ts b/packages/execution/src/index.ts index 7f12b1b5..c7e764ac 100644 --- a/packages/execution/src/index.ts +++ b/packages/execution/src/index.ts @@ -1,5 +1,6 @@ export { runConfiguredCommand } from "./command"; export { checkCommandSafety } from "./safety"; +export { createSafeCommandPolicy } from "./safe-policy"; export { authorizeCapability, appendCapabilityAuditEvent, diff --git a/packages/execution/src/safe-policy.ts b/packages/execution/src/safe-policy.ts new file mode 100644 index 00000000..0b390998 --- /dev/null +++ b/packages/execution/src/safe-policy.ts @@ -0,0 +1,20 @@ +import { createDefaultCapabilityPolicy } from "./capability"; +import type { CapabilityPolicy } from "./capability"; +import type { SafeCommandPolicy } from "./types"; + +export function createSafeCommandPolicy(input: { + allowCommands: boolean; + capabilityPolicy?: CapabilityPolicy | undefined; + allowUnsafeCommands?: boolean | undefined; +}): SafeCommandPolicy { + const policy: SafeCommandPolicy = { + allowCommands: input.allowCommands, + capabilityPolicy: input.capabilityPolicy ?? createDefaultCapabilityPolicy() + }; + + if (input.allowUnsafeCommands !== undefined) { + policy.allowUnsafeCommands = input.allowUnsafeCommands; + } + + return policy; +} diff --git a/packages/execution/test/capability-policy.test.ts b/packages/execution/test/capability-policy.test.ts index 93e53eb5..ba6a29c7 100644 --- a/packages/execution/test/capability-policy.test.ts +++ b/packages/execution/test/capability-policy.test.ts @@ -7,6 +7,7 @@ import { authorizeCapability, checkPathWithinAllowedRoots, createDefaultCapabilityPolicy, + createSafeCommandPolicy, detectShellSubstitution, fetchWithoutExternalRedirect, resolveCapabilityAuditPath, @@ -175,6 +176,40 @@ describe("capability policy foundation", () => { expect(allowed.allowed).toBe(true); }); + it("passes loaded capabilityPolicy into configured command execution", async () => { + const root = createTempDir(); + const result = await runConfiguredCommand({ + command: "node -e \"console.log('policy')\"", + cwd: root, + timeoutMs: 1000, + safety: createSafeCommandPolicy({ + allowCommands: true, + capabilityPolicy: { + version: 1, + allow: [{ capability: "command.execute", commands: ["node"] }] + } + }) + }); + + expect(result.status).toBe("passed"); + + const denied = await runConfiguredCommand({ + command: "node -e \"console.log('nope')\"", + cwd: root, + timeoutMs: 1000, + safety: createSafeCommandPolicy({ + allowCommands: true, + capabilityPolicy: { + version: 1, + allow: [{ capability: "command.execute", commands: ["pnpm"] }] + } + }) + }); + + expect(denied.status).toBe("blocked"); + expect(denied.blockedReason).toContain("not listed in capabilityPolicy.allow commands"); + }); + it("blocks credentials, metadata hosts, and off-allowlist redirect targets", async () => { expect( validateNetworkDestination("http://user:pass@127.0.0.1/health", { diff --git a/packages/mcp/src/execution/safety.ts b/packages/mcp/src/execution/safety.ts index 54519224..7cc95a5e 100644 --- a/packages/mcp/src/execution/safety.ts +++ b/packages/mcp/src/execution/safety.ts @@ -8,7 +8,9 @@ export function createExecutionSafety( const notes = [ "This MCP tool never runs arbitrary commands from MCP input.", "Only commands explicitly configured in CodeDecay config and enabled tool adapters are eligible to run.", - "Command execution also requires safety.allowCommands: true in CodeDecay config." + "Command execution also requires safety.allowCommands: true in CodeDecay config.", + "Configured commands also pass through safety.capabilityPolicy (default deny for elevated capabilities).", + "confirmExecution authorizes only this execute_configured_checks call; it does not grant unrelated later capabilities." ]; if (!confirmExecution) { diff --git a/packages/mcp/src/product/safety.ts b/packages/mcp/src/product/safety.ts index 86d2a56b..1236894c 100644 --- a/packages/mcp/src/product/safety.ts +++ b/packages/mcp/src/product/safety.ts @@ -12,7 +12,8 @@ export function createProductSafety( allowCommands: loadedConfig.config.safety.allowCommands, notes: [ ...notes, - "Product target startup, browser automation, and generated test execution still obey safety.allowCommands in CodeDecay config.", + "Product target startup, browser automation, and generated test execution still obey safety.allowCommands and safety.capabilityPolicy in CodeDecay config.", + "MCP confirmation authorizes only this product operation; it does not grant unrelated capabilities.", "No telemetry, cloud execution, LLM calls, or arbitrary MCP-provided commands are used." ] }; diff --git a/packages/tool-adapters/src/agent-process/configured.ts b/packages/tool-adapters/src/agent-process/configured.ts index 6aa41c94..5bd38ffe 100644 --- a/packages/tool-adapters/src/agent-process/configured.ts +++ b/packages/tool-adapters/src/agent-process/configured.ts @@ -11,10 +11,11 @@ import { createAgentProcessHarness } from "./harness"; export function createConfiguredAgentProcessHarness( adapter: CodeDecayAgentProcessToolAdapter, - allowCommands: boolean + safety: { allowCommands: boolean; capabilityPolicy?: import("@submuxhq/codedecay-execution").CapabilityPolicy | undefined } ): ConfiguredToolHarness { const options: AgentProcessHarnessOptions = { - allowCommands + allowCommands: safety.allowCommands, + capabilityPolicy: safety.capabilityPolicy }; if (adapter.command !== undefined) { diff --git a/packages/tool-adapters/src/agent-process/run.ts b/packages/tool-adapters/src/agent-process/run.ts index a2fe611c..18362781 100644 --- a/packages/tool-adapters/src/agent-process/run.ts +++ b/packages/tool-adapters/src/agent-process/run.ts @@ -25,6 +25,7 @@ import { agentProcessMissingCommandEvidence } from "./evidence"; import { validateAgentProcessPlan } from "./validation"; +import { toolAdapterSafeCommandPolicy } from "../safety"; export async function runAgentProcessPlan( plan: HarnessPlan, @@ -65,10 +66,7 @@ export async function runAgentProcessPlan( CODEDECAY_AGENT_PROFILE: profile, CODEDECAY_AGENT_OUTPUT_UNTRUSTED: "1" }, - safety: { - allowCommands: options.allowCommands ?? false, - allowUnsafeCommands: options.allowUnsafeCommands - } + safety: toolAdapterSafeCommandPolicy(options) }); const durationMs = elapsed(startedAt); const artifacts = [{ path: bundle.artifactPath, description: "CodeDecay agent task bundle passed to the local agent process." }]; diff --git a/packages/tool-adapters/src/coverage/configured.ts b/packages/tool-adapters/src/coverage/configured.ts index 2cd52fb8..0558c50d 100644 --- a/packages/tool-adapters/src/coverage/configured.ts +++ b/packages/tool-adapters/src/coverage/configured.ts @@ -4,10 +4,11 @@ import { resolveCoverageDisplayCommand } from "./plan"; export function createConfiguredCoverageHarness( adapter: CodeDecayCoverageToolAdapter, - allowCommands: boolean + safety: { allowCommands: boolean; capabilityPolicy?: import("@submuxhq/codedecay-execution").CapabilityPolicy | undefined } ): ConfiguredToolHarness { const options: CoverageHarnessOptions = { - allowCommands + allowCommands: safety.allowCommands, + capabilityPolicy: safety.capabilityPolicy }; if (adapter.command !== undefined) { diff --git a/packages/tool-adapters/src/coverage/run.ts b/packages/tool-adapters/src/coverage/run.ts index d9549389..6d44c835 100644 --- a/packages/tool-adapters/src/coverage/run.ts +++ b/packages/tool-adapters/src/coverage/run.ts @@ -24,6 +24,7 @@ import { DEFAULT_COVERAGE_TIMEOUT_MS } from "./constants"; import { validateCoveragePlan } from "./validation"; +import { toolAdapterSafeCommandPolicy } from "../safety"; export async function runCoveragePlan( plan: HarnessPlan, @@ -134,9 +135,6 @@ async function runCoverageCommand( cwd: context.cwd, timeoutMs, outputLimit: options.outputLimit, - safety: { - allowCommands: options.allowCommands ?? false, - allowUnsafeCommands: options.allowUnsafeCommands - } + safety: toolAdapterSafeCommandPolicy(options) }); } diff --git a/packages/tool-adapters/src/index.ts b/packages/tool-adapters/src/index.ts index 4f213a43..f7b2f8f9 100644 --- a/packages/tool-adapters/src/index.ts +++ b/packages/tool-adapters/src/index.ts @@ -33,33 +33,37 @@ export type { export function createConfiguredToolHarnesses(config: CodeDecayConfig): ConfiguredToolHarness[] { const configured: ConfiguredToolHarness[] = []; + const safety = { + allowCommands: config.safety.allowCommands, + capabilityPolicy: config.safety.capabilityPolicy + }; if (config.toolAdapters.agentProcess?.enabled) { - configured.push(createConfiguredAgentProcessHarness(config.toolAdapters.agentProcess, config.safety.allowCommands)); + configured.push(createConfiguredAgentProcessHarness(config.toolAdapters.agentProcess, safety)); } if (config.toolAdapters.playwright?.enabled) { - configured.push(createConfiguredPlaywrightHarness(config.toolAdapters.playwright, config.safety.allowCommands)); + configured.push(createConfiguredPlaywrightHarness(config.toolAdapters.playwright, safety)); } if (config.toolAdapters.stryker?.enabled) { - configured.push(createConfiguredStrykerHarness(config.toolAdapters.stryker, config.safety.allowCommands)); + configured.push(createConfiguredStrykerHarness(config.toolAdapters.stryker, safety)); } if (config.toolAdapters.schemathesis?.enabled) { - configured.push(createConfiguredSchemathesisHarness(config.toolAdapters.schemathesis, config.safety.allowCommands)); + configured.push(createConfiguredSchemathesisHarness(config.toolAdapters.schemathesis, safety)); } if (config.toolAdapters.pact?.enabled) { - configured.push(createConfiguredPactHarness(config.toolAdapters.pact, config.safety.allowCommands)); + configured.push(createConfiguredPactHarness(config.toolAdapters.pact, safety)); } if (config.toolAdapters.semgrep?.enabled) { - configured.push(createConfiguredSemgrepHarness(config.toolAdapters.semgrep, config.safety.allowCommands)); + configured.push(createConfiguredSemgrepHarness(config.toolAdapters.semgrep, safety)); } if (config.toolAdapters.coverage?.enabled) { - configured.push(createConfiguredCoverageHarness(config.toolAdapters.coverage, config.safety.allowCommands)); + configured.push(createConfiguredCoverageHarness(config.toolAdapters.coverage, safety)); } return configured; diff --git a/packages/tool-adapters/src/pact/configured.ts b/packages/tool-adapters/src/pact/configured.ts index 04adb7dc..86b6bf6d 100644 --- a/packages/tool-adapters/src/pact/configured.ts +++ b/packages/tool-adapters/src/pact/configured.ts @@ -4,12 +4,13 @@ import type { CodeDecayCommandToolAdapter, ConfiguredToolHarness, PactHarnessOpt export function createConfiguredPactHarness( adapter: CodeDecayCommandToolAdapter, - allowCommands: boolean + safety: { allowCommands: boolean; capabilityPolicy?: import("@submuxhq/codedecay-execution").CapabilityPolicy | undefined } ): ConfiguredToolHarness { const command = adapter.command ?? DEFAULT_PACT_COMMAND; const harnessOptions: PactHarnessOptions & { command: string } = { command, - allowCommands + allowCommands: safety.allowCommands, + capabilityPolicy: safety.capabilityPolicy }; if (adapter.timeoutMs !== undefined) { diff --git a/packages/tool-adapters/src/pact/harness.ts b/packages/tool-adapters/src/pact/harness.ts index 05653bfb..53f50b4e 100644 --- a/packages/tool-adapters/src/pact/harness.ts +++ b/packages/tool-adapters/src/pact/harness.ts @@ -27,6 +27,7 @@ import { } from "../shared/execution"; import { elapsed } from "../shared/values"; import type { PactHarnessOptions } from "../types"; +import { toolAdapterSafeCommandPolicy } from "../safety"; export function createPactHarness(options: PactHarnessOptions = {}): CodeDecayHarness { const command = options.command ?? DEFAULT_PACT_COMMAND; @@ -95,10 +96,7 @@ async function runPactPlan( cwd: context.cwd, timeoutMs, outputLimit: options.outputLimit, - safety: { - allowCommands: options.allowCommands ?? false, - allowUnsafeCommands: options.allowUnsafeCommands - } + safety: toolAdapterSafeCommandPolicy(options) }); const durationMs = elapsed(startedAt); const evidence = [pactEvidenceFromExecution(execution)]; diff --git a/packages/tool-adapters/src/playwright/configured.ts b/packages/tool-adapters/src/playwright/configured.ts index 7f2de512..74d232e9 100644 --- a/packages/tool-adapters/src/playwright/configured.ts +++ b/packages/tool-adapters/src/playwright/configured.ts @@ -4,12 +4,13 @@ import type { CodeDecayCommandToolAdapter, ConfiguredToolHarness, PlaywrightHarn export function createConfiguredPlaywrightHarness( adapter: CodeDecayCommandToolAdapter, - allowCommands: boolean + safety: { allowCommands: boolean; capabilityPolicy?: import("@submuxhq/codedecay-execution").CapabilityPolicy | undefined } ): ConfiguredToolHarness { const command = adapter.command ?? DEFAULT_PLAYWRIGHT_COMMAND; const harnessOptions: PlaywrightHarnessOptions & { command: string } = { command, - allowCommands + allowCommands: safety.allowCommands, + capabilityPolicy: safety.capabilityPolicy }; if (adapter.timeoutMs !== undefined) { diff --git a/packages/tool-adapters/src/playwright/harness.ts b/packages/tool-adapters/src/playwright/harness.ts index d03645c7..5a34e6e7 100644 --- a/packages/tool-adapters/src/playwright/harness.ts +++ b/packages/tool-adapters/src/playwright/harness.ts @@ -27,6 +27,7 @@ import { } from "../shared/execution"; import { elapsed } from "../shared/values"; import type { PlaywrightHarnessOptions } from "../types"; +import { toolAdapterSafeCommandPolicy } from "../safety"; export function createPlaywrightHarness(options: PlaywrightHarnessOptions = {}): CodeDecayHarness { const command = options.command ?? DEFAULT_PLAYWRIGHT_COMMAND; @@ -95,10 +96,7 @@ async function runPlaywrightPlan( cwd: context.cwd, timeoutMs, outputLimit: options.outputLimit, - safety: { - allowCommands: options.allowCommands ?? false, - allowUnsafeCommands: options.allowUnsafeCommands - } + safety: toolAdapterSafeCommandPolicy(options) }); const durationMs = elapsed(startedAt); const evidence = [playwrightEvidenceFromExecution(execution)]; diff --git a/packages/tool-adapters/src/safety.ts b/packages/tool-adapters/src/safety.ts new file mode 100644 index 00000000..1dc1f4b5 --- /dev/null +++ b/packages/tool-adapters/src/safety.ts @@ -0,0 +1,11 @@ +import type { CapabilityPolicy } from "@submuxhq/codedecay-execution"; +import { createSafeCommandPolicy } from "@submuxhq/codedecay-execution"; +import type { ToolAdapterExecutionSafety } from "./types"; + +export function toolAdapterSafeCommandPolicy(options: ToolAdapterExecutionSafety) { + return createSafeCommandPolicy({ + allowCommands: options.allowCommands ?? false, + capabilityPolicy: options.capabilityPolicy as CapabilityPolicy | undefined, + allowUnsafeCommands: options.allowUnsafeCommands + }); +} diff --git a/packages/tool-adapters/src/schemathesis/configured.ts b/packages/tool-adapters/src/schemathesis/configured.ts index 33b9a818..78387c17 100644 --- a/packages/tool-adapters/src/schemathesis/configured.ts +++ b/packages/tool-adapters/src/schemathesis/configured.ts @@ -8,10 +8,11 @@ import type { export function createConfiguredSchemathesisHarness( adapter: CodeDecaySchemathesisToolAdapter, - allowCommands: boolean + safety: { allowCommands: boolean; capabilityPolicy?: import("@submuxhq/codedecay-execution").CapabilityPolicy | undefined } ): ConfiguredToolHarness { const options: SchemathesisHarnessOptions = { - allowCommands + allowCommands: safety.allowCommands, + capabilityPolicy: safety.capabilityPolicy }; if (adapter.command !== undefined) { diff --git a/packages/tool-adapters/src/schemathesis/harness.ts b/packages/tool-adapters/src/schemathesis/harness.ts index c3742076..531e286a 100644 --- a/packages/tool-adapters/src/schemathesis/harness.ts +++ b/packages/tool-adapters/src/schemathesis/harness.ts @@ -27,6 +27,7 @@ import { } from "../shared/execution"; import { elapsed } from "../shared/values"; import type { SchemathesisHarnessOptions } from "../types"; +import { toolAdapterSafeCommandPolicy } from "../safety"; export function createSchemathesisHarness(options: SchemathesisHarnessOptions = {}): CodeDecayHarness { const command = resolveSchemathesisCommand(options); @@ -105,10 +106,7 @@ async function runSchemathesisPlan( cwd: context.cwd, timeoutMs, outputLimit: options.outputLimit, - safety: { - allowCommands: options.allowCommands ?? false, - allowUnsafeCommands: options.allowUnsafeCommands - } + safety: toolAdapterSafeCommandPolicy(options) }); const durationMs = elapsed(startedAt); const evidence = [schemathesisEvidenceFromExecution(execution)]; diff --git a/packages/tool-adapters/src/semgrep/configured.ts b/packages/tool-adapters/src/semgrep/configured.ts index 118f3fcc..97029514 100644 --- a/packages/tool-adapters/src/semgrep/configured.ts +++ b/packages/tool-adapters/src/semgrep/configured.ts @@ -4,10 +4,11 @@ import { resolveSemgrepDisplayCommand } from "./commands"; export function createConfiguredSemgrepHarness( adapter: CodeDecaySemgrepToolAdapter, - allowCommands: boolean + safety: { allowCommands: boolean; capabilityPolicy?: import("@submuxhq/codedecay-execution").CapabilityPolicy | undefined } ): ConfiguredToolHarness { const options: SemgrepHarnessOptions = { - allowCommands + allowCommands: safety.allowCommands, + capabilityPolicy: safety.capabilityPolicy }; if (adapter.command !== undefined) { diff --git a/packages/tool-adapters/src/semgrep/run.ts b/packages/tool-adapters/src/semgrep/run.ts index 44b59457..dfccea90 100644 --- a/packages/tool-adapters/src/semgrep/run.ts +++ b/packages/tool-adapters/src/semgrep/run.ts @@ -26,6 +26,7 @@ import { } from "./evidence"; import { analyzeSemgrepReport, findingsAtOrAboveThreshold } from "./report"; import { validateSemgrepPlan } from "./validation"; +import { toolAdapterSafeCommandPolicy } from "../safety"; export async function runSemgrepPlan( plan: HarnessPlan, @@ -70,10 +71,7 @@ export async function runSemgrepPlan( cwd: context.cwd, timeoutMs, outputLimit: options.outputLimit, - safety: { - allowCommands: options.allowCommands ?? false, - allowUnsafeCommands: options.allowUnsafeCommands - } + safety: toolAdapterSafeCommandPolicy(options) }); const durationMs = elapsed(startedAt); const canParseSemgrepReport = execution.status === "passed" || execution.status === "failed"; diff --git a/packages/tool-adapters/src/stryker/configured.ts b/packages/tool-adapters/src/stryker/configured.ts index 1314731e..594e1dbf 100644 --- a/packages/tool-adapters/src/stryker/configured.ts +++ b/packages/tool-adapters/src/stryker/configured.ts @@ -4,12 +4,13 @@ import { createStrykerHarness } from "./harness"; export function createConfiguredStrykerHarness( adapter: CodeDecayStrykerToolAdapter, - allowCommands: boolean + safety: { allowCommands: boolean; capabilityPolicy?: import("@submuxhq/codedecay-execution").CapabilityPolicy | undefined } ): ConfiguredToolHarness { const command = adapter.command ?? DEFAULT_STRYKER_COMMAND; const options: StrykerHarnessOptions = { command, - allowCommands + allowCommands: safety.allowCommands, + capabilityPolicy: safety.capabilityPolicy }; if (adapter.timeoutMs !== undefined) { diff --git a/packages/tool-adapters/src/stryker/run.ts b/packages/tool-adapters/src/stryker/run.ts index ed92e978..d987abb5 100644 --- a/packages/tool-adapters/src/stryker/run.ts +++ b/packages/tool-adapters/src/stryker/run.ts @@ -26,6 +26,7 @@ import { strykerReportFailureMessage } from "./report"; import { validateStrykerPlan } from "./validation"; +import { toolAdapterSafeCommandPolicy } from "../safety"; export async function runStrykerPlan( plan: HarnessPlan, @@ -40,10 +41,7 @@ export async function runStrykerPlan( cwd: context.cwd, timeoutMs, outputLimit: options.outputLimit, - safety: { - allowCommands: options.allowCommands ?? false, - allowUnsafeCommands: options.allowUnsafeCommands - } + safety: toolAdapterSafeCommandPolicy(options) }); const durationMs = elapsed(startedAt); const mutationReport = analyzeStrykerMutationReport(context.cwd, options.reportPath ?? DEFAULT_STRYKER_REPORT_PATH); diff --git a/packages/tool-adapters/src/types.ts b/packages/tool-adapters/src/types.ts index 16f4fb88..98ab4eb2 100644 --- a/packages/tool-adapters/src/types.ts +++ b/packages/tool-adapters/src/types.ts @@ -4,6 +4,7 @@ import type { CodeDecayCoverageFailOn, CodeDecayToolSeverity, } from "@submuxhq/codedecay-config"; +import type { CapabilityPolicy } from "@submuxhq/codedecay-execution"; import type { CodeDecayHarness } from "@submuxhq/codedecay-harness"; export type { @@ -20,70 +21,56 @@ export type { CodeDecayStrykerToolAdapter } from "@submuxhq/codedecay-config"; -export interface PlaywrightHarnessOptions { - command?: string | undefined; - timeoutMs?: number | undefined; +export interface ToolAdapterExecutionSafety { allowCommands?: boolean | undefined; allowUnsafeCommands?: boolean | undefined; + capabilityPolicy?: CapabilityPolicy | undefined; outputLimit?: number | undefined; } -export interface StrykerHarnessOptions { +export interface PlaywrightHarnessOptions extends ToolAdapterExecutionSafety { + command?: string | undefined; + timeoutMs?: number | undefined; +} + +export interface StrykerHarnessOptions extends ToolAdapterExecutionSafety { command?: string | undefined; reportPath?: string | undefined; timeoutMs?: number | undefined; - allowCommands?: boolean | undefined; - allowUnsafeCommands?: boolean | undefined; - outputLimit?: number | undefined; } -export interface SchemathesisHarnessOptions { +export interface SchemathesisHarnessOptions extends ToolAdapterExecutionSafety { command?: string | undefined; schema?: string | undefined; baseUrl?: string | undefined; timeoutMs?: number | undefined; - allowCommands?: boolean | undefined; - allowUnsafeCommands?: boolean | undefined; - outputLimit?: number | undefined; } -export interface PactHarnessOptions { +export interface PactHarnessOptions extends ToolAdapterExecutionSafety { command?: string | undefined; timeoutMs?: number | undefined; - allowCommands?: boolean | undefined; - allowUnsafeCommands?: boolean | undefined; - outputLimit?: number | undefined; } -export interface SemgrepHarnessOptions { +export interface SemgrepHarnessOptions extends ToolAdapterExecutionSafety { command?: string | undefined; config?: string | undefined; reportPath?: string | undefined; failOnSeverity?: CodeDecayToolSeverity | undefined; timeoutMs?: number | undefined; - allowCommands?: boolean | undefined; - allowUnsafeCommands?: boolean | undefined; - outputLimit?: number | undefined; } -export interface CoverageHarnessOptions { +export interface CoverageHarnessOptions extends ToolAdapterExecutionSafety { command?: string | undefined; reportPaths?: string[] | undefined; failOn?: CodeDecayCoverageFailOn | undefined; timeoutMs?: number | undefined; - allowCommands?: boolean | undefined; - allowUnsafeCommands?: boolean | undefined; - outputLimit?: number | undefined; } -export interface AgentProcessHarnessOptions { +export interface AgentProcessHarnessOptions extends ToolAdapterExecutionSafety { command?: string | undefined; profile?: CodeDecayAgentProfile | undefined; bundleFormat?: CodeDecayAgentBundleFormat | undefined; timeoutMs?: number | undefined; - allowCommands?: boolean | undefined; - allowUnsafeCommands?: boolean | undefined; - outputLimit?: number | undefined; } export type ConfiguredToolAdapterKind =