7.1 KiB
Swift-Rust Communication Architecture
This document describes the inter-process communication (IPC) architecture between the Swift VibeTunnel macOS application and the Rust tty-fwd terminal multiplexer.
Overview
VibeTunnel uses a Unix domain socket for communication between the Swift app and Rust components. This approach avoids UI spawning issues and provides reliable, bidirectional communication.
Architecture Components
1. Terminal Spawn Service (Swift)
File: VibeTunnel/Core/Services/TerminalSpawnService.swift
The TerminalSpawnService listens on a Unix domain socket at /tmp/vibetunnel-terminal.sock and handles requests to spawn terminal windows.
Key features:
- Uses POSIX socket APIs (socket, bind, listen, accept) for reliable Unix domain socket communication
- Runs on a dedicated queue with
.userInitiatedQoS - Automatically cleans up the socket on startup and shutdown
- Handles JSON-encoded spawn requests and responses
- Non-blocking accept loop with proper error handling
Lifecycle:
- Started in
AppDelegate.applicationDidFinishLaunching - Stopped in
AppDelegate.applicationWillTerminate
2. Socket Client (Rust)
File: tty-fwd/src/term_socket.rs
The Rust client connects to the Unix socket to request terminal spawning:
pub fn spawn_terminal_via_socket(
command: &[String],
working_dir: Option<&str>,
) -> Result<String>
Communication Protocol:
Request format (optimized):
{
"command": "tty-fwd --session-id=\"uuid\" -- zsh && exit",
"workingDir": "/Users/example",
"sessionId": "uuid-here",
"ttyFwdPath": "/path/to/tty-fwd",
"terminal": "ghostty" // optional
}
Response format:
{
"success": true,
"error": null,
"sessionId": "uuid-here"
}
Key optimizations:
- Command is pre-formatted in Rust to avoid double-escaping issues
- ttyFwdPath is provided to avoid path discovery
- Terminal preference can be specified per-request
- Working directory handling is simplified
3. Integration Points
Swift Server (Hummingbird)
File: VibeTunnel/Core/Services/TunnelServer.swift
When spawn_terminal: true is received in a session creation request:
- Connects to the Unix socket using low-level socket APIs
- Sends the spawn request
- Reads the response
- Returns appropriate HTTP response to the web UI
Rust API Server
File: tty-fwd/src/api_server.rs
The API server handles HTTP requests and uses spawn_terminal_command when the spawn_terminal flag is set.
Communication Flow
Web UI → HTTP POST /api/sessions (spawn_terminal: true)
↓
API Server (Swift or Rust)
↓
Unix Socket Client
↓
/tmp/vibetunnel-terminal.sock
↓
TerminalSpawnService (Swift)
↓
TerminalLauncher
↓
AppleScript execution
↓
Terminal.app/iTerm2/etc opens with command
Benefits of This Architecture
-
No UI Spawning: The main VibeTunnel app handles all terminal spawning, avoiding macOS restrictions on spawning UI apps from background processes.
-
Process Isolation: tty-fwd doesn't need to know about VibeTunnel's location or how to invoke it.
-
Reliable Communication: Unix domain sockets provide fast, reliable local IPC.
-
Clean Separation: Terminal spawning logic stays in the Swift app where it belongs.
-
Fallback Support: If the socket is unavailable, appropriate error messages guide the user.
Error Handling
Common error scenarios:
-
Socket Unavailable:
- Error: "Terminal spawn service not available at /tmp/vibetunnel-terminal.sock"
- Cause: VibeTunnel app not running or service not started
- Solution: Ensure VibeTunnel is running
-
Permission Denied:
- Error: "Failed to spawn terminal: Accessibility permission denied"
- Cause: macOS security restrictions on AppleScript
- Solution: Grant accessibility permissions to VibeTunnel
-
Terminal Not Found:
- Error: "Selected terminal application not found"
- Cause: Configured terminal app not installed
- Solution: Install the terminal or change preferences
Implementation Notes
Socket Path
The socket path /tmp/vibetunnel-terminal.sock was chosen because:
/tmpis accessible to all processes- Automatically cleaned up on system restart
- No permission issues between different processes
JSON Protocol
JSON was chosen for the protocol because:
- Easy to parse in both Swift and Rust
- Human-readable for debugging
- Extensible for future features
Performance Optimizations
- Pre-formatted Commands: Rust formats the complete command string, avoiding complex escaping logic in Swift
- Path Discovery: tty-fwd path is passed in the request to avoid repeated file system lookups
- Direct Terminal Selection: Terminal preference can be specified per-request without changing global settings
- Simplified Escaping: Using shell-words crate in Rust for proper command escaping
- Reduced Payload Size: Command is a single string instead of an array
Security Considerations
- The socket is created with default permissions (user-only access)
- No authentication is required as it's local-only communication
- The socket is cleaned up on app termination
- Commands are properly escaped using shell-words to prevent injection
Adding New IPC Features
To add new IPC commands:
- Define the request/response structures in both Swift and Rust
- Add a new handler in
TerminalSpawnService.handleRequest - Create a corresponding client function in Rust
- Update error handling for the new command
Example:
struct NewCommand: Codable {
let action: String
let parameters: [String: String]
}
#[derive(serde::Serialize)]
struct NewCommand {
action: String,
parameters: HashMap<String, String>,
}
Debugging
To debug socket communication:
- Check if the socket exists:
ls -la /tmp/vibetunnel-terminal.sock - Monitor Swift logs: Look for
TerminalSpawnServicecategory - Check Rust debug output when running tty-fwd with verbose logging
- Use
netstat -an | grep vibetunnelto see socket connections
Implementation Details
POSIX Socket Implementation
The service uses low-level POSIX socket APIs for maximum compatibility:
// Socket creation
serverSocket = socket(AF_UNIX, SOCK_STREAM, 0)
// Binding to path
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
bind(serverSocket, &addr, socklen_t(MemoryLayout<sockaddr_un>.size))
// Accept connections
let clientSocket = accept(serverSocket, &clientAddr, &clientAddrLen)
This approach avoids the Network framework's limitations with Unix domain sockets and provides reliable, cross-platform compatible IPC.
Historical Context
Previously, tty-fwd would spawn VibeTunnel as a subprocess with CLI arguments. This approach had several issues:
- macOS security restrictions on spawning UI apps
- Duplicate instance detection conflicts
- Complex error handling
- Path discovery problems
The Unix socket approach would resolve these issues while providing a cleaner architecture, but needs to be implemented using lower-level APIs due to Network framework limitations.