address issue #161

This commit is contained in:
Pedram Amini
2026-01-08 05:31:54 -06:00
parent db3b5f546c
commit f30d57a28d
2 changed files with 84 additions and 2 deletions

View File

@@ -1084,5 +1084,80 @@ describe('process IPC handlers', () => {
// The remote path should be embedded in the SSH command args instead
expect(spawnCall.args.join(' ')).toContain('claude');
});
it('should use agent binaryName for SSH remote instead of local path (fixes Codex/Claude remote path issue)', async () => {
// This test verifies the fix for GitHub issue #161
// The bug: When executing agents on remote hosts, Maestro was using the locally-detected
// full path (e.g., /opt/homebrew/bin/codex on macOS) instead of the agent's binary name.
// This caused "zsh:1: no such file or directory: /opt/homebrew/bin/codex" on remote hosts.
// The fix: Use agent.binaryName (e.g., 'codex') for remote execution, letting the
// remote shell's PATH find the binary at its correct location.
const mockAgent = {
id: 'codex',
name: 'Codex',
binaryName: 'codex', // Just the binary name, without path
path: '/opt/homebrew/bin/codex', // Local macOS path
requiresPty: false,
};
mockAgentDetector.getAgent.mockResolvedValue(mockAgent);
mockSettingsStore.get.mockImplementation((key, defaultValue) => {
if (key === 'sshRemotes') return [mockSshRemote];
return defaultValue;
});
mockProcessManager.spawn.mockReturnValue({ pid: 12345, success: true });
const handler = handlers.get('process:spawn');
await handler!({} as any, {
sessionId: 'session-1',
toolType: 'codex',
cwd: '/home/devuser/project',
command: '/opt/homebrew/bin/codex', // Local path passed from renderer
args: ['exec', '--json'],
sessionSshRemoteConfig: {
enabled: true,
remoteId: 'remote-1',
},
});
// The SSH command args should contain 'codex' (binaryName), NOT '/opt/homebrew/bin/codex'
const spawnCall = mockProcessManager.spawn.mock.calls[0][0];
expect(spawnCall.command).toBe('ssh');
// The remote command in SSH args should use just 'codex', not the full local path
const remoteCommandArg = spawnCall.args[spawnCall.args.length - 1];
expect(remoteCommandArg).toContain("'codex'");
expect(remoteCommandArg).not.toContain('/opt/homebrew/bin/codex');
});
it('should fall back to config.command when agent.binaryName is not available', async () => {
// Edge case: if agent lookup fails or binaryName is undefined, fall back to command
mockAgentDetector.getAgent.mockResolvedValue(null); // Agent not found
mockSettingsStore.get.mockImplementation((key, defaultValue) => {
if (key === 'sshRemotes') return [mockSshRemote];
return defaultValue;
});
mockProcessManager.spawn.mockReturnValue({ pid: 12345, success: true });
const handler = handlers.get('process:spawn');
await handler!({} as any, {
sessionId: 'session-1',
toolType: 'unknown-agent',
cwd: '/home/devuser/project',
command: 'custom-agent', // When agent not found, this should be used
args: ['--help'],
sessionSshRemoteConfig: {
enabled: true,
remoteId: 'remote-1',
},
});
const spawnCall = mockProcessManager.spawn.mock.calls[0][0];
expect(spawnCall.command).toBe('ssh');
// Should fall back to config.command when agent.binaryName is unavailable
const remoteCommandArg = spawnCall.args[spawnCall.args.length - 1];
expect(remoteCommandArg).toContain("'custom-agent'");
});
});
});

View File

@@ -267,8 +267,14 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
// Build the SSH command that wraps the agent execution
// The cwd is the local project path which may not exist on remote
// Remote should use remoteWorkingDir from SSH config if set
//
// IMPORTANT: For remote execution, use the agent's binaryName (e.g., 'codex', 'claude')
// instead of the locally detected full path (e.g., '/opt/homebrew/bin/codex').
// The remote shell's PATH will resolve the binary correctly. This fixes the issue
// where Maestro would try to execute a local macOS path on a remote Linux host.
const remoteCommand = agent?.binaryName || config.command;
const sshCommand = await buildSshCommand(sshResult.config, {
command: config.command,
command: remoteCommand,
args: sshArgs,
// Use the local cwd - the SSH command builder will handle remote path resolution
// If SSH config has remoteWorkingDir, that takes precedence
@@ -286,7 +292,8 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
remoteName: sshResult.config.name,
remoteHost: sshResult.config.host,
source: sshResult.source,
originalCommand: config.command,
localCommand: config.command,
remoteCommand: remoteCommand,
sshCommand: `${sshCommand.command} ${sshCommand.args.join(' ')}`,
});
}