fix(coding-agent): avoid SSH ControlMaster on Windows

This commit is contained in:
jiwangyihao
2026-05-11 04:54:05 +08:00
parent 0d5a8831e0
commit 375ef060a7
4 changed files with 76 additions and 25 deletions
+1
View File
@@ -5,6 +5,7 @@
### Fixed
- Fixed subagents launched in the same parallel batch not seeing each other in their initial `# IRC Peers` system-prompt block by pre-registering the agent in the global `AgentRegistry` before `rebuildSystemPrompt` runs and attaching the live session afterwards
- Fixed the SSH tool on native Windows by avoiding OpenSSH ControlMaster multiplexing, which Win32-OpenSSH does not support and reports as `getsockname failed` ([#154](https://github.com/can1357/oh-my-pi/issues/154)).
## [14.9.3] - 2026-05-10
### Breaking Changes
@@ -15,6 +15,11 @@ export interface SSHConnectionTarget {
export type SSHHostOs = "windows" | "linux" | "macos" | "unknown";
export type SSHHostShell = "cmd" | "powershell" | "bash" | "zsh" | "sh" | "unknown";
export type SshPlatform = typeof process.platform;
export function supportsSshControlMaster(platform: SshPlatform = process.platform): boolean {
return platform !== "win32";
}
export interface SSHHostInfo {
version: number;
@@ -33,6 +38,10 @@ const activeHosts = new Map<string, SSHConnectionTarget>();
const pendingConnections = new Map<string, Promise<void>>();
const hostInfoCache = new Map<string, SSHHostInfo>();
interface SSHArgsOptions {
platform?: SshPlatform;
}
function ensureControlDir() {
fs.mkdirSync(CONTROL_DIR, { recursive: true, mode: 0o700 });
try {
@@ -66,20 +75,14 @@ async function validateKeyPermissions(keyPath?: string): Promise<void> {
}
}
function buildCommonArgs(host: SSHConnectionTarget): string[] {
const args = [
"-n",
"-o",
"ControlMaster=auto",
"-o",
`ControlPath=${CONTROL_PATH}`,
"-o",
"ControlPersist=3600",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=accept-new",
];
function buildCommonArgs(host: SSHConnectionTarget, options?: SSHArgsOptions): string[] {
const args = ["-n"];
if (supportsSshControlMaster(options?.platform)) {
args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");
}
args.push("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new");
if (host.port) {
args.push("-p", String(host.port));
@@ -357,9 +360,13 @@ export async function ensureHostInfo(host: SSHConnectionTarget): Promise<SSHHost
return probeHostInfo(host);
}
export async function buildRemoteCommand(host: SSHConnectionTarget, command: string): Promise<string[]> {
export async function buildRemoteCommand(
host: SSHConnectionTarget,
command: string,
options?: SSHArgsOptions,
): Promise<string[]> {
await validateKeyPermissions(host.keyPath);
return [...buildCommonArgs(host), buildSshTarget(host.username, host.host), command];
return [...buildCommonArgs(host, options), buildSshTarget(host.username, host.host), command];
}
let registered = false;
@@ -385,6 +392,14 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
}
const target = buildSshTarget(host.username, host.host);
if (!supportsSshControlMaster()) {
activeHosts.set(key, host);
if (!hostInfoCache.has(key) && !(await loadHostInfoFromDisk(host))) {
await probeHostInfo(host);
}
return;
}
const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
if (check.exitCode === 0) {
activeHosts.set(key, host);
@@ -415,6 +430,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
}
async function closeConnectionInternal(host: SSHConnectionTarget): Promise<void> {
if (!supportsSshControlMaster()) return;
const target = buildSshTarget(host.username, host.host);
await runSshSync(["-O", "exit", ...buildCommonArgs(host), target]);
}
+10 -7
View File
@@ -2,7 +2,12 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { $which, getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
import { $ } from "bun";
import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
import {
getControlDir,
getControlPathTemplate,
type SSHConnectionTarget,
supportsSshControlMaster,
} from "./connection-manager";
import { buildSshTarget, sanitizeHostName } from "./utils";
const REMOTE_DIR = getRemoteDir();
@@ -40,14 +45,12 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=accept-new",
"-o",
"ControlMaster=auto",
"-o",
`ControlPath=${CONTROL_PATH}`,
"-o",
"ControlPersist=3600",
];
if (supportsSshControlMaster()) {
args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");
}
if (host.port) {
args.push("-p", String(host.port));
}
@@ -1,14 +1,15 @@
import { describe, expect, it } from "bun:test";
import { buildRemoteCommand } from "../../src/ssh/connection-manager";
import { buildRemoteCommand, supportsSshControlMaster } from "../../src/ssh/connection-manager";
describe("buildRemoteCommand", () => {
it("includes -n to bind stdin to /dev/null for mux channel opens", async () => {
it("includes -n and OpenSSH ControlMaster options on Unix-like platforms", async () => {
const args = await buildRemoteCommand(
{
name: "host",
host: "192.168.3.146",
},
"ls -la",
{ platform: "linux" },
);
expect(args[0]).toBe("-n");
@@ -16,4 +17,34 @@ describe("buildRemoteCommand", () => {
expect(args.at(-2)).toBe("192.168.3.146");
expect(args.at(-1)).toBe("ls -la");
});
it("omits OpenSSH ControlMaster options on Windows", async () => {
const args = await buildRemoteCommand(
{
name: "host",
host: "192.168.3.146",
},
"ls -la",
{ platform: "win32" },
);
expect(args[0]).toBe("-n");
expect(args).not.toContain("ControlMaster=auto");
expect(args.some(arg => arg.startsWith("ControlPath="))).toBe(false);
expect(args).not.toContain("ControlPersist=3600");
expect(args).toContain("BatchMode=yes");
expect(args.at(-2)).toBe("192.168.3.146");
expect(args.at(-1)).toBe("ls -la");
});
});
describe("supportsSshControlMaster", () => {
it("disables OpenSSH connection multiplexing on native Windows", () => {
expect(supportsSshControlMaster("win32")).toBe(false);
});
it("keeps OpenSSH connection multiplexing on Unix-like platforms", () => {
expect(supportsSshControlMaster("linux")).toBe(true);
expect(supportsSshControlMaster("darwin")).toBe(true);
});
});