mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
refactor: route inline eval through command analysis
This commit is contained in:
@@ -129,7 +129,7 @@ vi.mock("./bash-process-registry.js", () => ({
|
||||
tail: vi.fn((value) => value),
|
||||
}));
|
||||
|
||||
vi.mock("../infra/exec-inline-eval.js", () => ({
|
||||
vi.mock("../infra/command-analysis/inline-eval.js", () => ({
|
||||
describeInterpreterInlineEval: vi.fn(() => "python -c"),
|
||||
detectInterpreterInlineEvalArgv: detectInterpreterInlineEvalArgvMock,
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { describeInterpreterInlineEval } from "../infra/command-analysis/inline-eval.js";
|
||||
import { detectPolicyInlineEval } from "../infra/command-analysis/policy.js";
|
||||
import {
|
||||
addDurableCommandApproval,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
resolveApprovalAuditCandidatePath,
|
||||
requiresExecApproval,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import { describeInterpreterInlineEval } from "../infra/exec-inline-eval.js";
|
||||
import type { SafeBinProfile } from "../infra/exec-safe-bin-policy.js";
|
||||
import { markBackgrounded, tail } from "./bash-process-registry.js";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import {
|
||||
describeInterpreterInlineEval,
|
||||
type InterpreterInlineEvalHit,
|
||||
} from "../infra/command-analysis/inline-eval.js";
|
||||
import { detectPolicyInlineEval } from "../infra/command-analysis/policy.js";
|
||||
import {
|
||||
type ExecApprovalsFile,
|
||||
@@ -10,10 +14,6 @@ import {
|
||||
hasDurableExecApproval,
|
||||
resolveExecApprovalsFromFile,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import {
|
||||
describeInterpreterInlineEval,
|
||||
type InterpreterInlineEvalHit,
|
||||
} from "../infra/exec-inline-eval.js";
|
||||
import { buildNodeShellCommand } from "../infra/node-shell.js";
|
||||
import { parsePreparedSystemRunPayload } from "../infra/system-run-approval-context.js";
|
||||
import { formatExecCommand, resolveSystemRunCommandRequest } from "../infra/system-run-command.js";
|
||||
|
||||
@@ -92,7 +92,7 @@ vi.mock("../infra/exec-approvals.js", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../infra/exec-inline-eval.js", () => ({
|
||||
vi.mock("../infra/command-analysis/inline-eval.js", () => ({
|
||||
describeInterpreterInlineEval: vi.fn(() => "inline-eval"),
|
||||
detectInterpreterInlineEvalArgv: detectInterpreterInlineEvalArgvMock,
|
||||
}));
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
summarizeCommandSegmentsForDisplay,
|
||||
type CommandExplanationSummary,
|
||||
} from "../../infra/command-analysis/explain.js";
|
||||
import { analyzeCommandForPolicy } from "../../infra/command-analysis/policy.js";
|
||||
import { resolveCommandAnalysisSummaryForDisplay } from "../../infra/command-analysis/explain.js";
|
||||
import {
|
||||
resolveExecApprovalCommandDisplay,
|
||||
sanitizeExecApprovalDisplayText,
|
||||
@@ -47,43 +43,6 @@ const APPROVAL_ALLOW_ALWAYS_UNAVAILABLE_DETAILS = {
|
||||
} as const;
|
||||
const RESERVED_PLUGIN_APPROVAL_ID_PREFIX = "plugin:";
|
||||
|
||||
function sanitizeCommandAnalysisSummary(
|
||||
summary: CommandExplanationSummary,
|
||||
): CommandExplanationSummary {
|
||||
return {
|
||||
commandCount: summary.commandCount,
|
||||
nestedCommandCount: summary.nestedCommandCount,
|
||||
riskKinds: summary.riskKinds.map((kind) => sanitizeExecApprovalWarningText(kind)),
|
||||
warningLines: summary.warningLines.map((line) => sanitizeExecApprovalWarningText(line)),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveExecApprovalCommandAnalysis(params: {
|
||||
host: string;
|
||||
commandText: string;
|
||||
commandArgv?: string[];
|
||||
cwd?: string | null;
|
||||
}): CommandExplanationSummary | null {
|
||||
const analysis =
|
||||
Array.isArray(params.commandArgv) && params.commandArgv.length > 0
|
||||
? analyzeCommandForPolicy({
|
||||
source: "argv",
|
||||
argv: params.commandArgv,
|
||||
cwd: params.cwd ?? undefined,
|
||||
})
|
||||
: params.host === "node"
|
||||
? null
|
||||
: analyzeCommandForPolicy({
|
||||
source: "shell",
|
||||
command: params.commandText,
|
||||
cwd: params.cwd ?? undefined,
|
||||
});
|
||||
if (!analysis?.ok) {
|
||||
return null;
|
||||
}
|
||||
return sanitizeCommandAnalysisSummary(summarizeCommandSegmentsForDisplay(analysis.segments));
|
||||
}
|
||||
|
||||
type ExecApprovalIosPushDelivery = {
|
||||
handleRequested?: (request: ExecApprovalRequest) => Promise<boolean>;
|
||||
handleResolved?: (resolved: ExecApprovalResolved) => Promise<void>;
|
||||
@@ -249,11 +208,12 @@ export function createExecApprovalHandlers(
|
||||
}
|
||||
const envBinding = buildSystemRunApprovalEnvBinding(p.env);
|
||||
const warningText = normalizeOptionalString(p.warningText);
|
||||
const commandAnalysis = resolveExecApprovalCommandAnalysis({
|
||||
const commandAnalysis = resolveCommandAnalysisSummaryForDisplay({
|
||||
host,
|
||||
commandText: effectiveCommandText,
|
||||
commandArgv: effectiveCommandArgv,
|
||||
cwd: effectiveCwd,
|
||||
sanitizeText: sanitizeExecApprovalWarningText,
|
||||
});
|
||||
const systemRunBinding =
|
||||
host === "node"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { explainShellCommand } from "../command-explainer/index.js";
|
||||
import { summarizeCommandExplanation, summarizeCommandSegmentsForDisplay } from "./explain.js";
|
||||
import {
|
||||
resolveCommandAnalysisSummaryForDisplay,
|
||||
summarizeCommandExplanation,
|
||||
summarizeCommandSegmentsForDisplay,
|
||||
} from "./explain.js";
|
||||
|
||||
describe("command-analysis explanation summary", () => {
|
||||
it("summarizes commands and risk kinds", async () => {
|
||||
@@ -26,4 +30,33 @@ describe("command-analysis explanation summary", () => {
|
||||
expect(summary.riskKinds).toEqual(["inline-eval"]);
|
||||
expect(summary.warningLines).toEqual(["Contains inline-eval: python3 -c"]);
|
||||
});
|
||||
|
||||
it("resolves display summaries from argv or shell commands", () => {
|
||||
expect(
|
||||
resolveCommandAnalysisSummaryForDisplay({
|
||||
host: "gateway",
|
||||
commandText: "echo ok",
|
||||
commandArgv: ["python3", "-c", "print(1)"],
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
commandCount: 1,
|
||||
riskKinds: ["inline-eval"],
|
||||
warningLines: ["Contains inline-eval: python3 -c"],
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
resolveCommandAnalysisSummaryForDisplay({
|
||||
host: "node",
|
||||
commandText: "python3 -c 'print(1)'",
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveCommandAnalysisSummaryForDisplay({
|
||||
host: "gateway",
|
||||
commandText: "python3 -c 'print(1)'",
|
||||
sanitizeText: (value) => value.replaceAll("python3", "python"),
|
||||
})?.warningLines,
|
||||
).toEqual(["Contains inline-eval: python -c"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { explainShellCommand } from "../command-explainer/extract.js";
|
||||
import type { CommandExplanation, CommandRisk } from "../command-explainer/types.js";
|
||||
import type { ExecCommandSegment } from "../exec-approvals-analysis.js";
|
||||
import { analyzeCommandForPolicy } from "./policy.js";
|
||||
import { detectCommandCarrierArgv, detectInlineEvalInSegments } from "./risks.js";
|
||||
|
||||
export type CommandExplanationSummary = {
|
||||
@@ -80,6 +81,43 @@ export function summarizeCommandSegmentsForDisplay(
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCommandAnalysisSummaryForDisplay(params: {
|
||||
host?: string | null;
|
||||
commandText: string;
|
||||
commandArgv?: string[];
|
||||
cwd?: string | null;
|
||||
sanitizeText?: (value: string) => string;
|
||||
}): CommandExplanationSummary | null {
|
||||
const analysis =
|
||||
Array.isArray(params.commandArgv) && params.commandArgv.length > 0
|
||||
? analyzeCommandForPolicy({
|
||||
source: "argv",
|
||||
argv: params.commandArgv,
|
||||
cwd: params.cwd ?? undefined,
|
||||
})
|
||||
: params.host === "node"
|
||||
? null
|
||||
: analyzeCommandForPolicy({
|
||||
source: "shell",
|
||||
command: params.commandText,
|
||||
cwd: params.cwd ?? undefined,
|
||||
});
|
||||
if (!analysis?.ok) {
|
||||
return null;
|
||||
}
|
||||
const summary = summarizeCommandSegmentsForDisplay(analysis.segments);
|
||||
const sanitizeText = params.sanitizeText;
|
||||
if (!sanitizeText) {
|
||||
return summary;
|
||||
}
|
||||
return {
|
||||
commandCount: summary.commandCount,
|
||||
nestedCommandCount: summary.nestedCommandCount,
|
||||
riskKinds: summary.riskKinds.map((kind) => sanitizeText(kind)),
|
||||
warningLines: summary.warningLines.map((line) => sanitizeText(line)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function explainCommandForDisplay(
|
||||
command: string,
|
||||
): Promise<{ explanation: CommandExplanation; summary: CommandExplanationSummary } | null> {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
describeInterpreterInlineEval,
|
||||
detectInterpreterInlineEvalArgv,
|
||||
isInterpreterLikeAllowlistPattern,
|
||||
} from "./exec-inline-eval.js";
|
||||
} from "./inline-eval.js";
|
||||
|
||||
describe("exec inline eval detection", () => {
|
||||
it.each([
|
||||
@@ -1,5 +1,5 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { normalizeExecutableToken } from "./exec-wrapper-resolution.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { normalizeExecutableToken } from "../exec-wrapper-resolution.js";
|
||||
|
||||
export type InterpreterInlineEvalHit = {
|
||||
executable: string;
|
||||
@@ -1,15 +1,12 @@
|
||||
import { splitShellArgs } from "../../utils/shell-argv.js";
|
||||
import { unwrapKnownDispatchWrapperInvocation } from "../dispatch-wrapper-resolution.js";
|
||||
import type { ExecCommandSegment } from "../exec-approvals-analysis.js";
|
||||
import {
|
||||
detectInterpreterInlineEvalArgv,
|
||||
type InterpreterInlineEvalHit,
|
||||
} from "../exec-inline-eval.js";
|
||||
import { normalizeExecutableToken } from "../exec-wrapper-resolution.js";
|
||||
import {
|
||||
extractShellWrapperInlineCommand,
|
||||
isShellWrapperExecutable,
|
||||
} from "../shell-wrapper-resolution.js";
|
||||
import { detectInterpreterInlineEvalArgv, type InterpreterInlineEvalHit } from "./inline-eval.js";
|
||||
|
||||
export const COMMAND_CARRIER_EXECUTABLES = new Set(["sudo", "doas", "env", "command", "builtin"]);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Node as TreeSitterNode } from "web-tree-sitter";
|
||||
import type { InterpreterInlineEvalHit } from "../command-analysis/inline-eval.js";
|
||||
import {
|
||||
detectCarriedShellBuiltinArgv,
|
||||
detectCommandCarrierArgv,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
detectShellWrapperThroughCarrierArgv,
|
||||
SOURCE_EXECUTABLES,
|
||||
} from "../command-analysis/risks.js";
|
||||
import type { InterpreterInlineEvalHit } from "../exec-inline-eval.js";
|
||||
import { normalizeExecutableToken } from "../exec-wrapper-resolution.js";
|
||||
import {
|
||||
extractShellWrapperCommand,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { isInterpreterLikeAllowlistPattern } from "./command-analysis/inline-eval.js";
|
||||
import { detectInlineEvalArgv } from "./command-analysis/risks.js";
|
||||
import { isDispatchWrapperExecutable } from "./dispatch-wrapper-resolution.js";
|
||||
import {
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
type ShellChainOperator,
|
||||
} from "./exec-approvals-analysis.js";
|
||||
import type { ExecAllowlistEntry } from "./exec-approvals.types.js";
|
||||
import { isInterpreterLikeAllowlistPattern } from "./exec-inline-eval.js";
|
||||
import {
|
||||
DEFAULT_SAFE_BINS,
|
||||
SAFE_BIN_PROFILES,
|
||||
|
||||
@@ -1212,7 +1212,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
});
|
||||
|
||||
it("requires explicit approval for strict inline-eval carriers", async () => {
|
||||
// The full carrier matrix lives in exec-inline-eval.test.ts; this is the
|
||||
// The full carrier matrix lives in command-analysis tests; this is the
|
||||
// handle-level smoke for strictInlineEval denial wiring.
|
||||
const cases = [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { GatewayClient } from "../gateway/client.js";
|
||||
import {
|
||||
describeInterpreterInlineEval,
|
||||
type InterpreterInlineEvalHit,
|
||||
} from "../infra/command-analysis/inline-eval.js";
|
||||
import { detectPolicyInlineEval } from "../infra/command-analysis/policy.js";
|
||||
import {
|
||||
addDurableCommandApproval,
|
||||
@@ -15,10 +19,6 @@ import {
|
||||
type ExecSecurity,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import type { ExecHostRequest, ExecHostResponse, ExecHostRunResult } from "../infra/exec-host.js";
|
||||
import {
|
||||
describeInterpreterInlineEval,
|
||||
type InterpreterInlineEvalHit,
|
||||
} from "../infra/exec-inline-eval.js";
|
||||
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js";
|
||||
import {
|
||||
extractEnvAssignmentKeysFromDispatchWrappers,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/config.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||
import { isInterpreterLikeAllowlistPattern } from "../infra/command-analysis/inline-eval.js";
|
||||
import { type ExecApprovalsFile, loadExecApprovals } from "../infra/exec-approvals.js";
|
||||
import { isInterpreterLikeAllowlistPattern } from "../infra/exec-inline-eval.js";
|
||||
import {
|
||||
listInterpreterLikeSafeBins,
|
||||
resolveMergedSafeBinProfileFixtures,
|
||||
|
||||
Reference in New Issue
Block a user