fix(coding-agent): skipped windows ssh key mode check
Skipped Unix mode-bit validation for ssh identity files on native Windows while preserving the stricter check for Unix-like platforms. Added regression coverage for Windows ssh argument building with permissive low mode bits and for Unix rejection of group/world-readable keys. Fixes #2850
This commit is contained in:
@@ -10,6 +10,10 @@
|
||||
|
||||
- Fixed image attachment handling for text-only models by saving attachments to `local://` and injecting generated descriptions so they are no longer lost when the target model cannot process images
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the ssh tool rejecting valid Windows identity files before invoking OpenSSH by skipping Unix mode-bit key validation on native Windows ([#2850](https://github.com/can1357/oh-my-pi/issues/2850)).
|
||||
|
||||
## [16.0.4] - 2026-06-17
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -65,8 +65,8 @@ async function deleteHostInfoFromDisk(hostName: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function validateKeyPermissions(keyPath?: string): Promise<void> {
|
||||
if (!keyPath) return;
|
||||
async function validateKeyPermissions(keyPath?: string, platform: SshPlatform = process.platform): Promise<void> {
|
||||
if (!keyPath || platform === "win32") return;
|
||||
let stats: fs.Stats;
|
||||
try {
|
||||
stats = await fs.promises.stat(keyPath);
|
||||
@@ -402,7 +402,7 @@ export async function buildRemoteCommand(
|
||||
command: string,
|
||||
options?: SSHArgsOptions,
|
||||
): Promise<string[]> {
|
||||
await validateKeyPermissions(host.keyPath);
|
||||
await validateKeyPermissions(host.keyPath, options?.platform);
|
||||
return [...buildCommonArgs(host, options), buildSshTarget(host.username, host.host), command];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import * as connectionManager from "@oh-my-pi/pi-coding-agent/ssh/connection-manager";
|
||||
|
||||
async function withLooseKey<T>(run: (keyPath: string) => Promise<T>): Promise<T> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-ssh-key-"));
|
||||
const keyPath = path.join(dir, "id_ed25519");
|
||||
await Bun.write(keyPath, "dummy-key");
|
||||
await fs.chmod(keyPath, 0o666);
|
||||
try {
|
||||
return await run(keyPath);
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("buildRemoteCommand", () => {
|
||||
it("includes -n and OpenSSH ControlMaster options on Unix-like platforms", async () => {
|
||||
const args = await connectionManager.buildRemoteCommand(
|
||||
@@ -36,6 +51,41 @@ describe("buildRemoteCommand", () => {
|
||||
expect(args.at(-2)).toBe("192.168.3.146");
|
||||
expect(args.at(-1)).toBe("ls -la");
|
||||
});
|
||||
|
||||
it("skips Unix mode-bit key validation for Windows args", async () => {
|
||||
await withLooseKey(async keyPath => {
|
||||
const args = await connectionManager.buildRemoteCommand(
|
||||
{
|
||||
name: "host",
|
||||
host: "192.168.3.146",
|
||||
keyPath,
|
||||
},
|
||||
"ls -la",
|
||||
{ platform: "win32" },
|
||||
);
|
||||
|
||||
expect(args).toContain("-i");
|
||||
expect(args).toContain(keyPath);
|
||||
expect(args.at(-2)).toBe("192.168.3.146");
|
||||
expect(args.at(-1)).toBe("ls -la");
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects group/world-readable identity files on Unix-like platforms", async () => {
|
||||
await withLooseKey(async keyPath => {
|
||||
await expect(
|
||||
connectionManager.buildRemoteCommand(
|
||||
{
|
||||
name: "host",
|
||||
host: "192.168.3.146",
|
||||
keyPath,
|
||||
},
|
||||
"ls -la",
|
||||
{ platform: "linux" },
|
||||
),
|
||||
).rejects.toThrow("SSH key permissions must be 600 or stricter");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("supportsSshControlMaster", () => {
|
||||
|
||||
Reference in New Issue
Block a user