import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" ;
type StrictInlineEvalBoundary =
typeof import ("./bash-tools.exec-host-shared.js" ).enforceStrictInlineEvalApprovalBoundary;
const INLINE_EVAL_HIT = {
executable: "python3" ,
normalizedExecutable: "python3" ,
flag: "-c" ,
argv: ["python3" , "-c" , "print(1)" ],
};
const createAndRegisterDefaultExecApprovalRequestMock = vi.hoisted(() => vi.fn());
const buildExecApprovalPendingToolResultMock = vi.hoisted(() => vi.fn());
const buildExecApprovalFollowupTargetMock = vi.hoisted(() => vi.fn(() => null ));
const createExecApprovalDecisionStateMock = vi.hoisted(() =>
vi.fn(
(): {
baseDecision: { timedOut: boolean };
approvedByAsk: boolean ;
deniedReason: string | null ;
} => ({
baseDecision: { timedOut: false },
approvedByAsk: false ,
deniedReason: "approval-required" ,
}),
),
);
const evaluateShellAllowlistMock = vi.hoisted(() =>
vi.fn(() => ({
allowlistMatches: [],
analysisOk: true ,
allowlistSatisfied: true ,
segments: [{ resolution: null , argv: ["echo" , "ok" ] }],
segmentAllowlistEntries: [{ pattern: "/usr/bin/echo" , source: "allow-always" }],
})),
);
const hasDurableExecApprovalMock = vi.hoisted(() => vi.fn(() => true ));
const buildEnforcedShellCommandMock = vi.hoisted(() =>
vi.fn((): { ok: boolean ; reason?: string; command?: string } => ({
ok: false ,
reason: "segment execution plan unavailable" ,
})),
);
const recordAllowlistMatchesUseMock = vi.hoisted(() => vi.fn());
const resolveApprovalDecisionOrUndefinedMock = vi.hoisted(() =>
vi.fn(async (): Promise<string | null | undefined> => undefined),
);
const resolveExecHostApprovalContextMock = vi.hoisted(() =>
vi.fn(() => ({
approvals: { allowlist: [], file: { version: 1 , agents: {} } },
hostSecurity: "allowlist" ,
hostAsk: "off" ,
askFallback: "deny" ,
})),
);
const runExecProcessMock = vi.hoisted(() => vi.fn());
const sendExecApprovalFollowupResultMock = vi.hoisted(() => vi.fn(async () => undefined));
const enforceStrictInlineEvalApprovalBoundaryMock = vi.hoisted(() =>
vi.fn<StrictInlineEvalBoundary>((value) => ({
approvedByAsk: value.approvedByAsk,
deniedReason: value.deniedReason,
})),
);
const detectInterpreterInlineEvalArgvMock = vi.hoisted(() =>
vi.fn(
(): {
executable: string;
normalizedExecutable: string;
flag: string;
argv: string[];
} | null => null ,
),
);
vi.mock("../infra/exec-approvals.js" , () => ({
evaluateShellAllowlist: evaluateShellAllowlistMock,
hasDurableExecApproval: hasDurableExecApprovalMock,
buildEnforcedShellCommand: buildEnforcedShellCommandMock,
requiresExecApproval: vi.fn(() => false ),
recordAllowlistUse: vi.fn(),
recordAllowlistMatchesUse: recordAllowlistMatchesUseMock,
resolveApprovalAuditCandidatePath: vi.fn(() => null ),
resolveAllowAlwaysPatterns: vi.fn(() => []),
resolveExecApprovalAllowedDecisions: vi.fn(() => ["allow-once" , "allow-always" , "deny" ]),
addAllowlistEntry: vi.fn(),
addDurableCommandApproval: vi.fn(),
}));
vi.mock("./bash-tools.exec-approval-request.js" , () => ({
buildExecApprovalRequesterContext: vi.fn(() => ({})),
buildExecApprovalTurnSourceContext: vi.fn(() => ({})),
registerExecApprovalRequestForHostOrThrow: vi.fn(async () => undefined),
}));
vi.mock("./bash-tools.exec-host-shared.js" , () => ({
resolveExecHostApprovalContext: resolveExecHostApprovalContextMock,
buildDefaultExecApprovalRequestArgs: vi.fn(() => ({})),
buildHeadlessExecApprovalDeniedMessage: vi.fn(() => "denied" ),
buildExecApprovalFollowupTarget: buildExecApprovalFollowupTargetMock,
buildExecApprovalPendingToolResult: buildExecApprovalPendingToolResultMock,
createExecApprovalDecisionState: createExecApprovalDecisionStateMock,
createAndRegisterDefaultExecApprovalRequest: createAndRegisterDefaultExecApprovalRequestMock,
enforceStrictInlineEvalApprovalBoundary: enforceStrictInlineEvalApprovalBoundaryMock,
resolveApprovalDecisionOrUndefined: resolveApprovalDecisionOrUndefinedMock,
sendExecApprovalFollowupResult: sendExecApprovalFollowupResultMock,
shouldResolveExecApprovalUnavailableInline: vi.fn(() => false ),
}));
vi.mock("./bash-tools.exec-runtime.js" , () => ({
DEFAULT_NOTIFY_TAIL_CHARS: 1000 ,
createApprovalSlug: vi.fn(() => "slug" ),
normalizeNotifyOutput: vi.fn((value) => value),
runExecProcess: runExecProcessMock,
}));
vi.mock("./bash-process-registry.js" , () => ({
markBackgrounded: vi.fn(),
tail: vi.fn((value) => value),
}));
vi.mock("../infra/exec-inline-eval.js" , () => ({
describeInterpreterInlineEval: vi.fn(() => "python -c" ),
detectInterpreterInlineEvalArgv: detectInterpreterInlineEvalArgvMock,
}));
let processGatewayAllowlist: typeof import ("./bash-tools.exec-host-gateway.js" ).processGatewayAllowlist;
type GatewayAllowlistParams = Parameters<typeof processGatewayAllowlist>[0 ];
describe("processGatewayAllowlist" , () => {
beforeAll(async () => {
({ processGatewayAllowlist } = await import ("./bash-tools.exec-host-gateway.js" ));
});
beforeEach(() => {
buildExecApprovalPendingToolResultMock.mockReset();
buildExecApprovalFollowupTargetMock.mockReset();
buildExecApprovalFollowupTargetMock.mockReturnValue(null );
createExecApprovalDecisionStateMock.mockReset();
createExecApprovalDecisionStateMock.mockReturnValue({
baseDecision: { timedOut: false },
approvedByAsk: false ,
deniedReason: "approval-required" ,
});
evaluateShellAllowlistMock.mockReset();
evaluateShellAllowlistMock.mockReturnValue({
allowlistMatches: [],
analysisOk: true ,
allowlistSatisfied: true ,
segments: [{ resolution: null , argv: ["echo" , "ok" ] }],
segmentAllowlistEntries: [{ pattern: "/usr/bin/echo" , source: "allow-always" }],
});
hasDurableExecApprovalMock.mockReset();
hasDurableExecApprovalMock.mockReturnValue(true );
buildEnforcedShellCommandMock.mockReset();
buildEnforcedShellCommandMock.mockReturnValue({
ok: false ,
reason: "segment execution plan unavailable" ,
});
recordAllowlistMatchesUseMock.mockReset();
resolveApprovalDecisionOrUndefinedMock.mockReset();
resolveApprovalDecisionOrUndefinedMock.mockResolvedValue(undefined);
resolveExecHostApprovalContextMock.mockReset();
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1 , agents: {} } },
hostSecurity: "allowlist" ,
hostAsk: "off" ,
askFallback: "deny" ,
});
runExecProcessMock.mockReset();
sendExecApprovalFollowupResultMock.mockReset();
enforceStrictInlineEvalApprovalBoundaryMock.mockReset();
enforceStrictInlineEvalApprovalBoundaryMock.mockImplementation((value) => ({
approvedByAsk: value.approvedByAsk,
deniedReason: value.deniedReason,
}));
detectInterpreterInlineEvalArgvMock.mockReset();
detectInterpreterInlineEvalArgvMock.mockReturnValue(null );
buildExecApprovalPendingToolResultMock.mockReturnValue({
details: { status: "approval-pending" },
content: [],
});
createAndRegisterDefaultExecApprovalRequestMock.mockReset();
createAndRegisterDefaultExecApprovalRequestMock.mockResolvedValue({
approvalId: "req-1" ,
approvalSlug: "slug-1" ,
warningText: "" ,
expiresAtMs: Date.now() + 60 _000 ,
preResolvedDecision: null ,
initiatingSurface: "origin" ,
sentApproverDms: false ,
unavailableReason: null ,
});
});
function runGatewayAllowlist(
overrides: Partial<GatewayAllowlistParams> & Pick<GatewayAllowlistParams, "command" >,
) {
const { command, ...rest } = overrides;
return processGatewayAllowlist({
command,
workdir: process.cwd(),
env: process.env as Record<string, string>,
pty: false ,
defaultTimeoutSec: 30 ,
security: "allowlist" ,
ask: "off" ,
safeBins: new Set(),
safeBinProfiles: {},
warnings: [],
approvalRunningNoticeMs: 0 ,
maxOutput: 1000 ,
pendingMaxOutput: 1000 ,
...rest,
});
}
async function runTimedOutStrictInlineEval(params: {
security: "full" | "allowlist" ;
askFallback: "full" | "allowlist" ;
approvedByAsk: boolean ;
}) {
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1 , agents: {} } },
hostSecurity: params.security,
hostAsk: "always" ,
askFallback: params.askFallback,
});
detectInterpreterInlineEvalArgvMock.mockReturnValue(INLINE_EVAL_HIT);
resolveApprovalDecisionOrUndefinedMock.mockResolvedValue(null );
createExecApprovalDecisionStateMock.mockReturnValue({
baseDecision: { timedOut: true },
approvedByAsk: params.approvedByAsk,
deniedReason: null ,
});
enforceStrictInlineEvalApprovalBoundaryMock.mockReturnValue({
approvedByAsk: false ,
deniedReason: "approval-timeout" ,
});
return runGatewayAllowlist({
command: "python3 -c 'print(1)'" ,
security: params.security,
ask: "always" ,
strictInlineEval: true ,
});
}
it("still requires approval when allowlist execution plan is unavailable despite durable trust" , async () => {
const result = await runGatewayAllowlist({
command: "echo ok" ,
});
expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1 );
expect(result.pendingResult?.details.status).toBe("approval-pending" );
});
it("allows durable exact-command trust to bypass the synchronous allowlist miss" , async () => {
evaluateShellAllowlistMock.mockReturnValue({
allowlistMatches: [],
analysisOk: false ,
allowlistSatisfied: false ,
segments: [{ resolution: null , argv: ["node" , "--version" ] }],
segmentAllowlistEntries: [],
});
hasDurableExecApprovalMock.mockReturnValue(true );
buildEnforcedShellCommandMock.mockReturnValue({
ok: true ,
command: "node --version" ,
});
const result = await runGatewayAllowlist({
command: "node --version" ,
});
expect(createAndRegisterDefaultExecApprovalRequestMock).not.toHaveBeenCalled();
expect(result).toEqual({ execCommandOverride: undefined });
});
it("keeps denying allowlist misses when durable trust does not match" , async () => {
evaluateShellAllowlistMock.mockReturnValue({
allowlistMatches: [],
analysisOk: false ,
allowlistSatisfied: false ,
segments: [{ resolution: null , argv: ["node" , "--version" ] }],
segmentAllowlistEntries: [],
});
hasDurableExecApprovalMock.mockReturnValue(false );
await expect(
runGatewayAllowlist({
command: "node --version" ,
}),
).rejects.toThrow("exec denied: allowlist miss" );
});
it("uses sessionKey for followups when notifySessionKey is absent" , async () => {
await runGatewayAllowlist({
command: "echo ok" ,
sessionKey: "agent:main:telegram:direct:123" ,
});
expect(buildExecApprovalFollowupTargetMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "agent:main:telegram:direct:123" ,
}),
);
});
it("denies timed-out inline-eval requests instead of auto-running them" , async () => {
const result = await runTimedOutStrictInlineEval({
security: "full" ,
askFallback: "full" ,
approvedByAsk: true ,
});
expect(result.pendingResult?.details.status).toBe("approval-pending" );
await vi.waitFor(() => {
expect(sendExecApprovalFollowupResultMock).toHaveBeenCalledWith(
null ,
"Exec denied (gateway id=req-1, approval-timeout): python3 -c 'print(1)'" ,
);
});
expect(runExecProcessMock).not.toHaveBeenCalled();
});
it("denies allowlist timeout fallback for strict inline-eval commands" , async () => {
const result = await runTimedOutStrictInlineEval({
security: "allowlist" ,
askFallback: "allowlist" ,
approvedByAsk: false ,
});
expect(result.pendingResult?.details.status).toBe("approval-pending" );
await vi.waitFor(() => {
expect(sendExecApprovalFollowupResultMock).toHaveBeenCalledWith(
null ,
"Exec denied (gateway id=req-1, approval-timeout): python3 -c 'print(1)'" ,
);
});
expect(runExecProcessMock).not.toHaveBeenCalled();
});
});
Messung V0.5 in Prozent C=100 H=97 G=98
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-05-26)
¤
*© Formatika GbR, Deutschland