mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: add pure ssh2 remote operations
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
applyShellDecisionFloor,
|
||||
SHELL_TOOL_DECISION_FLOOR_PATTERN,
|
||||
} from "#src/handlers/gates/shell-decision-floor";
|
||||
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
||||
|
||||
function check(state: PermissionState, source: PermissionCheckResult["source"] = "bash") {
|
||||
return {
|
||||
state,
|
||||
toolName: "bash",
|
||||
source,
|
||||
origin: source === "session" ? ("session" as const) : ("global" as const),
|
||||
command: "pwd",
|
||||
matchedPattern: source === "session" ? "pwd" : "*",
|
||||
} satisfies PermissionCheckResult;
|
||||
}
|
||||
|
||||
describe("applyShellDecisionFloor", () => {
|
||||
it("raises allow to ask", () => {
|
||||
expect(applyShellDecisionFloor(check("allow"), "ask")).toEqual({
|
||||
...check("allow"),
|
||||
state: "ask",
|
||||
source: "bash",
|
||||
matchedPattern: SHELL_TOOL_DECISION_FLOOR_PATTERN,
|
||||
reason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("raises a session allow to ask without retaining the session fast path", () => {
|
||||
const result = applyShellDecisionFloor(check("allow", "session"), "ask");
|
||||
expect(result.state).toBe("ask");
|
||||
expect(result.source).toBe("bash");
|
||||
expect(result.matchedPattern).toBe(SHELL_TOOL_DECISION_FLOOR_PATTERN);
|
||||
});
|
||||
|
||||
it.each(["ask", "deny"] as const)("preserves an existing %s", (state) => {
|
||||
const original = check(state);
|
||||
expect(applyShellDecisionFloor(original, "ask")).toBe(original);
|
||||
});
|
||||
|
||||
it("does nothing when no floor is configured", () => {
|
||||
const original = check("allow");
|
||||
expect(applyShellDecisionFloor(original, undefined)).toBe(original);
|
||||
});
|
||||
});
|
||||
@@ -120,6 +120,36 @@ describe("describeToolGate", () => {
|
||||
expect(desc.payload.request.invokedToolName).toBe("exec_command");
|
||||
});
|
||||
|
||||
it("records a shell alias formatter preview for review and authorization context", () => {
|
||||
const shell: ShellInvocation = { command: "rm -rf dist", workdir: undefined };
|
||||
const formatter = new ToolPreviewFormatter(
|
||||
{
|
||||
toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
||||
toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
||||
},
|
||||
{
|
||||
get: (name) =>
|
||||
name === "ssh_bash"
|
||||
? () => "SSH target 'packaging-server' in remote cwd '/srv/build'"
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
const desc = describeToolGate(
|
||||
makeTcc({ toolName: "ssh_bash", input: { command: "rm -rf dist" } }),
|
||||
makeCheckResult("ask", { toolName: "bash", source: "bash", command: "rm -rf dist" }),
|
||||
formatter,
|
||||
undefined,
|
||||
shell,
|
||||
);
|
||||
|
||||
expect(desc.promptDetails.toolInputPreview).toBe(
|
||||
"SSH target 'packaging-server' in remote cwd '/srv/build'",
|
||||
);
|
||||
expect(desc.logContext.toolInputPreview).toBe(
|
||||
"SSH target 'packaging-server' in remote cwd '/srv/build'",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns mcp surface with target in decision.value for MCP tools", () => {
|
||||
const check = makeCheckResult("ask", {
|
||||
toolName: "mcp",
|
||||
|
||||
@@ -32,6 +32,14 @@ const execShellTools = {
|
||||
exec_command: { commandArgument: "cmd", workdirArgument: "workdir" },
|
||||
};
|
||||
|
||||
const reviewedExecShellTools = {
|
||||
exec_command: {
|
||||
commandArgument: "cmd",
|
||||
workdirArgument: "workdir",
|
||||
decisionFloor: "ask" as const,
|
||||
},
|
||||
};
|
||||
|
||||
describe("shell-tool alias gating (#574)", () => {
|
||||
it("denies an aliased command that a bash: rule denies", async () => {
|
||||
const { handler, events } = makeHandler({
|
||||
@@ -84,6 +92,37 @@ describe("shell-tool alias gating (#574)", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("raises an allowed aliased command to ask before execution", async () => {
|
||||
const prompter = denyingPrompter();
|
||||
const { handler, events } = makeHandler({
|
||||
shellTools: reviewedExecShellTools,
|
||||
tools: ["exec_command"],
|
||||
prompter,
|
||||
session: {
|
||||
checkPermission: makeBashCommandCheck({
|
||||
deny: /rm -rf/,
|
||||
denyMatched: "rm -rf *",
|
||||
allowMatched: "*",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await handler.handleToolCall(
|
||||
makeToolCallEvent("exec_command", { input: { cmd: "git status" } }),
|
||||
makeCtx(),
|
||||
);
|
||||
|
||||
expect(prompter.escalate).toHaveBeenCalledOnce();
|
||||
expect(getDecisionEvents(events)).toContainEqual(
|
||||
expect.objectContaining({
|
||||
surface: "bash",
|
||||
value: "git status",
|
||||
result: "deny",
|
||||
matchedPattern: "<shell-tool-decision-floor>",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("decomposes a chained aliased command so a denied sub-command still blocks", async () => {
|
||||
const { handler, events } = makeHandler({
|
||||
shellTools: execShellTools,
|
||||
|
||||
Reference in New Issue
Block a user