fix(exec): make Windows exec hints accurate and dynamic

This commit is contained in:
Peter Steinberger
2026-04-02 15:30:12 +01:00
parent fff6333773
commit 36d953aab6

View File

@@ -2,7 +2,13 @@ import fs from "node:fs/promises";
import path from "node:path";
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
import { analyzeShellCommand } from "../infra/exec-approvals-analysis.js";
import { type ExecHost, loadExecApprovals, maxAsk, minSecurity } from "../infra/exec-approvals.js";
import {
type ExecHost,
loadExecApprovals,
maxAsk,
minSecurity,
resolveExecApprovalsFromFile,
} from "../infra/exec-approvals.js";
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js";
import { sanitizeHostExecEnvWithDiagnostics } from "../infra/host-env-security.js";
import {
@@ -430,6 +436,55 @@ function rejectExecApprovalShellCommand(command: string): void {
}
}
/**
* Show the exact approved token in hints. Absolute paths stay absolute so the
* hint cannot imply an equivalent PATH lookup that resolves to a different binary.
*/
function deriveExecShortName(fullPath: string): string {
if (path.isAbsolute(fullPath)) {
return fullPath;
}
const base = path.basename(fullPath);
return base.replace(/\.exe$/i, "") || base;
}
function buildExecToolDescription(agentId?: string): string {
const base =
"Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. Use pty=true for TTY-required commands (terminal UIs, coding agents).";
if (process.platform !== "win32") {
return base;
}
const lines: string[] = [base];
lines.push(
"IMPORTANT (Windows): Run executables directly — do NOT wrap commands in `cmd /c`, `powershell -Command`, `& ` prefix, or WSL. Use backslash paths (C:\\path), not forward slashes. Use short executable names (e.g. `node`, `python3`) instead of full paths.",
);
try {
const approvalsFile = loadExecApprovals();
const approvals = resolveExecApprovalsFromFile({ file: approvalsFile, agentId });
const allowlist = approvals.allowlist.filter((entry) => {
const pattern = entry.pattern?.trim() ?? "";
return (
pattern.length > 0 &&
pattern !== "*" &&
!pattern.startsWith("=command:") &&
(pattern.includes("/") || pattern.includes("\\") || pattern.includes("~"))
);
});
if (allowlist.length > 0) {
lines.push(
"Pre-approved executables (exact arguments are enforced at runtime; no approval prompt needed when args match):",
);
for (const entry of allowlist.slice(0, 10)) {
const shortName = deriveExecShortName(entry.pattern);
const argNote = entry.argPattern ? "(restricted args)" : "(any arguments)";
lines.push(` ${shortName} ${argNote}`);
}
}
} catch {
// Allowlist loading is best-effort; don't block tool creation.
}
return lines.join("\n");
}
export function createExecTool(
defaults?: ExecToolDefaults,
// oxlint-disable-next-line typescript/no-explicit-any
@@ -485,8 +540,9 @@ export function createExecTool(
return {
name: "exec",
label: "exec",
description:
"Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. Use pty=true for TTY-required commands (terminal UIs, coding agents).",
get description() {
return buildExecToolDescription(agentId);
},
parameters: execSchema,
execute: async (_toolCallId, args, signal, onUpdate) => {
const params = args as {