Repository navigation
[Proposal] Distinguish tool origin (built-in vs third-party MCP) in permission evaluations #327
Description
What do you want to change?
We propose introducing explicit tool origin metadata (e.g., origin: "core" | "builtin_extension" | "user_extension" | "mcp") or capability manifests into tool definitions and runtime events, preserving this provenance across the execution pipeline into decideStepToolCall().
This enables Step-Code to establish clear trust tiers:
- Core & Built-in Sandbox Tools (e.g.,
workflow,plan,subagent): Evaluated against their known, fixed runtime characteristics (e.g.workflowruns exclusively in an in-memory QuickJS WebAssembly sandbox without host shell access; it skips shell AST heuristics completely). - Host Shell Execution Tools (e.g.,
bash,powershell,run_command): Route to the AST-basedanalyzeCommandPolicy()to enforce dangerous-command safety policies. - External & Third-Party Extensions / MCP Tools: Evaluated under conservative, zero-trust defensive heuristics (inspecting
command,cmd,script, etc.) to prevent malicious tools from wrapping shell commands to bypass auditing.
Why?
1. Background & Context
Step-Code adopts a microkernel architecture where even first-party capabilities are packaged as extensions. In packages/coding-agent/src/features/step-capabilities.ts, key features such as workflow, step-plan, step-tasks, subagent, and clarify_user are registered dynamically via InlineExtension (createStepCapabilitiesExtension) calling pi.registerTool().
2. The Architectural Paradox: Lost Provenance
While registering first-party capabilities through extensions provides clean modularity, tool provenance is completely erased once tools enter the runtime pool:
- In
packages/coding-agent/src/core/extensions/types.ts,ToolCallEventstatically distinguishes events (BashToolCallEvent,ReadToolCallEvent, etc., while dynamic tools collapse intoCustomToolCallEvent). - However, when entering the security gate in
packages/coding-agent/src/step/permissions.ts,decideStepToolCall(toolName: string, ...)only accepts a flattoolName: string. - As a result, the permission engine cannot determine whether a tool called
workflowwas registered by the trusted internal QuickJS runtime or an unvetted third-party MCP server.
3. The Irony of Defensive Heuristics
Because decideStepToolCall has zero provenance context, it applies a blanket heuristic via extractCommand(): any parameter named command, cmd, or script across all tools is assumed to carry a host shell command and fed into the Bash AST parser (analyzeCommandPolicy).
- The Irony: The
workflowtool was engineered with a pure-JavaScript QuickJS WebAssembly engine (src/features/workflow/vm.ts) precisely so agents can run multi-step algorithms in an isolated memory sandbox without host shell access. Yet the permission layer treats JavaScript statements insideworkflow.scriptas unanalyzable or hazardous Bash commands underApprove for Me. - The Point-fix Limitation: PR fix(coding-agent): stop analyzing workflow scripts as shell commands #317 resolved the immediate false-positive by introducing
NON_SHELL_SCRIPT_TOOLS = new Set(["workflow"]). While this fixed the bug, it introduced a new anti-pattern: the core permission layer now hardcodes the names of specific extensions, defeating the decoupled spirit of the microkernel.
4. Lessons from Modern Extensible Architectures
Modern extensible architectures (such as VS Code's distinction between Built-in Extensions and Marketplace Extensions, or Deno/Browser permission origins) separate trust boundaries by provenance:
- Trusted first-party extensions have defined, audited execution models.
- Third-party MCP servers are strictly zero-trust.
- Merging both into an indistinguishable namespace forces defensive heuristics onto tools whose safety profile is already known to the core engine.
How?
We propose a progressive, backward-compatible migration path:
Step 1: Extend Tool Definitions & Event Contracts
Add an optional origin property to ToolDefinitionBase and ToolCallEventBase:
export type ToolOrigin = "core" | "builtin" | "extension" | "mcp";
export interface ToolDefinitionBase {
name: string;
description: string;
origin?: ToolOrigin;
// Optional capability manifest for future extensibility:
capabilities?: {
shellExecution?: boolean;
filesystemAccess?: "none" | "read" | "write";
};
}- In
step-capabilities.ts, internal extensions automatically register withorigin: "builtin". - In MCP catalog loaders (
packages/coding-agent/src/core/mcp), tools loaded from MCP servers automatically register withorigin: "mcp". - User extensions discovered in
~/.stepcode/agent/extensions/receiveorigin: "extension".
Step 2: Flow Provenance into decideStepToolCall
Update StepPermissionController.handleToolCall:
const decision = decideStepToolCall(
event.toolName,
input,
state,
this.toolOverrides,
this.shellContext(),
event.origin // or pass full tool metadata object
);Step 3: Branch Policy Enforcement by Trust Boundary
In permissions.ts, replace hardcoded sets with origin-aware policy routing:
function extractCommand(toolName: string, input: Record<string, unknown>, origin?: ToolOrigin): string | undefined {
// Built-in tools only extract commands if they are dedicated shell tools
if (origin === "builtin" || origin === "core") {
if (toolName === "run_command" || toolName === "bash" || toolName === "powershell") {
return typeof input.command === "string" ? input.command : undefined;
}
return undefined; // Sandbox tools like workflow bypass shell heuristics entirely
}
// External extensions & third-party MCP tools remain under strict defensive heuristics
for (const key of ["command", "cmd", "script"]) {
if (typeof input[key] === "string") return input[key] as string;
}
return undefined;
}Step 4: Backward Compatibility Guarantee
- If
originis omitted (legacy custom tools or standalone test fixtures), it defaults to"extension"(conservative / fail-closed mode). - Existing
toolOverridesand user-configured whitelist policies remain 100% compatible. - Zero breaking changes to the external extension API.
Activity
li-xiu-qi commentedon Oct 30, 2026
Draft implementation: #328.
This implements the provenance-based design proposed here and also covers the original workflow false positive tracked in #239. It supersedes the closed, unmerged point fix in #317.
core/builtin/extension/mcpprovenance to tool definitions andtool_callevents, and propagate it into permission evaluation.run_command,bash, orpowershell). Other structured inputs are not reinterpreted as shell commands.Validation: the origin regression suite passed 17 / 17 tests,
pnpm checkpassed, and full GitHub CI passed on final commitf7386db5. The PR remains a draft pending code and description review.