mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
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:
34
CLAUDE.md
34
CLAUDE.md
@@ -167,6 +167,40 @@ src/
|
||||
| Add colorblind palette | `src/renderer/constants/colorblindPalettes.ts` |
|
||||
| Add performance metrics | `src/shared/performance-metrics.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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
getContextWindowValue,
|
||||
} from '../utils/agent-args';
|
||||
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.
|
||||
@@ -63,6 +65,12 @@ export interface SessionOverrides {
|
||||
customEnvVars?: Record<string, string>;
|
||||
/** SSH remote name for display in participant card */
|
||||
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 agentConfigValues - Optional agent config values (from config store)
|
||||
* @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
|
||||
*/
|
||||
export async function addParticipant(
|
||||
@@ -88,7 +97,8 @@ export async function addParticipant(
|
||||
agentDetector?: AgentDetector,
|
||||
agentConfigValues?: Record<string, any>,
|
||||
customEnvVars?: Record<string, string>,
|
||||
sessionOverrides?: SessionOverrides
|
||||
sessionOverrides?: SessionOverrides,
|
||||
sshStore?: SshRemoteSettingsStore
|
||||
): Promise<GroupChatParticipant> {
|
||||
console.log(`[GroupChat:Debug] ========== ADD PARTICIPANT ==========`);
|
||||
console.log(`[GroupChat:Debug] Group Chat ID: ${groupChatId}`);
|
||||
@@ -163,18 +173,52 @@ export async function addParticipant(
|
||||
const sessionId = `group-chat-${groupChatId}-participant-${name}-${uuidv4()}`;
|
||||
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
|
||||
console.log(`[GroupChat:Debug] Spawning participant agent...`);
|
||||
const result = processManager.spawn({
|
||||
sessionId,
|
||||
toolType: agentId,
|
||||
cwd,
|
||||
command,
|
||||
args: configResolution.args,
|
||||
cwd: spawnCwd,
|
||||
command: spawnCommand,
|
||||
args: spawnArgs,
|
||||
readOnlyMode: false, // Participants can make changes
|
||||
prompt,
|
||||
prompt: spawnPrompt,
|
||||
contextWindow: getContextWindowValue(agentConfig, agentConfigValues || {}),
|
||||
customEnvVars: configResolution.effectiveCustomEnvVars ?? effectiveEnvVars,
|
||||
customEnvVars: spawnEnvVars,
|
||||
promptArgs: agentConfig?.promptArgs,
|
||||
noPromptSeparator: agentConfig?.noPromptSeparator,
|
||||
});
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
getContextWindowValue,
|
||||
} from '../utils/agent-args';
|
||||
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 { groupChatEmitters } from '../ipc/handlers/groupChat';
|
||||
@@ -51,6 +53,12 @@ export interface SessionInfo {
|
||||
customModel?: string;
|
||||
/** SSH remote name for display in participant card */
|
||||
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 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.
|
||||
* When all pending participants have responded, we spawn a moderator synthesis round.
|
||||
@@ -150,6 +161,14 @@ export function setGetAgentConfigCallback(callback: GetAgentConfigCallback): voi
|
||||
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.
|
||||
* Supports hyphenated names matching participants with spaces.
|
||||
@@ -447,18 +466,54 @@ ${message}`;
|
||||
// Add power block reason to prevent sleep during group chat activity
|
||||
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({
|
||||
sessionId,
|
||||
toolType: chat.moderatorAgentId,
|
||||
cwd: process.env.HOME || '/tmp',
|
||||
command,
|
||||
args: finalArgs,
|
||||
cwd: spawnCwd,
|
||||
command: spawnCommand,
|
||||
args: spawnArgs,
|
||||
readOnlyMode: true,
|
||||
prompt: fullPrompt,
|
||||
prompt: spawnPrompt,
|
||||
contextWindow: getContextWindowValue(agent, agentConfigValues),
|
||||
customEnvVars:
|
||||
configResolution.effectiveCustomEnvVars ??
|
||||
getCustomEnvVarsCallback?.(chat.moderatorAgentId),
|
||||
customEnvVars: spawnEnvVars,
|
||||
promptArgs: agent.promptArgs,
|
||||
noPromptSeparator: agent.noPromptSeparator,
|
||||
});
|
||||
@@ -610,13 +665,16 @@ export async function routeModeratorResponse(
|
||||
agentDetector,
|
||||
agentConfigValues,
|
||||
customEnvVars,
|
||||
// Pass session-specific overrides (customModel, customArgs, customEnvVars, sshRemoteName from session)
|
||||
// Pass session-specific overrides (customModel, customArgs, customEnvVars, sshRemoteConfig from session)
|
||||
{
|
||||
customModel: matchingSession.customModel,
|
||||
customArgs: matchingSession.customArgs,
|
||||
customEnvVars: matchingSession.customEnvVars,
|
||||
sshRemoteName: matchingSession.sshRemoteName,
|
||||
}
|
||||
sshRemoteConfig: matchingSession.sshRemoteConfig,
|
||||
},
|
||||
// Pass SSH store for remote execution support
|
||||
sshStore ?? undefined
|
||||
);
|
||||
existingParticipantNames.add(participantName);
|
||||
|
||||
@@ -771,18 +829,54 @@ export async function routeModeratorResponse(
|
||||
`[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({
|
||||
sessionId,
|
||||
toolType: participant.agentId,
|
||||
cwd,
|
||||
command: spawnCommand,
|
||||
args: spawnArgs,
|
||||
cwd: finalSpawnCwd,
|
||||
command: finalSpawnCommand,
|
||||
args: finalSpawnArgs,
|
||||
readOnlyMode: readOnly ?? false, // Propagate read-only mode from caller
|
||||
prompt: participantPrompt,
|
||||
prompt: finalSpawnPrompt,
|
||||
contextWindow: getContextWindowValue(agent, agentConfigValues),
|
||||
customEnvVars:
|
||||
configResolution.effectiveCustomEnvVars ??
|
||||
getCustomEnvVarsCallback?.(participant.agentId),
|
||||
customEnvVars: finalSpawnEnvVars,
|
||||
promptArgs: agent.promptArgs,
|
||||
noPromptSeparator: agent.noPromptSeparator,
|
||||
});
|
||||
|
||||
@@ -60,11 +60,13 @@ import {
|
||||
setGetSessionsCallback,
|
||||
setGetCustomEnvVarsCallback,
|
||||
setGetAgentConfigCallback,
|
||||
setSshStore,
|
||||
markParticipantResponded,
|
||||
spawnModeratorSynthesis,
|
||||
getGroupChatReadOnlyState,
|
||||
respawnParticipantWithRecovery,
|
||||
} from './group-chat/group-chat-router';
|
||||
import { createSshRemoteStoreAdapter } from './utils/ssh-remote-resolver';
|
||||
import { updateParticipant, loadGroupChat, updateGroupChat } from './group-chat/group-chat-storage';
|
||||
import { needsSessionRecovery, initiateSessionRecovery } from './group-chat/session-recovery';
|
||||
import { initializeSessionStorages } from './storage';
|
||||
@@ -553,6 +555,8 @@ function setupIpcHandlers() {
|
||||
customEnvVars: s.customEnvVars,
|
||||
customModel: s.customModel,
|
||||
sshRemoteName,
|
||||
// Pass full SSH config for remote execution support
|
||||
sshRemoteConfig: s.sessionSshRemoteConfig,
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -561,6 +565,9 @@ function setupIpcHandlers() {
|
||||
setGetCustomEnvVarsCallback(getCustomEnvVarsForAgent);
|
||||
setGetAgentConfigCallback(getAgentConfigForAgent);
|
||||
|
||||
// Set up SSH store for group chat SSH remote execution support
|
||||
setSshStore(createSshRemoteStoreAdapter(store));
|
||||
|
||||
// Setup logger event forwarding to renderer
|
||||
setupLoggerEventForwarding(() => mainWindow);
|
||||
|
||||
|
||||
194
src/main/utils/ssh-spawn-wrapper.ts
Normal file
194
src/main/utils/ssh-spawn-wrapper.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -73,6 +73,12 @@ export interface ModeratorConfig {
|
||||
customArgs?: string;
|
||||
/** Custom environment variables */
|
||||
customEnvVars?: Record<string, string>;
|
||||
/** SSH remote config for remote execution */
|
||||
sshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
remoteId: string | null;
|
||||
workingDirOverride?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user