fix(group-chat): add SSH remote execution support for moderator and participant spawns

Group Chat was spawning agents locally even when sessions were configured
for SSH remote execution. This fix:

- Creates reusable SSH spawn wrapper utility (ssh-spawn-wrapper.ts)
- Updates SessionInfo, SessionOverrides, and ModeratorConfig interfaces
  to include sshRemoteConfig
- Applies SSH wrapping to addParticipant(), moderator spawn, and
  participant spawn in routeModeratorResponse()
- Wires up SSH store in index.ts for group chat router
- Documents SSH/provider awareness pattern in CLAUDE.md

This ensures Group Chat respects SSH remote configuration just like
regular session spawns.

Closes issue #172
This commit is contained in:
Pedram Amini
2026-01-30 17:42:17 -05:00
parent ace64f94cc
commit b7614b25be
6 changed files with 402 additions and 23 deletions

View File

@@ -167,6 +167,40 @@ src/
| Add colorblind palette | `src/renderer/constants/colorblindPalettes.ts` | | Add colorblind palette | `src/renderer/constants/colorblindPalettes.ts` |
| Add performance metrics | `src/shared/performance-metrics.ts` | | Add performance metrics | `src/shared/performance-metrics.ts` |
| Add power management | `src/main/power-manager.ts`, `src/main/ipc/handlers/system.ts` | | Add power management | `src/main/power-manager.ts`, `src/main/ipc/handlers/system.ts` |
| Spawn agent with SSH support | `src/main/utils/ssh-spawn-wrapper.ts` (required for SSH remote execution) |
---
## Critical Implementation Guidelines
### SSH Remote Execution Awareness
**IMPORTANT:** When implementing any feature that spawns agent processes (e.g., context grooming, group chat, batch operations), you MUST support SSH remote execution.
Sessions can be configured to run on remote hosts via SSH. Without proper SSH wrapping, agents will always execute locally, breaking the user's expected behavior.
**Required pattern:**
1. Check if the session has `sshRemoteConfig` with `enabled: true`
2. Use `wrapSpawnWithSsh()` from `src/main/utils/ssh-spawn-wrapper.ts` to wrap the spawn config
3. Pass the SSH store (available via `createSshRemoteStoreAdapter(settingsStore)`)
```typescript
import { wrapSpawnWithSsh } from '../utils/ssh-spawn-wrapper';
import { createSshRemoteStoreAdapter } from '../utils/ssh-remote-resolver';
// Before spawning, wrap the config with SSH if needed
if (sshStore && session.sshRemoteConfig?.enabled) {
const sshWrapped = await wrapSpawnWithSsh(spawnConfig, session.sshRemoteConfig, sshStore);
// Use sshWrapped.command, sshWrapped.args, sshWrapped.cwd, etc.
}
```
**Also ensure:**
- The correct agent type is used (don't hardcode `claude-code`)
- Custom agent configuration (customPath, customArgs, customEnvVars) is passed through
- Agent's `binaryName` is used for remote execution (not local paths)
See [[CLAUDE-PATTERNS.md]] for detailed SSH patterns.
--- ---

View File

@@ -25,6 +25,8 @@ import {
getContextWindowValue, getContextWindowValue,
} from '../utils/agent-args'; } from '../utils/agent-args';
import { groupChatParticipantPrompt } from '../../prompts'; import { groupChatParticipantPrompt } from '../../prompts';
import { wrapSpawnWithSsh } from '../utils/ssh-spawn-wrapper';
import type { SshRemoteSettingsStore } from '../utils/ssh-remote-resolver';
/** /**
* In-memory store for active participant sessions. * In-memory store for active participant sessions.
@@ -63,6 +65,12 @@ export interface SessionOverrides {
customEnvVars?: Record<string, string>; customEnvVars?: Record<string, string>;
/** SSH remote name for display in participant card */ /** SSH remote name for display in participant card */
sshRemoteName?: string; sshRemoteName?: string;
/** Full SSH remote config for remote execution */
sshRemoteConfig?: {
enabled: boolean;
remoteId: string | null;
workingDirOverride?: string;
};
} }
/** /**
@@ -76,7 +84,8 @@ export interface SessionOverrides {
* @param agentDetector - Optional agent detector for resolving agent paths * @param agentDetector - Optional agent detector for resolving agent paths
* @param agentConfigValues - Optional agent config values (from config store) * @param agentConfigValues - Optional agent config values (from config store)
* @param customEnvVars - Optional custom environment variables for the agent (deprecated, use sessionOverrides) * @param customEnvVars - Optional custom environment variables for the agent (deprecated, use sessionOverrides)
* @param sessionOverrides - Optional session-specific overrides (customModel, customArgs, customEnvVars) * @param sessionOverrides - Optional session-specific overrides (customModel, customArgs, customEnvVars, sshRemoteConfig)
* @param sshStore - Optional SSH settings store for remote execution support
* @returns The created participant * @returns The created participant
*/ */
export async function addParticipant( export async function addParticipant(
@@ -88,7 +97,8 @@ export async function addParticipant(
agentDetector?: AgentDetector, agentDetector?: AgentDetector,
agentConfigValues?: Record<string, any>, agentConfigValues?: Record<string, any>,
customEnvVars?: Record<string, string>, customEnvVars?: Record<string, string>,
sessionOverrides?: SessionOverrides sessionOverrides?: SessionOverrides,
sshStore?: SshRemoteSettingsStore
): Promise<GroupChatParticipant> { ): Promise<GroupChatParticipant> {
console.log(`[GroupChat:Debug] ========== ADD PARTICIPANT ==========`); console.log(`[GroupChat:Debug] ========== ADD PARTICIPANT ==========`);
console.log(`[GroupChat:Debug] Group Chat ID: ${groupChatId}`); console.log(`[GroupChat:Debug] Group Chat ID: ${groupChatId}`);
@@ -163,18 +173,52 @@ export async function addParticipant(
const sessionId = `group-chat-${groupChatId}-participant-${name}-${uuidv4()}`; const sessionId = `group-chat-${groupChatId}-participant-${name}-${uuidv4()}`;
console.log(`[GroupChat:Debug] Generated session ID: ${sessionId}`); console.log(`[GroupChat:Debug] Generated session ID: ${sessionId}`);
// Wrap spawn config with SSH if configured
let spawnCommand = command;
let spawnArgs = configResolution.args;
let spawnCwd = cwd;
let spawnPrompt: string | undefined = prompt;
let spawnEnvVars = configResolution.effectiveCustomEnvVars ?? effectiveEnvVars;
// Apply SSH wrapping if SSH is configured and store is available
if (sshStore && sessionOverrides?.sshRemoteConfig) {
console.log(`[GroupChat:Debug] Applying SSH wrapping for participant...`);
const sshWrapped = await wrapSpawnWithSsh(
{
command,
args: configResolution.args,
cwd,
prompt,
customEnvVars: configResolution.effectiveCustomEnvVars ?? effectiveEnvVars,
promptArgs: agentConfig?.promptArgs,
noPromptSeparator: agentConfig?.noPromptSeparator,
agentBinaryName: agentConfig?.binaryName,
},
sessionOverrides.sshRemoteConfig,
sshStore
);
spawnCommand = sshWrapped.command;
spawnArgs = sshWrapped.args;
spawnCwd = sshWrapped.cwd;
spawnPrompt = sshWrapped.prompt;
spawnEnvVars = sshWrapped.customEnvVars;
if (sshWrapped.sshRemoteUsed) {
console.log(`[GroupChat:Debug] SSH remote used: ${sshWrapped.sshRemoteUsed.name}`);
}
}
// Spawn the participant agent // Spawn the participant agent
console.log(`[GroupChat:Debug] Spawning participant agent...`); console.log(`[GroupChat:Debug] Spawning participant agent...`);
const result = processManager.spawn({ const result = processManager.spawn({
sessionId, sessionId,
toolType: agentId, toolType: agentId,
cwd, cwd: spawnCwd,
command, command: spawnCommand,
args: configResolution.args, args: spawnArgs,
readOnlyMode: false, // Participants can make changes readOnlyMode: false, // Participants can make changes
prompt, prompt: spawnPrompt,
contextWindow: getContextWindowValue(agentConfig, agentConfigValues || {}), contextWindow: getContextWindowValue(agentConfig, agentConfigValues || {}),
customEnvVars: configResolution.effectiveCustomEnvVars ?? effectiveEnvVars, customEnvVars: spawnEnvVars,
promptArgs: agentConfig?.promptArgs, promptArgs: agentConfig?.promptArgs,
noPromptSeparator: agentConfig?.noPromptSeparator, noPromptSeparator: agentConfig?.noPromptSeparator,
}); });

View File

@@ -34,6 +34,8 @@ import {
getContextWindowValue, getContextWindowValue,
} from '../utils/agent-args'; } from '../utils/agent-args';
import { groupChatParticipantRequestPrompt } from '../../prompts'; import { groupChatParticipantRequestPrompt } from '../../prompts';
import { wrapSpawnWithSsh } from '../utils/ssh-spawn-wrapper';
import type { SshRemoteSettingsStore } from '../utils/ssh-remote-resolver';
// Import emitters from IPC handlers (will be populated after handlers are registered) // Import emitters from IPC handlers (will be populated after handlers are registered)
import { groupChatEmitters } from '../ipc/handlers/groupChat'; import { groupChatEmitters } from '../ipc/handlers/groupChat';
@@ -51,6 +53,12 @@ export interface SessionInfo {
customModel?: string; customModel?: string;
/** SSH remote name for display in participant card */ /** SSH remote name for display in participant card */
sshRemoteName?: string; sshRemoteName?: string;
/** Full SSH remote config for remote execution */
sshRemoteConfig?: {
enabled: boolean;
remoteId: string | null;
workingDirOverride?: string;
};
} }
/** /**
@@ -71,6 +79,9 @@ let getSessionsCallback: GetSessionsCallback | null = null;
let getCustomEnvVarsCallback: GetCustomEnvVarsCallback | null = null; let getCustomEnvVarsCallback: GetCustomEnvVarsCallback | null = null;
let getAgentConfigCallback: GetAgentConfigCallback | null = null; let getAgentConfigCallback: GetAgentConfigCallback | null = null;
// Module-level SSH store for remote execution support
let sshStore: SshRemoteSettingsStore | null = null;
/** /**
* Tracks pending participant responses for each group chat. * Tracks pending participant responses for each group chat.
* When all pending participants have responded, we spawn a moderator synthesis round. * When all pending participants have responded, we spawn a moderator synthesis round.
@@ -150,6 +161,14 @@ export function setGetAgentConfigCallback(callback: GetAgentConfigCallback): voi
getAgentConfigCallback = callback; getAgentConfigCallback = callback;
} }
/**
* Sets the SSH store for remote execution support.
* Called from index.ts during initialization.
*/
export function setSshStore(store: SshRemoteSettingsStore): void {
sshStore = store;
}
/** /**
* Extracts @mentions from text that match known participants. * Extracts @mentions from text that match known participants.
* Supports hyphenated names matching participants with spaces. * Supports hyphenated names matching participants with spaces.
@@ -447,18 +466,54 @@ ${message}`;
// Add power block reason to prevent sleep during group chat activity // Add power block reason to prevent sleep during group chat activity
powerManager.addBlockReason(`groupchat:${groupChatId}`); powerManager.addBlockReason(`groupchat:${groupChatId}`);
// Prepare spawn config with potential SSH wrapping
let spawnCommand = command;
let spawnArgs = finalArgs;
let spawnCwd = process.env.HOME || '/tmp';
let spawnPrompt: string | undefined = fullPrompt;
let spawnEnvVars =
configResolution.effectiveCustomEnvVars ??
getCustomEnvVarsCallback?.(chat.moderatorAgentId);
// Apply SSH wrapping if configured
if (sshStore && chat.moderatorConfig?.sshRemoteConfig) {
console.log(`[GroupChat:Debug] Applying SSH wrapping for moderator...`);
const sshWrapped = await wrapSpawnWithSsh(
{
command,
args: finalArgs,
cwd: process.env.HOME || '/tmp',
prompt: fullPrompt,
customEnvVars:
configResolution.effectiveCustomEnvVars ??
getCustomEnvVarsCallback?.(chat.moderatorAgentId),
promptArgs: agent.promptArgs,
noPromptSeparator: agent.noPromptSeparator,
agentBinaryName: agent.binaryName,
},
chat.moderatorConfig.sshRemoteConfig,
sshStore
);
spawnCommand = sshWrapped.command;
spawnArgs = sshWrapped.args;
spawnCwd = sshWrapped.cwd;
spawnPrompt = sshWrapped.prompt;
spawnEnvVars = sshWrapped.customEnvVars;
if (sshWrapped.sshRemoteUsed) {
console.log(`[GroupChat:Debug] SSH remote used: ${sshWrapped.sshRemoteUsed.name}`);
}
}
const spawnResult = processManager.spawn({ const spawnResult = processManager.spawn({
sessionId, sessionId,
toolType: chat.moderatorAgentId, toolType: chat.moderatorAgentId,
cwd: process.env.HOME || '/tmp', cwd: spawnCwd,
command, command: spawnCommand,
args: finalArgs, args: spawnArgs,
readOnlyMode: true, readOnlyMode: true,
prompt: fullPrompt, prompt: spawnPrompt,
contextWindow: getContextWindowValue(agent, agentConfigValues), contextWindow: getContextWindowValue(agent, agentConfigValues),
customEnvVars: customEnvVars: spawnEnvVars,
configResolution.effectiveCustomEnvVars ??
getCustomEnvVarsCallback?.(chat.moderatorAgentId),
promptArgs: agent.promptArgs, promptArgs: agent.promptArgs,
noPromptSeparator: agent.noPromptSeparator, noPromptSeparator: agent.noPromptSeparator,
}); });
@@ -610,13 +665,16 @@ export async function routeModeratorResponse(
agentDetector, agentDetector,
agentConfigValues, agentConfigValues,
customEnvVars, customEnvVars,
// Pass session-specific overrides (customModel, customArgs, customEnvVars, sshRemoteName from session) // Pass session-specific overrides (customModel, customArgs, customEnvVars, sshRemoteConfig from session)
{ {
customModel: matchingSession.customModel, customModel: matchingSession.customModel,
customArgs: matchingSession.customArgs, customArgs: matchingSession.customArgs,
customEnvVars: matchingSession.customEnvVars, customEnvVars: matchingSession.customEnvVars,
sshRemoteName: matchingSession.sshRemoteName, sshRemoteName: matchingSession.sshRemoteName,
} sshRemoteConfig: matchingSession.sshRemoteConfig,
},
// Pass SSH store for remote execution support
sshStore ?? undefined
); );
existingParticipantNames.add(participantName); existingParticipantNames.add(participantName);
@@ -771,18 +829,54 @@ export async function routeModeratorResponse(
`[GroupChat:Debug] CustomEnvVars: ${JSON.stringify(configResolution.effectiveCustomEnvVars || {})}` `[GroupChat:Debug] CustomEnvVars: ${JSON.stringify(configResolution.effectiveCustomEnvVars || {})}`
); );
// Prepare spawn config with potential SSH wrapping
let finalSpawnCommand = spawnCommand;
let finalSpawnArgs = spawnArgs;
let finalSpawnCwd = cwd;
let finalSpawnPrompt: string | undefined = participantPrompt;
let finalSpawnEnvVars =
configResolution.effectiveCustomEnvVars ??
getCustomEnvVarsCallback?.(participant.agentId);
// Apply SSH wrapping if configured for this session
if (sshStore && matchingSession?.sshRemoteConfig) {
console.log(`[GroupChat:Debug] Applying SSH wrapping for participant ${participantName}...`);
const sshWrapped = await wrapSpawnWithSsh(
{
command: spawnCommand,
args: spawnArgs,
cwd,
prompt: participantPrompt,
customEnvVars:
configResolution.effectiveCustomEnvVars ??
getCustomEnvVarsCallback?.(participant.agentId),
promptArgs: agent.promptArgs,
noPromptSeparator: agent.noPromptSeparator,
agentBinaryName: agent.binaryName,
},
matchingSession.sshRemoteConfig,
sshStore
);
finalSpawnCommand = sshWrapped.command;
finalSpawnArgs = sshWrapped.args;
finalSpawnCwd = sshWrapped.cwd;
finalSpawnPrompt = sshWrapped.prompt;
finalSpawnEnvVars = sshWrapped.customEnvVars;
if (sshWrapped.sshRemoteUsed) {
console.log(`[GroupChat:Debug] SSH remote used: ${sshWrapped.sshRemoteUsed.name}`);
}
}
const spawnResult = processManager.spawn({ const spawnResult = processManager.spawn({
sessionId, sessionId,
toolType: participant.agentId, toolType: participant.agentId,
cwd, cwd: finalSpawnCwd,
command: spawnCommand, command: finalSpawnCommand,
args: spawnArgs, args: finalSpawnArgs,
readOnlyMode: readOnly ?? false, // Propagate read-only mode from caller readOnlyMode: readOnly ?? false, // Propagate read-only mode from caller
prompt: participantPrompt, prompt: finalSpawnPrompt,
contextWindow: getContextWindowValue(agent, agentConfigValues), contextWindow: getContextWindowValue(agent, agentConfigValues),
customEnvVars: customEnvVars: finalSpawnEnvVars,
configResolution.effectiveCustomEnvVars ??
getCustomEnvVarsCallback?.(participant.agentId),
promptArgs: agent.promptArgs, promptArgs: agent.promptArgs,
noPromptSeparator: agent.noPromptSeparator, noPromptSeparator: agent.noPromptSeparator,
}); });

View File

@@ -60,11 +60,13 @@ import {
setGetSessionsCallback, setGetSessionsCallback,
setGetCustomEnvVarsCallback, setGetCustomEnvVarsCallback,
setGetAgentConfigCallback, setGetAgentConfigCallback,
setSshStore,
markParticipantResponded, markParticipantResponded,
spawnModeratorSynthesis, spawnModeratorSynthesis,
getGroupChatReadOnlyState, getGroupChatReadOnlyState,
respawnParticipantWithRecovery, respawnParticipantWithRecovery,
} from './group-chat/group-chat-router'; } from './group-chat/group-chat-router';
import { createSshRemoteStoreAdapter } from './utils/ssh-remote-resolver';
import { updateParticipant, loadGroupChat, updateGroupChat } from './group-chat/group-chat-storage'; import { updateParticipant, loadGroupChat, updateGroupChat } from './group-chat/group-chat-storage';
import { needsSessionRecovery, initiateSessionRecovery } from './group-chat/session-recovery'; import { needsSessionRecovery, initiateSessionRecovery } from './group-chat/session-recovery';
import { initializeSessionStorages } from './storage'; import { initializeSessionStorages } from './storage';
@@ -553,6 +555,8 @@ function setupIpcHandlers() {
customEnvVars: s.customEnvVars, customEnvVars: s.customEnvVars,
customModel: s.customModel, customModel: s.customModel,
sshRemoteName, sshRemoteName,
// Pass full SSH config for remote execution support
sshRemoteConfig: s.sessionSshRemoteConfig,
}; };
}); });
}); });
@@ -561,6 +565,9 @@ function setupIpcHandlers() {
setGetCustomEnvVarsCallback(getCustomEnvVarsForAgent); setGetCustomEnvVarsCallback(getCustomEnvVarsForAgent);
setGetAgentConfigCallback(getAgentConfigForAgent); setGetAgentConfigCallback(getAgentConfigForAgent);
// Set up SSH store for group chat SSH remote execution support
setSshStore(createSshRemoteStoreAdapter(store));
// Setup logger event forwarding to renderer // Setup logger event forwarding to renderer
setupLoggerEventForwarding(() => mainWindow); setupLoggerEventForwarding(() => mainWindow);

View File

@@ -0,0 +1,194 @@
/**
* SSH Spawn Wrapper Utility
*
* Provides a reusable function to wrap spawn configurations with SSH remote execution.
* This extracts the SSH wrapping logic from the process:spawn IPC handler so it can be
* used by other components like Group Chat that spawn processes directly.
*
* IMPORTANT: Any feature that spawns agent processes must use this utility to properly
* support SSH remote execution. Without it, agents will always run locally even when
* the session is configured for SSH remote execution.
*/
import * as os from 'os';
import type { SshRemoteConfig, AgentSshRemoteConfig } from '../../shared/types';
import { getSshRemoteConfig, SshRemoteSettingsStore } from './ssh-remote-resolver';
import { buildSshCommand } from './ssh-command-builder';
import { logger } from './logger';
const LOG_CONTEXT = '[SshSpawnWrapper]';
/**
* Configuration for wrapping a spawn with SSH.
*/
export interface SshSpawnWrapConfig {
/** The command to execute */
command: string;
/** Arguments for the command */
args: string[];
/** Working directory */
cwd: string;
/** The prompt to send (if any) */
prompt?: string;
/** Custom environment variables */
customEnvVars?: Record<string, string>;
/** Agent's promptArgs function (for building prompt flags) */
promptArgs?: (prompt: string) => string[];
/** Whether agent uses -- separator before prompt */
noPromptSeparator?: boolean;
/** Agent's binary name (used for SSH remote command) */
agentBinaryName?: string;
}
/**
* Result of wrapping a spawn config with SSH.
*/
export interface SshSpawnWrapResult {
/** The command to spawn (may be 'ssh' if SSH is enabled) */
command: string;
/** Arguments for the command */
args: string[];
/** Working directory (local home for SSH, original for local) */
cwd: string;
/** Custom environment variables (undefined for SSH since they're in the command) */
customEnvVars?: Record<string, string>;
/** The prompt to pass to ProcessManager (undefined for small SSH prompts, original for large) */
prompt?: string;
/** Whether SSH remote was used */
sshRemoteUsed: SshRemoteConfig | null;
}
/**
* Wrap a spawn configuration with SSH remote execution if configured.
*
* This function handles:
* 1. Resolving the SSH remote configuration from session config
* 2. Building the SSH command wrapper for remote execution
* 3. Handling prompt embedding (small prompts in command line, large via stdin)
* 4. Adjusting cwd and env vars for SSH execution
*
* @param config The original spawn configuration
* @param sshConfig Session-level SSH configuration (if any)
* @param sshStore Store adapter for resolving SSH remote settings
* @returns Wrapped spawn configuration ready for ProcessManager
*
* @example
* const wrapped = await wrapSpawnWithSsh(
* { command: 'claude', args: ['--print'], cwd: '/project', prompt: 'Hello' },
* { enabled: true, remoteId: 'my-server' },
* createSshRemoteStoreAdapter(settingsStore)
* );
* processManager.spawn(wrapped);
*/
export async function wrapSpawnWithSsh(
config: SshSpawnWrapConfig,
sshConfig: AgentSshRemoteConfig | undefined,
sshStore: SshRemoteSettingsStore
): Promise<SshSpawnWrapResult> {
// Check if SSH is enabled for this session
if (!sshConfig?.enabled) {
// Local execution - return config unchanged
return {
command: config.command,
args: config.args,
cwd: config.cwd,
customEnvVars: config.customEnvVars,
prompt: config.prompt,
sshRemoteUsed: null,
};
}
// Resolve the SSH remote configuration
const sshResult = getSshRemoteConfig(sshStore, {
sessionSshConfig: sshConfig,
});
if (!sshResult.config) {
// SSH config not found or disabled - fall back to local execution
logger.warn('SSH remote config not found, falling back to local execution', LOG_CONTEXT, {
remoteId: sshConfig.remoteId,
source: sshResult.source,
});
return {
command: config.command,
args: config.args,
cwd: config.cwd,
customEnvVars: config.customEnvVars,
prompt: config.prompt,
sshRemoteUsed: null,
};
}
logger.info('Wrapping spawn with SSH remote execution', LOG_CONTEXT, {
remoteId: sshResult.config.id,
remoteName: sshResult.config.name,
host: sshResult.config.host,
});
// For SSH execution, we need to include the prompt in the args
// because ProcessManager.spawn() won't add it (we pass prompt: undefined for SSH)
// Use promptArgs if available (e.g., OpenCode -p), otherwise use positional arg
//
// For large prompts (>4000 chars), don't embed in command line to avoid
// command line length limits. Instead, use --input-format stream-json and
// let ProcessManager send via stdin.
const isLargePrompt = config.prompt && config.prompt.length > 4000;
let sshArgs = [...config.args];
let passPromptToSpawn: string | undefined;
if (config.prompt && !isLargePrompt) {
// Small prompt - embed in command line
if (config.promptArgs) {
sshArgs = [...config.args, ...config.promptArgs(config.prompt)];
} else if (config.noPromptSeparator) {
sshArgs = [...config.args, config.prompt];
} else {
sshArgs = [...config.args, '--', config.prompt];
}
} else if (config.prompt && isLargePrompt) {
// Large prompt - use stdin mode
sshArgs = [...config.args, '--input-format', 'stream-json'];
passPromptToSpawn = config.prompt; // Pass to ProcessManager for stdin delivery
logger.info('Using stdin for large prompt in SSH remote execution', LOG_CONTEXT, {
promptLength: config.prompt.length,
reason: 'avoid-command-line-length-limit',
});
}
// Determine the command to run on the remote host:
// Use agentBinaryName if provided (e.g., 'codex', 'claude'), otherwise use the command
// This avoids using local paths like '/opt/homebrew/bin/codex' on the remote
const remoteCommand = config.agentBinaryName || config.command;
// Check if we'll send input via stdin
const useStdin = sshArgs.includes('--input-format') && sshArgs.includes('stream-json');
// Build the SSH command
const sshCommand = await buildSshCommand(sshResult.config, {
command: remoteCommand,
args: sshArgs,
cwd: config.cwd,
env: config.customEnvVars,
useStdin,
});
logger.debug('SSH command built', LOG_CONTEXT, {
sshBinary: sshCommand.command,
sshArgsCount: sshCommand.args.length,
remoteCommand,
remoteArgs: sshArgs,
remoteCwd: config.cwd,
});
return {
command: sshCommand.command,
args: sshCommand.args,
// Use local home directory as cwd - remote cwd is in the SSH command
cwd: os.homedir(),
// Env vars are passed via the SSH command, not locally
customEnvVars: undefined,
// For large prompts, pass to ProcessManager for stdin delivery
prompt: passPromptToSpawn,
sshRemoteUsed: sshResult.config,
};
}

View File

@@ -73,6 +73,12 @@ export interface ModeratorConfig {
customArgs?: string; customArgs?: string;
/** Custom environment variables */ /** Custom environment variables */
customEnvVars?: Record<string, string>; customEnvVars?: Record<string, string>;
/** SSH remote config for remote execution */
sshRemoteConfig?: {
enabled: boolean;
remoteId: string | null;
workingDirOverride?: string;
};
} }
/** /**