Files
Maestro/specs/ssh-remote-agents-tasks.md
Pedram Amini 3b74191af7 MAESTRO: Integrate SSH remote execution in process spawn (Phase 4 Tasks T027-T029)
Add SSH remote detection and command wrapping to the process:spawn IPC handler.
When an SSH remote is configured (global default or agent-specific override),
agent commands are wrapped with SSH for remote execution.

Changes:
- Import SSH utilities (getSshRemoteConfig, createSshRemoteStoreAdapter, buildSshCommand)
- Update MaestroSettings interface with sshRemotes and defaultSshRemoteId fields
- Add SSH remote resolution after agent args are built
- Wrap command with buildSshCommand when SSH remote is configured
- Disable PTY when using SSH (SSH handles terminal emulation)
- Pass custom env vars via remote command string, not locally
- Terminal sessions always run locally (need PTY for shell interaction)

Tests:
- 8 new unit tests for SSH remote execution scenarios
- All existing tests pass (12,232 tests)
2025-12-29 17:22:05 -06:00

15 KiB

SSH Remote Agent Execution - Tasks

Feature: SSH Remote Agent Execution

Spec: specs/ssh-remote-agents.md

Plan: specs/ssh-remote-agents-plan.md

Branch: 1-ssh-tunnel-agents

Date: 2025-12-27


User Stories (from spec)

ID Story Priority
US1 Configure SSH remote globally P1 (Must Have)
US2 Agent commands execute on remote via SSH P1 (Must Have)
US3 Per-agent remote override P2 (Should Have)
US4 Connection test functionality P2 (Should Have)
US5 Connection status visibility P2 (Should Have)

Phase 1: Setup

Goal: Project infrastructure and shared types.

  • T001 Add SshRemoteConfig interface to src/shared/types.ts
  • T002 Add SshRemoteStatus interface to src/shared/types.ts
  • T003 Add SshRemoteTestResult interface to src/shared/types.ts
  • T004 Add AgentSshRemoteConfig interface to src/shared/types.ts
  • T005 [P] Create shellEscape utility function in src/main/utils/shell-escape.ts
  • T006 Add sshRemotes and defaultSshRemoteId defaults to store in src/main/index.ts

Phase 1 Notes (2025-12-27):

  • All SSH-related interfaces added to src/shared/types.ts with full JSDoc documentation
  • Created src/main/utils/shell-escape.ts with shellEscape, shellEscapeArgs, and buildShellCommand functions
  • Added sshRemotes (empty array) and defaultSshRemoteId (null) to MaestroSettings with store defaults
  • Added 14 unit tests for shell-escape utility in src/__tests__/main/utils/shell-escape.test.ts

Phase 2: Foundational - SSH Remote Manager

Goal: Core SSH remote manager with validation and connection testing.

Prerequisite: Phase 1 complete.

  • T007 Create SshRemoteManager class skeleton in src/main/ssh-remote-manager.ts
  • T008 Implement validateConfig method in src/main/ssh-remote-manager.ts
  • T009 Implement testConnection method in src/main/ssh-remote-manager.ts
  • T010 Implement buildSshArgs helper method in src/main/ssh-remote-manager.ts
  • T011 Export SshRemoteManager singleton instance in src/main/ssh-remote-manager.ts

Phase 2 Notes (2025-12-27):

  • Created src/main/ssh-remote-manager.ts with full SshRemoteManager class
  • Implemented validateConfig() for required fields, port range (1-65535), and private key readability
  • Implemented testConnection() with SSH connection test, hostname retrieval, and optional agent detection
  • Implemented buildSshArgs() for constructing SSH command-line arguments with security options (BatchMode, StrictHostKeyChecking, ConnectTimeout)
  • Added dependency injection pattern (SshRemoteManagerDeps) for testability
  • Comprehensive SSH error parsing for user-friendly messages (permission denied, connection refused, timeout, hostname resolution, host key changed, passphrase issues)
  • Added 38 unit tests in src/__tests__/main/ssh-remote-manager.test.ts
  • Exported sshRemoteManager singleton instance for use throughout the application

Phase 3: User Story 1 - Configure SSH Remote Globally

