fix(exec): bind node auto-review to prepared plans

This commit is contained in:
Peter Steinberger
2026-05-29 22:01:07 +02:00
parent e2966faea7
commit b3dc7a4a80
4 changed files with 828 additions and 50 deletions

View File

@@ -66,6 +66,7 @@ const resolveExecApprovalsFromFileMock = vi.hoisted(() =>
})),
);
const requiresExecApprovalMock = vi.hoisted(() => vi.fn(() => true));
const hasDurableExecApprovalMock = vi.hoisted(() => vi.fn(() => false));
const resolveExecHostApprovalContextMock = vi.hoisted(() =>
vi.fn(() => ({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
@@ -117,7 +118,7 @@ vi.mock("../infra/exec-approvals.js", () => ({
evaluateShellAllowlist: evaluateShellAllowlistMock,
commandRequiresSecurityAuditSuppressionApproval:
commandRequiresSecurityAuditSuppressionApprovalMock,
hasDurableExecApproval: vi.fn(() => false),
hasDurableExecApproval: hasDurableExecApprovalMock,
requiresExecApproval: requiresExecApprovalMock,
resolveExecApprovalAllowedDecisions: vi.fn(() => ["allow-once", "allow-always", "deny"]),
resolveExecApprovalsFromFile: resolveExecApprovalsFromFileMock,
@@ -318,6 +319,8 @@ describe("executeNodeHostCommand", () => {
});
requiresExecApprovalMock.mockReset();
requiresExecApprovalMock.mockReturnValue(true);
hasDurableExecApprovalMock.mockReset();
hasDurableExecApprovalMock.mockReturnValue(false);
resolveExecHostApprovalContextMock.mockReset();
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
@@ -425,6 +428,10 @@ describe("executeNodeHostCommand", () => {
hostAsk: "on-miss",
askFallback: "deny",
});
requiresExecApprovalMock.mockImplementation(
(params?: { allowlistSatisfied?: boolean; durableApprovalSatisfied?: boolean }) =>
params?.allowlistSatisfied !== true && params?.durableApprovalSatisfied !== true,
);
const result = await executeNodeHostCommand({
command: "bun ./script.ts",
@@ -466,6 +473,614 @@ describe("executeNodeHostCommand", () => {
);
});
it("reviews the prepared node plan before suppressing human approval", async () => {
const divergentPlan = {
argv: ["rm", "-rf", "/tmp/work"],
cwd: "/tmp/work",
commandText: "rm -rf /tmp/work",
commandPreview: "./scripts/check_mail.sh --limit 5",
agentId: "prepared-agent",
sessionKey: "prepared-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: divergentPlan,
execPolicy: { security: "full", ask: "off" },
});
const nodeAllowlist = [{ pattern: "./scripts/check_mail.sh" }];
resolveExecApprovalsFromFileMock.mockReturnValue({
allowlist: nodeAllowlist,
file: { version: 1, agents: {} },
agent: {
security: "full",
ask: "off",
askFallback: "deny",
autoAllowSkills: false,
},
});
evaluateShellAllowlistMock.mockImplementation(
(params?: { command?: string; allowlist?: unknown[] }) => {
const command = params?.command ?? "";
const hasNodeAllowlist = Array.isArray(params?.allowlist) && params.allowlist.length > 0;
const previewMatch = command === "./scripts/check_mail.sh --limit 5";
return {
allowlistMatches: previewMatch && hasNodeAllowlist ? [{}] : [],
analysisOk: true,
allowlistSatisfied: previewMatch && hasNodeAllowlist,
segments: [
previewMatch
? {
resolution: null,
argv: ["./scripts/check_mail.sh", "--limit", "5"],
raw: "./scripts/check_mail.sh --limit 5",
}
: {
resolution: null,
argv: ["rm", "-rf", "/tmp/work"],
raw: "rm -rf /tmp/work",
},
],
segmentAllowlistEntries: previewMatch && hasNodeAllowlist ? [{}] : [],
};
},
);
const autoReviewer = vi.fn<ExecAutoReviewer>(async (input) =>
input.command.includes("rm -rf")
? {
decision: "ask",
risk: "high",
rationale: "destructive prepared plan",
}
: {
decision: "allow-once",
risk: "low",
rationale: "safe requested text",
},
);
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
hostSecurity: "allowlist",
hostAsk: "on-miss",
askFallback: "deny",
});
requiresExecApprovalMock.mockImplementation(
(params?: { allowlistSatisfied?: boolean; durableApprovalSatisfied?: boolean }) =>
params?.allowlistSatisfied !== true && params?.durableApprovalSatisfied !== true,
);
const result = await executeNodeHostCommand({
command: "echo SAFE",
workdir: "/tmp/work",
env: {},
security: "allowlist",
ask: "on-miss",
autoReview: true,
autoReviewer,
defaultTimeoutSec: 30,
approvalRunningNoticeMs: 0,
warnings: [],
agentId: "requested-agent",
sessionKey: "requested-session",
});
expect(result.details?.status).toBe("approval-pending");
expect(autoReviewer).toHaveBeenCalledWith(
expect.objectContaining({
command: "rm -rf /tmp/work",
argv: ["rm", "-rf", "/tmp/work"],
agent: {
id: "prepared-agent",
sessionKey: "prepared-session",
},
}),
);
expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalled();
expect(callGatewayToolMock).not.toHaveBeenCalledWith(
"exec.approval.resolve",
expect.anything(),
expect.anything(),
expect.anything(),
);
});
it("honors node allowlist matches on prepared POSIX shell payloads", async () => {
const wrapperPlan = {
argv: ["/bin/sh", "-lc", "./scripts/check_mail.sh --limit 5"],
cwd: "/tmp/work",
commandText: `/bin/sh -lc "./scripts/check_mail.sh --limit 5"`,
commandPreview: "./scripts/check_mail.sh --limit 5",
agentId: "prepared-agent",
sessionKey: "prepared-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: wrapperPlan,
execPolicy: { security: "full", ask: "off" },
});
const nodeAllowlist = [{ pattern: "./scripts/check_mail.sh" }];
resolveExecApprovalsFromFileMock.mockReturnValue({
allowlist: nodeAllowlist,
file: { version: 1, agents: {} },
agent: {
security: "full",
ask: "off",
askFallback: "deny",
autoAllowSkills: false,
},
});
evaluateShellAllowlistMock.mockImplementation(
(params?: { command?: string; allowlist?: unknown[] }) => {
const command = params?.command ?? "";
const hasNodeAllowlist = Array.isArray(params?.allowlist) && params.allowlist.length > 0;
const semanticMatch = command === "./scripts/check_mail.sh --limit 5";
return {
allowlistMatches: semanticMatch && hasNodeAllowlist ? [{}] : [],
analysisOk: true,
allowlistSatisfied: semanticMatch && hasNodeAllowlist,
segments: [
command.startsWith("/bin/sh")
? {
resolution: null,
argv: ["/bin/sh", "-lc", "./scripts/check_mail.sh --limit 5"],
raw: `/bin/sh -lc "./scripts/check_mail.sh --limit 5"`,
}
: {
resolution: null,
argv: ["./scripts/check_mail.sh", "--limit", "5"],
raw: "./scripts/check_mail.sh --limit 5",
},
],
segmentAllowlistEntries: semanticMatch && hasNodeAllowlist ? [{}] : [],
};
},
);
const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
decision: "ask",
risk: "medium",
rationale: "should not be needed",
}));
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
hostSecurity: "allowlist",
hostAsk: "on-miss",
askFallback: "deny",
});
requiresExecApprovalMock.mockImplementation(
(params?: { allowlistSatisfied?: boolean; durableApprovalSatisfied?: boolean }) =>
params?.allowlistSatisfied !== true && params?.durableApprovalSatisfied !== true,
);
const result = await executeNodeHostCommand({
command: "./scripts/check_mail.sh --limit 5",
workdir: "/tmp/work",
env: {},
security: "allowlist",
ask: "on-miss",
autoReview: true,
autoReviewer,
defaultTimeoutSec: 30,
approvalRunningNoticeMs: 0,
warnings: [],
agentId: "requested-agent",
sessionKey: "requested-session",
});
expect(result.details?.status).toBe("completed");
expect(autoReviewer).not.toHaveBeenCalled();
expect(createAndRegisterDefaultExecApprovalRequestMock).not.toHaveBeenCalled();
expect(resolveExecApprovalsFromFileMock).toHaveBeenCalledWith(
expect.objectContaining({ agentId: "prepared-agent" }),
);
expectSystemRunInvoke({ invokeTimeoutMs: 35_000, runTimeoutMs: 30_000 });
});
it("does not let transport wrapper allowlist matches approve shell payloads", async () => {
const wrapperPlan = {
argv: ["/bin/sh", "-lc", "./scripts/untrusted.sh"],
cwd: "/tmp/work",
commandText: `/bin/sh -lc "./scripts/untrusted.sh"`,
commandPreview: "./scripts/untrusted.sh",
agentId: "prepared-agent",
sessionKey: "prepared-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: wrapperPlan,
execPolicy: { security: "full", ask: "off" },
});
const nodeAllowlist = [{ pattern: "/bin/sh" }];
resolveExecApprovalsFromFileMock.mockReturnValue({
allowlist: nodeAllowlist,
file: { version: 1, agents: {} },
agent: {
security: "full",
ask: "off",
askFallback: "deny",
autoAllowSkills: false,
},
});
evaluateShellAllowlistMock.mockImplementation(
(params?: { command?: string; allowlist?: unknown[] }) => {
const command = params?.command ?? "";
const hasNodeAllowlist = Array.isArray(params?.allowlist) && params.allowlist.length > 0;
const wrapperMatch = command.startsWith("/bin/sh") && hasNodeAllowlist;
return {
allowlistMatches: wrapperMatch ? [{}] : [],
analysisOk: true,
allowlistSatisfied: wrapperMatch,
segments: [
command.startsWith("/bin/sh")
? {
resolution: null,
argv: ["/bin/sh", "-lc", "./scripts/untrusted.sh"],
raw: `/bin/sh -lc "./scripts/untrusted.sh"`,
}
: {
resolution: null,
argv: ["./scripts/untrusted.sh"],
raw: "./scripts/untrusted.sh",
},
],
segmentAllowlistEntries: wrapperMatch ? [{}] : [],
};
},
);
requiresExecApprovalMock.mockImplementation(
(params?: { allowlistSatisfied?: boolean; durableApprovalSatisfied?: boolean }) =>
params?.allowlistSatisfied !== true && params?.durableApprovalSatisfied !== true,
);
hasDurableExecApprovalMock.mockImplementation(
(params?: { segmentAllowlistEntries?: unknown[] }) =>
Array.isArray(params?.segmentAllowlistEntries) && params.segmentAllowlistEntries.length > 0,
);
const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
decision: "ask",
risk: "medium",
rationale: "inner payload is not allowlisted",
}));
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
hostSecurity: "allowlist",
hostAsk: "on-miss",
askFallback: "deny",
});
const result = await executeNodeHostCommand({
command: "./scripts/untrusted.sh",
workdir: "/tmp/work",
env: {},
security: "allowlist",
ask: "on-miss",
autoReview: true,
autoReviewer,
defaultTimeoutSec: 30,
approvalRunningNoticeMs: 0,
warnings: [],
agentId: "requested-agent",
sessionKey: "requested-session",
});
expect(result.details?.status).toBe("approval-pending");
expect(autoReviewer).toHaveBeenCalledWith(
expect.objectContaining({
command: `/bin/sh -lc "./scripts/untrusted.sh"`,
argv: ["./scripts/untrusted.sh"],
}),
);
expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalled();
});
it("reuses exact durable approvals for prepared shell wrappers", async () => {
const wrapperPlan = {
argv: ["/bin/sh", "-lc", "cd ."],
cwd: "/tmp/work",
commandText: `/bin/sh -lc "cd ."`,
commandPreview: "cd .",
agentId: "prepared-agent",
sessionKey: "prepared-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: wrapperPlan,
execPolicy: { security: "full", ask: "off" },
});
resolveExecApprovalsFromFileMock.mockReturnValue({
allowlist: [
{
pattern: "=command:placeholder",
source: "allow-always",
commandText: wrapperPlan.commandText,
},
],
file: { version: 1, agents: {} },
agent: {
security: "full",
ask: "off",
askFallback: "deny",
autoAllowSkills: false,
},
});
evaluateShellAllowlistMock.mockImplementation((params?: { command?: string }) => {
const command = params?.command ?? "";
return {
allowlistMatches: [],
analysisOk: true,
allowlistSatisfied: false,
segments: [
command.startsWith("/bin/sh")
? {
resolution: null,
argv: ["/bin/sh", "-lc", "cd ."],
raw: `/bin/sh -lc "cd ."`,
}
: {
resolution: null,
argv: ["cd", "."],
raw: "cd .",
},
],
segmentAllowlistEntries: [],
};
});
hasDurableExecApprovalMock.mockImplementation(
(params?: { commandText?: string | null }) => params?.commandText === wrapperPlan.commandText,
);
requiresExecApprovalMock.mockImplementation(
(params?: { allowlistSatisfied?: boolean; durableApprovalSatisfied?: boolean }) =>
params?.allowlistSatisfied !== true && params?.durableApprovalSatisfied !== true,
);
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
hostSecurity: "allowlist",
hostAsk: "on-miss",
askFallback: "deny",
});
const result = await executeNodeHostCommand({
command: "cd .",
workdir: "/tmp/work",
env: {},
security: "allowlist",
ask: "on-miss",
autoReview: true,
defaultTimeoutSec: 30,
approvalRunningNoticeMs: 0,
warnings: [],
agentId: "requested-agent",
sessionKey: "requested-session",
});
expect(result.details?.status).toBe("completed");
expect(createAndRegisterDefaultExecApprovalRequestMock).not.toHaveBeenCalled();
expectSystemRunInvoke({ invokeTimeoutMs: 35_000, runTimeoutMs: 30_000 });
});
it("keeps non-transport login shells approval-gated", async () => {
const loginPlan = {
argv: ["bash", "-lc", "./scripts/check_mail.sh --limit 5"],
cwd: "/tmp/work",
commandText: `bash -lc "./scripts/check_mail.sh --limit 5"`,
commandPreview: "./scripts/check_mail.sh --limit 5",
agentId: "prepared-agent",
sessionKey: "prepared-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: loginPlan,
execPolicy: { security: "full", ask: "off" },
});
const nodeAllowlist = [{ pattern: "./scripts/check_mail.sh" }];
resolveExecApprovalsFromFileMock.mockReturnValue({
allowlist: nodeAllowlist,
file: { version: 1, agents: {} },
agent: {
security: "full",
ask: "off",
askFallback: "deny",
autoAllowSkills: false,
},
});
evaluateShellAllowlistMock.mockImplementation(
(params?: { command?: string; allowlist?: unknown[] }) => {
const command = params?.command ?? "";
const hasNodeAllowlist = Array.isArray(params?.allowlist) && params.allowlist.length > 0;
const semanticMatch = command === "./scripts/check_mail.sh --limit 5";
return {
allowlistMatches: semanticMatch && hasNodeAllowlist ? [{}] : [],
analysisOk: true,
allowlistSatisfied: semanticMatch && hasNodeAllowlist,
segments: [
command.startsWith("bash")
? {
resolution: null,
argv: ["bash", "-lc", "./scripts/check_mail.sh --limit 5"],
raw: `bash -lc "./scripts/check_mail.sh --limit 5"`,
}
: {
resolution: null,
argv: ["./scripts/check_mail.sh", "--limit", "5"],
raw: "./scripts/check_mail.sh --limit 5",
},
],
segmentAllowlistEntries: semanticMatch && hasNodeAllowlist ? [{}] : [],
};
},
);
requiresExecApprovalMock.mockImplementation(
(params?: { allowlistSatisfied?: boolean; durableApprovalSatisfied?: boolean }) =>
params?.allowlistSatisfied !== true && params?.durableApprovalSatisfied !== true,
);
const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
decision: "ask",
risk: "medium",
rationale: "login shell needs human approval",
}));
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
hostSecurity: "allowlist",
hostAsk: "on-miss",
askFallback: "deny",
});
const result = await executeNodeHostCommand({
command: "./scripts/check_mail.sh --limit 5",
workdir: "/tmp/work",
env: {},
security: "allowlist",
ask: "on-miss",
autoReview: true,
autoReviewer,
defaultTimeoutSec: 30,
approvalRunningNoticeMs: 0,
warnings: [],
agentId: "requested-agent",
sessionKey: "requested-session",
});
expect(result.details?.status).toBe("approval-pending");
expect(autoReviewer).toHaveBeenCalledWith(
expect.objectContaining({
command: `bash -lc "./scripts/check_mail.sh --limit 5"`,
argv: ["bash", "-lc", "./scripts/check_mail.sh --limit 5"],
}),
);
expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalled();
});
it("requires human approval when prepared shell payload has multiple commands", async () => {
const chainPlan = {
argv: ["/bin/sh", "-lc", "openclaw status; id"],
cwd: "/tmp/work",
commandText: `/bin/sh -lc "openclaw status; id"`,
commandPreview: "openclaw status; id",
agentId: "prepared-agent",
sessionKey: "prepared-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: chainPlan,
execPolicy: { security: "full", ask: "off" },
});
evaluateShellAllowlistMock.mockImplementation((params?: { command?: string }) => {
const command = params?.command ?? "";
return {
allowlistMatches: [],
analysisOk: true,
allowlistSatisfied: false,
segments: command.startsWith("/bin/sh")
? [
{
resolution: null,
argv: ["/bin/sh", "-lc", "openclaw status; id"],
raw: `/bin/sh -lc "openclaw status; id"`,
},
]
: [
{
resolution: null,
argv: ["openclaw", "status"],
raw: "openclaw status",
},
{
resolution: null,
argv: ["id"],
raw: "id",
},
],
segmentAllowlistEntries: [],
};
});
const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
decision: "allow-once",
risk: "low",
rationale: "test reviewer would allow it",
}));
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
hostSecurity: "allowlist",
hostAsk: "on-miss",
askFallback: "deny",
});
const result = await executeNodeHostCommand({
command: "openclaw status; id",
workdir: "/tmp/work",
env: {},
security: "allowlist",
ask: "on-miss",
autoReview: true,
autoReviewer,
defaultTimeoutSec: 30,
approvalRunningNoticeMs: 0,
warnings: [],
agentId: "requested-agent",
sessionKey: "requested-session",
});
expect(result.details?.status).toBe("approval-pending");
expect(autoReviewer).not.toHaveBeenCalled();
expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalled();
});
it("does not treat read-only suppression inspections as wrapper writes", async () => {
const wrapperPlan = {
argv: ["/bin/sh", "-lc", "openclaw config get security.audit.suppressions"],
cwd: "/tmp/work",
commandText: `/bin/sh -lc "openclaw config get security.audit.suppressions"`,
commandPreview: "openclaw config get security.audit.suppressions",
agentId: "prepared-agent",
sessionKey: "prepared-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: wrapperPlan,
execPolicy: { security: "full", ask: "off" },
});
evaluateShellAllowlistMock.mockImplementation((params?: { command?: string }) => {
const command = params?.command ?? "";
return {
allowlistMatches: [],
analysisOk: true,
allowlistSatisfied: true,
segments: [
command.startsWith("/bin/sh")
? {
resolution: null,
argv: ["/bin/sh", "-lc", "openclaw config get security.audit.suppressions"],
raw: `/bin/sh -lc "openclaw config get security.audit.suppressions"`,
}
: {
resolution: null,
argv: ["openclaw", "config", "get", "security.audit.suppressions"],
raw: "openclaw config get security.audit.suppressions",
},
],
segmentAllowlistEntries: [],
};
});
commandRequiresSecurityAuditSuppressionApprovalMock.mockImplementation(
(params?: { command?: string }) => params?.command?.startsWith("/bin/sh") === true,
);
requiresExecApprovalMock.mockReturnValue(false);
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
hostSecurity: "allowlist",
hostAsk: "on-miss",
askFallback: "deny",
});
const result = await executeNodeHostCommand({
command: "openclaw config get security.audit.suppressions",
workdir: "/tmp/work",
env: {},
security: "allowlist",
ask: "on-miss",
autoReview: true,
defaultTimeoutSec: 30,
approvalRunningNoticeMs: 0,
warnings: [],
agentId: "requested-agent",
sessionKey: "requested-session",
});
expect(result.details?.status).toBe("completed");
expect(createAndRegisterDefaultExecApprovalRequestMock).not.toHaveBeenCalled();
expectSystemRunInvoke({ invokeTimeoutMs: 35_000, runTimeoutMs: 30_000 });
});
it("requests human approval when node auto-review asks on an approval miss", async () => {
const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
decision: "ask",
@@ -744,24 +1359,46 @@ describe("executeNodeHostCommand", () => {
});
it("auto-reviews strict inline-eval commands before asking a human", async () => {
const inlinePlan = {
argv: ["/bin/sh", "-lc", "python3 -c 'print(1)'"],
cwd: "/tmp/work",
commandText: `/bin/sh -lc "python3 -c 'print(1)'"`,
commandPreview: "python3 -c 'print(1)'",
agentId: "requested-agent",
sessionKey: "requested-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: inlinePlan,
execPolicy: { security: "full", ask: "off" },
});
const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
decision: "allow-once",
risk: "low",
rationale: "safe inline eval",
}));
detectInterpreterInlineEvalArgvMock.mockReturnValue(INLINE_EVAL_HIT);
evaluateShellAllowlistMock.mockReturnValue({
allowlistMatches: [],
analysisOk: true,
allowlistSatisfied: false,
segments: [
{
resolution: null,
argv: ["python3", "-c", "print(1)"],
raw: "python3 -c 'print(1)'",
},
],
segmentAllowlistEntries: [],
detectInterpreterInlineEvalArgvMock.mockImplementation((argv?: unknown) =>
Array.isArray(argv) && argv[0] === "python3" ? INLINE_EVAL_HIT : null,
);
evaluateShellAllowlistMock.mockImplementation((params?: { command?: string }) => {
const command = params?.command ?? "";
const segment = command.startsWith("/bin/sh")
? {
resolution: null,
argv: ["/bin/sh", "-lc", "python3 -c 'print(1)'"],
raw: `/bin/sh -lc "python3 -c 'print(1)'"`,
}
: {
resolution: null,
argv: ["python3", "-c", "print(1)"],
raw: "python3 -c 'print(1)'",
};
return {
allowlistMatches: [],
analysisOk: true,
allowlistSatisfied: false,
segments: [segment],
segmentAllowlistEntries: [],
};
});
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1, agents: {} } },
@@ -790,7 +1427,7 @@ describe("executeNodeHostCommand", () => {
expect(result.details?.status).toBe("completed");
expect(autoReviewer).toHaveBeenCalledWith(
expect.objectContaining({
command: "python3 -c 'print(1)'",
command: `/bin/sh -lc "python3 -c 'print(1)'"`,
argv: ["python3", "-c", "print(1)"],
host: "node",
reason: "strict-inline-eval",
@@ -1273,6 +1910,18 @@ describe("executeNodeHostCommand", () => {
});
it("auto-reviews strict inline-eval commands with full/off host policy when node policy is available", async () => {
const inlinePlan = {
argv: ["python3", "-c", "print(1)"],
cwd: "/tmp/work",
commandText: "python3 -c 'print(1)'",
commandPreview: null,
agentId: "requested-agent",
sessionKey: "requested-session",
};
parsePreparedSystemRunPayloadMock.mockReturnValue({
plan: inlinePlan,
execPolicy: { security: "full", ask: "off" },
});
const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
decision: "allow-once",
risk: "low",