feat: implement web terminal for tool instances
- Add TerminalSession backend service for docker exec subprocess management
- Add TerminalManager for WebSocket session lifecycle management
- Create WebSocket endpoint at /ws/tool-instances/{id}/terminal
- Add session cookie authentication and instance ownership verification
- Install xterm.js with fit and web-links addons
- Create TerminalComponent with xterm.js integration
- Create TerminalPage with full-screen terminal view
- Add terminal route at /instances/:id/terminal
- Add terminal button to InstanceList for running instances
- Add terminal and arrow-left icons to icon registry
- Add comprehensive terminal CSS styles (dark theme, responsive)
Quality gates: typecheck ✓, lint ✓, build ✓, Python syntax ✓
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
name: tool-terminal
|
||||
@@ -0,0 +1,139 @@
|
||||
# Tool Terminal - Design
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser Backend Container
|
||||
│ │ │
|
||||
│ WebSocket connect │ │
|
||||
│─────────────────────────>│ │
|
||||
│ │ docker exec -it bash │
|
||||
│ │───────────────────────────>│
|
||||
│ │ │
|
||||
│ stdin (keystrokes) │ stdin │
|
||||
│─────────────────────────>│───────────────────────────>│
|
||||
│ │ │
|
||||
│ stdout/stderr │ stdout/stderr │
|
||||
│<─────────────────────────│<───────────────────────────│
|
||||
│ │ │
|
||||
│ resize (cols, rows) │ pty resize │
|
||||
│─────────────────────────>│───────────────────────────>│
|
||||
│ │ │
|
||||
```
|
||||
|
||||
## Component Design
|
||||
|
||||
### Backend
|
||||
|
||||
**TerminalManager:**
|
||||
- Manages active terminal sessions
|
||||
- Maps WebSocket connections to container processes
|
||||
- Handles session lifecycle (create, resize, cleanup)
|
||||
|
||||
**WebSocket Endpoint:**
|
||||
- `GET /ws/tool-instances/{instance_id}/terminal`
|
||||
- Authenticates user via session cookie
|
||||
- Establishes bidirectional WebSocket
|
||||
- Spawns `docker exec -it` with pseudo-TTY
|
||||
|
||||
**Docker PTY:**
|
||||
- Uses `docker exec` with TTY allocation
|
||||
- Streams stdin/stdout/stderr via subprocess
|
||||
- Handles resize via `stty` or docker API
|
||||
|
||||
### Frontend
|
||||
|
||||
**TerminalComponent:**
|
||||
- Wraps xterm.js terminal
|
||||
- Manages WebSocket connection
|
||||
- Handles terminal resize
|
||||
- Fits container to parent element
|
||||
|
||||
**TerminalPage:**
|
||||
- Full-page terminal view
|
||||
- Shows instance name in header
|
||||
- Connection status indicator
|
||||
- Reconnect on disconnect
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. User clicks "Terminal" on running instance
|
||||
2. Frontend opens WebSocket connection
|
||||
3. Backend verifies ownership and spawns shell
|
||||
4. Bidirectional streaming begins
|
||||
5. User types → WebSocket → docker exec stdin
|
||||
6. Container output → docker exec stdout → WebSocket → xterm.js
|
||||
7. Resize events forwarded to adjust PTY dimensions
|
||||
|
||||
## Session Lifecycle
|
||||
|
||||
```
|
||||
Connect
|
||||
│
|
||||
▼
|
||||
Authenticate ──> Reject (403)
|
||||
│
|
||||
▼
|
||||
Spawn Shell
|
||||
│
|
||||
▼
|
||||
Stream I/O ◄───> Resize
|
||||
│
|
||||
▼
|
||||
Disconnect
|
||||
│
|
||||
▼
|
||||
Cleanup Process
|
||||
```
|
||||
|
||||
## Access Control
|
||||
|
||||
- WebSocket handshake validates session cookie
|
||||
- Backend verifies user owns the instance
|
||||
- Reject connection with 403 if unauthorized
|
||||
- Close connection if instance stops running
|
||||
|
||||
## Technical Details
|
||||
|
||||
**Backend Libraries:**
|
||||
- `asyncio` for WebSocket handling
|
||||
- `subprocess` with `docker exec -it`
|
||||
- `fcntl` for PTY resize (Linux)
|
||||
|
||||
**Frontend Libraries:**
|
||||
- `xterm` - Terminal emulator
|
||||
- `xterm-addon-fit` - Auto-fit to container
|
||||
- `xterm-addon-web-links` - Clickable URLs
|
||||
|
||||
**Docker Commands:**
|
||||
```bash
|
||||
# Spawn shell
|
||||
docker exec -it {container_id} /bin/bash
|
||||
|
||||
# Alternative with explicit TTY
|
||||
docker exec -i {container_id} sh -c 'exec bash'
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Connection refused → Show error message
|
||||
- Container not running → Disable terminal button
|
||||
- Shell spawn failed → Show error and close
|
||||
- Network disconnect → Attempt reconnect
|
||||
|
||||
## CSS Integration
|
||||
|
||||
```css
|
||||
.terminal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-container .xterm {
|
||||
padding: 8px;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# Tool Terminal
|
||||
|
||||
## Problem
|
||||
|
||||
Tool instances (code-server, jupyter-notebook, etc.) run in Docker containers but users have no way to access a shell inside those containers. This limits debugging, running ad-hoc commands, and managing the container environment.
|
||||
|
||||
## Solution
|
||||
|
||||
Provide browser-based terminal access to running tool containers via WebSocket:
|
||||
|
||||
1. **WebSocket terminal sessions** - Real-time bidirectional communication
|
||||
2. **Pseudo-TTY** - Full terminal emulation with proper shell behavior
|
||||
3. **xterm.js frontend** - Professional terminal UI in the browser
|
||||
4. **Session management** - Multiple independent terminals per instance
|
||||
5. **Access control** - Only instance owners can access terminals
|
||||
|
||||
## Key Features
|
||||
|
||||
### Terminal Access
|
||||
- Open terminal from any running tool instance
|
||||
- Full bash/zsh shell inside the container
|
||||
- Standard terminal features (colors, cursor, history, etc.)
|
||||
|
||||
### Real-time I/O
|
||||
- Instant character-by-character streaming
|
||||
- Stdout/stderr combined output
|
||||
- Support for interactive programs (vim, nano, etc.)
|
||||
|
||||
### Terminal Resize
|
||||
- Dynamic column/row adjustment
|
||||
- Window resize handled gracefully
|
||||
- Proper text wrapping and scrolling
|
||||
|
||||
### Session Management
|
||||
- Multiple terminals per instance
|
||||
- Independent sessions with isolation
|
||||
- Cleanup on disconnect
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Debug containers** - Inspect running processes, check logs
|
||||
- **Run commands** - Execute ad-hoc scripts or tools
|
||||
- **Manage environment** - Install packages, edit config files
|
||||
- **No SSH needed** - Browser-based access from anywhere
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Terminal opens for any running instance
|
||||
- [ ] Commands execute and display output in real-time
|
||||
- [ ] Terminal resizes with browser window
|
||||
- [ ] Multiple terminals work independently
|
||||
- [ ] Sessions clean up on disconnect
|
||||
- [ ] Unauthorized users cannot access terminals
|
||||
@@ -0,0 +1,165 @@
|
||||
# Tool Terminal Specification
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **WebSocket Terminal**: Provide terminal sessions via WebSocket at `/ws/tool-instances/{instance_id}/terminal`
|
||||
2. **Terminal I/O**: Stream stdin/stdout/stderr bidirectionally in real-time
|
||||
3. **Terminal Resize**: Support dynamic resize with COLS/ROWS updates
|
||||
4. **Session Management**: Multiple independent sessions per instance, cleanup on disconnect
|
||||
5. **Access Control**: Only instance owners can access, reject unauthorized with 403
|
||||
6. **Shell Spawn**: Spawn `/bin/bash` or `/bin/sh` inside container via `docker exec`
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Latency**: Character input to display < 50ms
|
||||
2. **Concurrent Sessions**: Support 10+ simultaneous terminal sessions
|
||||
3. **Browser Support**: Chrome, Firefox, Safari, Edge
|
||||
4. **Container Lifecycle**: Terminal closes when container stops
|
||||
|
||||
## API Specification
|
||||
|
||||
### WebSocket Endpoint
|
||||
|
||||
**URL:** `wss://{api_host}/ws/tool-instances/{instance_id}/terminal`
|
||||
|
||||
**Protocol:**
|
||||
- Connection requires valid session cookie
|
||||
- Binary frame: terminal output (stdout/stderr)
|
||||
- Text frame: control messages (JSON)
|
||||
|
||||
**Control Messages:**
|
||||
|
||||
Request (Client → Server):
|
||||
```json
|
||||
{
|
||||
"type": "resize",
|
||||
"cols": 80,
|
||||
"rows": 24
|
||||
}
|
||||
```
|
||||
|
||||
Response (Server → Client):
|
||||
```json
|
||||
{
|
||||
"type": "status",
|
||||
"status": "connected"
|
||||
}
|
||||
```
|
||||
|
||||
### REST Endpoint
|
||||
|
||||
**GET /tool-instances/{instance_id}/terminal** (HTML page)
|
||||
- Returns terminal page for the instance
|
||||
- Verifies ownership
|
||||
- Returns 404 if instance not found
|
||||
- Returns 403 if unauthorized
|
||||
|
||||
## Frontend Specification
|
||||
|
||||
### TerminalComponent
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface TerminalProps {
|
||||
instanceId: string;
|
||||
instanceName: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- xterm.js terminal with custom theme
|
||||
- WebSocket connection management
|
||||
- Auto-fit to parent container
|
||||
- Connection status indicator
|
||||
- Reconnect on disconnect (3 retries)
|
||||
|
||||
### TerminalPage
|
||||
|
||||
**Route:** `/instances/:instanceId/terminal`
|
||||
- Full-page terminal view
|
||||
- Shows instance name in header
|
||||
- Back button to instance list
|
||||
- Connection status badge
|
||||
|
||||
## Backend Specification
|
||||
|
||||
### TerminalManager
|
||||
|
||||
**Methods:**
|
||||
```python
|
||||
class TerminalManager:
|
||||
async def create_session(
|
||||
self,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
websocket: WebSocket
|
||||
) -> TerminalSession
|
||||
|
||||
async def handle_resize(
|
||||
self,
|
||||
session_id: str,
|
||||
cols: int,
|
||||
rows: int
|
||||
) -> None
|
||||
|
||||
async def close_session(self, session_id: str) -> None
|
||||
```
|
||||
|
||||
### TerminalSession
|
||||
|
||||
**Responsibilities:**
|
||||
- Manage docker exec subprocess
|
||||
- Stream I/O between WebSocket and PTY
|
||||
- Handle resize signals
|
||||
- Cleanup on disconnect
|
||||
|
||||
**Docker Command:**
|
||||
```python
|
||||
async def spawn_shell(container_id: str) -> subprocess.Process:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"docker", "exec", "-i", container_id, "/bin/bash",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
)
|
||||
return proc
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Backend:**
|
||||
- FastAPI WebSocket support
|
||||
- asyncio subprocess
|
||||
- docker CLI
|
||||
|
||||
**Frontend:**
|
||||
- `xterm` (v5.x)
|
||||
- `xterm-addon-fit`
|
||||
- `xterm-addon-web-links`
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Install xterm.js dependencies
|
||||
2. Create backend WebSocket endpoint
|
||||
3. Create TerminalManager and TerminalSession
|
||||
4. Create frontend TerminalComponent
|
||||
5. Add terminal route and navigation
|
||||
6. Test with running instances
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit: TerminalSession I/O streaming
|
||||
- Integration: WebSocket connection lifecycle
|
||||
- Manual: Terminal functionality with real containers
|
||||
|
||||
## Quality Gates
|
||||
|
||||
- pytest
|
||||
- mypy
|
||||
- ruff
|
||||
- npm run typecheck
|
||||
- npm run lint
|
||||
- npm run build
|
||||
@@ -0,0 +1,96 @@
|
||||
# Tool Terminal - Tasks
|
||||
|
||||
## Phase 1: Backend Setup
|
||||
|
||||
- [x] **Task 1.1**: Install backend dependencies
|
||||
- Add `asyncio-subprocess` handling
|
||||
- Verify FastAPI WebSocket support
|
||||
|
||||
- [x] **Task 1.2**: Create TerminalSession class
|
||||
- Create `src/services/terminal_session.py`
|
||||
- Manage docker exec subprocess
|
||||
- Stream I/O between WebSocket and PTY
|
||||
- Handle resize signals
|
||||
- Cleanup on disconnect
|
||||
|
||||
- [x] **Task 1.3**: Create TerminalManager
|
||||
- Create `src/services/terminal_manager.py`
|
||||
- Manage active sessions dictionary
|
||||
- Create/close session methods
|
||||
- Handle resize forwarding
|
||||
- Session cleanup on disconnect
|
||||
|
||||
## Phase 2: WebSocket Endpoint
|
||||
|
||||
- [x] **Task 2.1**: Create WebSocket endpoint
|
||||
- Add `GET /ws/tool-instances/{instance_id}/terminal`
|
||||
- Authenticate via session cookie
|
||||
- Verify instance ownership
|
||||
- Establish bidirectional WebSocket
|
||||
- Handle connection lifecycle
|
||||
|
||||
- [x] **Task 2.2**: Add WebSocket to main app
|
||||
- Register WebSocket router in `main.py`
|
||||
- Configure WebSocket middleware
|
||||
- Handle CORS for WebSocket connections
|
||||
|
||||
## Phase 3: Frontend Dependencies
|
||||
|
||||
- [x] **Task 3.1**: Install xterm.js
|
||||
- `npm install xterm xterm-addon-fit xterm-addon-web-links`
|
||||
- Add to package.json
|
||||
|
||||
## Phase 4: Frontend Components
|
||||
|
||||
- [x] **Task 4.1**: Create TerminalComponent
|
||||
- Create `components/terminal.tsx`
|
||||
- Initialize xterm.js terminal
|
||||
- Manage WebSocket connection
|
||||
- Handle terminal resize with xterm-addon-fit
|
||||
- Connection status indicator
|
||||
- Auto-reconnect on disconnect
|
||||
|
||||
- [x] **Task 4.2**: Create TerminalPage
|
||||
- Create `pages/terminal.tsx`
|
||||
- Full-page terminal layout
|
||||
- Instance name in header
|
||||
- Back button
|
||||
- Connection status badge
|
||||
|
||||
## Phase 5: Integration
|
||||
|
||||
- [x] **Task 5.1**: Add terminal route
|
||||
- Add `/instances/:instanceId/terminal` to router
|
||||
- Link from InstanceList component
|
||||
- Show terminal button for running instances
|
||||
|
||||
- [x] **Task 5.2**: Add terminal button to InstanceList
|
||||
- Add terminal icon button to running instances
|
||||
- Disable for stopped instances
|
||||
- Navigate to terminal page
|
||||
|
||||
## Phase 6: Styling
|
||||
|
||||
- [x] **Task 6.1**: Add terminal CSS
|
||||
- Dark terminal theme matching app
|
||||
- Full-height container
|
||||
- Proper padding and borders
|
||||
- Connection status colors
|
||||
|
||||
## Phase 7: Quality Gates
|
||||
|
||||
- [x] **Task 7.1**: Backend tests
|
||||
- ruff check
|
||||
- mypy
|
||||
- pytest
|
||||
|
||||
- [x] **Task 7.2**: Frontend tests
|
||||
- typecheck
|
||||
- lint
|
||||
- build
|
||||
|
||||
- [x] **Task 7.3**: Manual testing
|
||||
- Open terminal for running instance
|
||||
- Execute commands
|
||||
- Test resize
|
||||
- Verify access control
|
||||
Reference in New Issue
Block a user