Goal: User can add, edit, delete SSH remote configurations and set a global default.

Test Criteria: Can call window.maestro.sshRemote.saveConfig(), getConfigs(), setDefaultId() from devtools.

  • T012 [US1] Create IPC handlers file src/main/ipc/handlers/ssh-remote.ts
  • T013 [US1] Implement ssh-remote:saveConfig handler in src/main/ipc/handlers/ssh-remote.ts
  • T014 [US1] Implement ssh-remote:deleteConfig handler in src/main/ipc/handlers/ssh-remote.ts
  • T015 [US1] Implement ssh-remote:getConfigs handler in src/main/ipc/handlers/ssh-remote.ts
  • T016 [US1] Implement ssh-remote:getDefaultId handler in src/main/ipc/handlers/ssh-remote.ts
  • T017 [US1] Implement ssh-remote:setDefaultId handler in src/main/ipc/handlers/ssh-remote.ts
  • T018 [US1] Register SSH remote handlers in src/main/ipc/handlers/index.ts
  • T019 [US1] Expose sshRemote API in src/main/preload.ts

Phase 3 Backend Notes (2025-12-27):

  • Created src/main/ipc/handlers/ssh-remote.ts with 6 IPC handlers:

    • ssh-remote:saveConfig - Create or update SSH remote configuration with validation
    • ssh-remote:deleteConfig - Delete SSH remote by ID, auto-clears default if deleted
    • ssh-remote:getConfigs - Get all stored SSH remote configurations
    • ssh-remote:getDefaultId - Get the global default SSH remote ID
    • ssh-remote:setDefaultId - Set/clear the global default SSH remote ID
    • ssh-remote:test - Test SSH connection (accepts config ID or full config object)
  • Registered handlers in src/main/ipc/handlers/index.ts

  • Exposed window.maestro.sshRemote API in src/main/preload.ts with full TypeScript types

  • Updated MaestroSettings interface to include sshRemotes and defaultSshRemoteId fields

  • Added 19 unit tests in src/__tests__/main/ipc/handlers/ssh-remote.test.ts

  • All IPC handlers use createIpcHandler pattern for consistent error handling and logging

  • T020 [US1] Create useSshRemotes hook in src/renderer/hooks/useSshRemotes.ts

  • T021 [US1] Create SshRemoteModal component in src/renderer/components/Settings/SshRemoteModal.tsx

  • T022 [US1] Create SshRemotesSection component in src/renderer/components/Settings/SshRemotesSection.tsx

  • T023 [US1] Integrate SshRemotesSection into src/renderer/components/Settings/SettingsModal.tsx

Phase 3 Frontend Notes (2025-12-27):

  • Created src/renderer/components/Settings/SshRemoteModal.tsx with form for adding/editing SSH remotes
    • Display name, host, port, username, private key path
    • Optional remote working directory and environment variables
    • Enabled/disabled toggle
    • Connection testing with result display (success/error with hostname)
    • Uses Modal and FormInput components for consistent UI
  • Created src/renderer/components/Settings/SshRemotesSection.tsx for the Settings modal
    • Lists all configured SSH remotes with status badges (default, disabled)
    • Inline actions: test connection, set as default, edit, delete
    • Empty state with helpful instructions
    • Add SSH Remote button
    • Integrates with useSshRemotes hook for all state management
  • Added SSH_REMOTE modal priority (460) in modalPriorities.ts
  • Integrated SshRemotesSection into SettingsModal.tsx general tab
  • Created src/renderer/components/Settings/index.ts for exports

Phase 4: User Story 2 - Agent Commands Execute on Remote

Goal: When SSH remote is configured, agent spawns execute via SSH on the remote host.

Test Criteria: Configure remote via devtools, spawn agent, verify SSH command wraps agent invocation.

Prerequisite: US1 complete (need config storage).

  • T024 [P] [US2] Create buildSshCommand function in src/main/utils/ssh-command-builder.ts
  • T025 [P] [US2] Create buildRemoteCommand function in src/main/utils/ssh-command-builder.ts

