From 1b202c9f147b92192a8c53e39ee441ddb77aebfe Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 17 Jun 2026 08:09:51 +0000 Subject: [PATCH] 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 --- packages/coding-agent/CHANGELOG.md | 4 ++ .../src/ssh/connection-manager.ts | 6 +-- .../test/ssh/connection-manager.test.ts | 50 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f809c4b8c..c756f8fe5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/ssh/connection-manager.ts b/packages/coding-agent/src/ssh/connection-manager.ts index b415f6ba5..c8e7d9d34 100644 --- a/packages/coding-agent/src/ssh/connection-manager.ts +++ b/packages/coding-agent/src/ssh/connection-manager.ts @@ -65,8 +65,8 @@ async function deleteHostInfoFromDisk(hostName: string): Promise { } } -async function validateKeyPermissions(keyPath?: string): Promise { - if (!keyPath) return; +async function validateKeyPermissions(keyPath?: string, platform: SshPlatform = process.platform): Promise { + 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 { - await validateKeyPermissions(host.keyPath); + await validateKeyPermissions(host.keyPath, options?.platform); return [...buildCommonArgs(host, options), buildSshTarget(host.username, host.host), command]; } diff --git a/packages/coding-agent/test/ssh/connection-manager.test.ts b/packages/coding-agent/test/ssh/connection-manager.test.ts index 275cea0f6..730794746 100644 --- a/packages/coding-agent/test/ssh/connection-manager.test.ts +++ b/packages/coding-agent/test/ssh/connection-manager.test.ts @@ -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(run: (keyPath: string) => Promise): Promise { + 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", () => {