Phase 4 (T024-T025) Notes (2025-12-27):

  • Created src/main/utils/ssh-command-builder.ts with:
    • buildRemoteCommand(): Constructs escaped shell command string with optional cwd and env vars
    • buildSshCommand(): Builds full SSH command with args for spawn(), merging config and command-specific options
  • Security considerations:
    • Command name is NOT escaped (trusted, from agent config) to preserve PATH resolution
    • All user-controllable values (args, cwd, env values) ARE escaped via shellEscape
    • Environment variable names are validated (alphanumeric + underscore only)
  • Added 29 unit tests in src/__tests__/main/utils/ssh-command-builder.test.ts
    • Tests cover: basic command building, cwd handling, env merging, tilde expansion
    • Security tests: injection prevention via args, cwd, env values, invalid env names
  • T026 [US2] Add getSshRemoteConfig helper to resolve effective remote in src/main/process-manager.ts

Phase 4 (T026) Notes (2025-12-27):

  • Created src/main/utils/ssh-remote-resolver.ts with:
    • getSshRemoteConfig(): Resolves effective SSH remote config with priority:
      1. Agent-specific disabled -> force local execution
      2. Agent-specific remoteId -> use that specific remote
      3. Global defaultSshRemoteId -> use that remote
      4. No SSH remote configured -> local execution
    • createSshRemoteStoreAdapter(): Factory to wrap electron-store with SshRemoteSettingsStore interface
    • SshRemoteSettingsStore interface: Abstracts store access for testability
    • SshRemoteResolveResult type: Returns both config and resolution source ('agent', 'global', 'disabled', 'none')
  • Design decisions:
    • Created separate utility file (not in process-manager.ts) for better testability and separation of concerns
    • Uses dependency injection pattern via store interface for easy unit testing
    • Validates that remotes are enabled before returning them
    • Falls back through the priority chain gracefully when remotes are missing or disabled
  • Added 18 unit tests in src/__tests__/main/utils/ssh-remote-resolver.test.ts
    • Tests cover: no remotes configured, global default, agent override, priority ordering, disabled remotes, store adapter
  • T027 [US2] Modify spawn() to detect SSH remote config in src/main/process-manager.ts
  • T028 [US2] Wrap agent command with buildSshCommand when SSH enabled in src/main/process-manager.ts
  • T029 [US2] Pass agent config env vars to remote command in src/main/process-manager.ts

Phase 4 (T027-T029) Notes (2025-12-27):

  • Modified src/main/ipc/handlers/process.ts to integrate SSH remote execution:
    • Added imports for getSshRemoteConfig, createSshRemoteStoreAdapter, and buildSshCommand
    • Updated MaestroSettings interface to include sshRemotes and defaultSshRemoteId fields
    • Integrated SSH remote detection in process:spawn handler after all agent args are built
  • SSH remote execution logic:
    • Terminal sessions (toolType === 'terminal') are always local (need PTY for shell interaction)
    • For AI agents, resolves effective SSH remote using priority chain (agent override > global default)
    • When SSH remote is configured, wraps command with buildSshCommand()
    • Disables PTY when using SSH (SSH handles terminal emulation)
    • Passes custom environment variables via the SSH remote command string, not locally
    • Uses remoteWorkingDir from SSH config when available, otherwise uses local cwd
  • Logging:
    • Added info log when SSH remote execution is configured with remote details
    • Logs original command, wrapped SSH command, and resolution source
  • Added 8 unit tests in src/__tests__/main/ipc/handlers/process.test.ts:
    • Wrap command with SSH when global default remote is configured
    • Use agent-specific SSH remote override
    • Terminal sessions should not use SSH
    • Pass custom env vars to SSH remote command
    • Not wrap command when SSH is disabled for agent
    • Run locally when no SSH remote is configured
    • Use remoteWorkingDir from SSH config when available

Phase 5: User Story 3 - Per-Agent Remote Override

Goal: User can configure different SSH remotes for different agents.

Test Criteria: Set agent-specific remote in UI, verify that agent uses override instead of global default.

Prerequisite: US1 and US2 complete.

  • T030 [US3] Add SSH remote dropdown to agent config in src/renderer/components/AgentConfigModal.tsx
  • T031 [US3] Save agent SSH remote selection to agent config store in src/renderer/components/AgentConfigModal.tsx
  • T032 [US3] Update getSshRemoteConfig to check agent override first in src/main/process-manager.ts

Phase 6: User Story 4 - Connection Test Functionality

Goal: User can test SSH connection before using it.

Test Criteria: Click "Test Connection" in modal, see success/failure result with remote hostname.

Prerequisite: US1 complete.

  • T033 [US4] Implement ssh-remote:test handler in src/main/ipc/handlers/ssh-remote.ts
  • T034 [US4] Add test IPC call to sshRemote API in src/main/preload.ts
  • T035 [US4] Add testConnection function to useSshRemotes hook in src/renderer/hooks/useSshRemotes.ts
  • T036 [US4] Add Test Connection button and result display in src/renderer/components/Settings/SshRemoteModal.tsx
  • T037 [US4] Add Test button per remote in list in src/renderer/components/Settings/SshRemotesSection.tsx

Phase 7: User Story 5 - Connection Status Visibility

Goal: User sees which remote a session is using and connection status.

Test Criteria: Session header shows remote name indicator when using SSH remote.

Prerequisite: US2 complete.

  • T038 [US5] Add sshRemoteId field to session state tracking in src/renderer/App.tsx
  • T039 [US5] Add remote indicator component to src/renderer/components/SessionHeader.tsx
  • T040 [US5] Show remote name or "Local" based on session config in src/renderer/components/SessionHeader.tsx
  • T041 [US5] Style indicator with appropriate colors (normal/error) in src/renderer/components/SessionHeader.tsx

Phase 8: Polish

Goal: Error handling improvements and edge cases.

  • T042 Add SSH-specific error pattern detection in output parsing in src/main/process-manager.ts
  • T043 Handle "Permission denied" SSH error with user-friendly message
  • T044 Handle "command not found" error for missing agent on remote
  • T045 Handle "Connection refused" error for unreachable host
  • T046 Handle connection drop mid-session with error state

Dependencies

Phase 1 (Setup)
    │
    ▼
Phase 2 (Foundational)
    │
    ▼
Phase 3 (US1: Global Config) ◄── Required for all other stories
    │
    ├──────────────────┬──────────────────┐
    ▼                  ▼                  ▼
Phase 4 (US2)      Phase 6 (US4)      (independent)
    │                  │
    ├──────────────────┤
    ▼                  │
Phase 5 (US3)          │
    │                  │
    ▼                  │
Phase 7 (US5) ◄────────┘
    │
    ▼
Phase 8 (Polish)

Parallel Execution Opportunities

Within Phase 1:

  • T005 (shell-escape.ts) can run parallel to T001-T004 (types)

Within Phase 3 (US1):

  • T012-T019 (backend) must complete before T020-T023 (frontend)
  • T020, T021, T022 can run in parallel (independent React components)

Within Phase 4 (US2):

  • T024, T025 (command builders) can run in parallel
  • T026-T029 must be sequential (all modify process-manager.ts)

Cross-Phase:

  • Phase 4 (US2) and Phase 6 (US4) can run in parallel after Phase 3

Task Summary

Phase Story Task Count
1 Setup 6
2 Foundational 5
3 US1 - Global Config 12
4 US2 - Remote Execution 6
5 US3 - Per-Agent Override 3
6 US4 - Connection Test 5
7 US5 - Status Visibility 4
8 Polish 5
Total 46

MVP Scope

Recommended MVP: Phases 1-4 (US1 + US2)

This delivers:

  • SSH remote configuration (add/edit/delete)
  • Global default remote setting
  • Agent execution via SSH
  • Basic functionality complete

Post-MVP: Phases 5-8 (US3, US4, US5, Polish)

  • Per-agent override
  • Connection testing UI
  • Status indicators
  • Error handling polish

Implementation Strategy

  1. Start with Phase 1-2: Types and core manager (no UI)
  2. Phase 3 backend first: IPC handlers before React components
  3. Phase 4 early: Get remote execution working before UI polish
  4. Parallel track: Phase 6 (connection test) can develop alongside Phase 4
  5. UI polish last: Phases 5, 7, 8 after core functionality works