Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/rust-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,5 @@ jobs:
with:
toolchain: stable

- name: Run Unit tests
- name: Run tests
run: make test
Comment thread
kerthcet marked this conversation as resolved.

- name: Run E2E tests
run: make test-e2e
7 changes: 2 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,12 @@ test-e2e: $(PYTEST) dev
@echo "Cleaning up containers..."
docker compose -f docker-compose.e2e.yml down

docker-build:
docker compose -f docker-compose.e2e.yml build
docker-up:
docker compose -f docker-compose.e2e.yml up -d

docker-down:
docker compose -f docker-compose.e2e.yml down

test-all: test test-e2e
@echo "All tests completed successfully"

.PHONY: lint
lint: $(RUFF)
$(RUFF) check .
Expand Down
46 changes: 23 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# SandD

**A Lightweight Sandbox Daemon for Secure Agent Execution in Isolated Environments.**
**A Lightweight Sandbox Daemon that Provides Secure, Isolated Execution Environments for Agents.**

[![Rust](https://img.shields.io/badge/rust-1.70+-orange.svg)](https://www.rust-lang.org/)
[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/)
Expand All @@ -18,8 +18,8 @@ Rust-powered WebSocket server with Python API for secure command execution in is

## Features

- ✅ **Command Execution**: Execute shell commands remotely with timeout support
- ✅ **Interactive Shell (PTY)**: Full terminal sessions for debugging and manual work
- ✅ **Command Execution**: Execute commands remotely with timeout support
- ✅ **Interactive Sessions (PTY)**: Full terminal sessions for debugging and manual work
- ✅ **File Transfer**: Upload/download files between agent and daemons
- ✅ **High Performance**: Rust-powered WebSocket server handles 200+ concurrent connections
- ✅ **Auto Reconnection**: Daemons automatically reconnect if connection drops
Expand All @@ -29,25 +29,25 @@ Rust-powered WebSocket server with Python API for secure command execution in is
## Architecture

```
┌─────────────────────────────────────────┐
│ Python Agent Application │
│ ┌────────────────────────────────────┐ │
│ │ from sandd import Server │ │
│ │ │ │
│ │ server = Server("0.0.0.0", 8765) │ │
│ │ result = server.execute_command( │ │
│ │ "daemon-1", "ls -la" │ │
│ │ ) │ │
│ └────────────────────────────────────┘ │
│ ▲ │
│ │ Python bindings (PyO3) │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ Rust WebSocket Server (tokio) │ │
│ │ • Command routing │ │
│ │ • Session management │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────
│ Python Agent Application
│ ┌────────────────────────────────────┐
│ │ from sandd import Server │
│ │ │
│ │ server = Server("0.0.0.0", 8765) │
│ │ result = server.exec( │
│ │ "daemon-1", "ls -la" │
│ │ ) │
Comment thread
kerthcet marked this conversation as resolved.
│ └────────────────────────────────────┘
│ ▲
│ │ Python bindings (PyO3)
│ ▼
│ ┌────────────────────────────────────┐
│ │ Rust WebSocket Server (tokio) │
│ │ • Command routing │
│ │ • Session management │
│ └────────────────────────────────────┘
└─────────────────────────────────────────
│ WebSocket (WSS)
│ (Daemon initiates connection)
Expand Down Expand Up @@ -90,7 +90,7 @@ print(f"Server listening on {server.address}")
server.wait_for_daemon("worker-1", timeout=30)

# Execute command
result = server.execute_command("worker-1", "hostname")
result = server.exec("worker-1", "hostname")
print(f"Output: {result.stdout}")
```

Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ At 200+ connections, Python asyncio:
### Why WebSocket?

- Persistent bidirectional connection
- Efficient for streaming (shell output)
- Efficient for streaming (session output)
- Well-supported libraries
- Can multiplex multiple sessions over one connection

Expand Down
16 changes: 6 additions & 10 deletions docs/DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ SandD/
│ ├── src/
│ │ ├── main.rs # Daemon entry point
│ │ ├── executor.rs # Command execution
│ │ ├── shell.rs # Shell (not implemented)
│ │ ├── session.rs # Interactive sessions (PTY)
│ │ └── protocol.rs # Message protocol
│ └── Cargo.toml
Expand Down Expand Up @@ -124,7 +124,7 @@ command_tx: mpsc::UnboundedSender<Message> // Stored in registry
**Incoming (Daemon → Python):**
```rust
pending_commands: oneshot::Sender<Result> // Request/Response
shell_sessions: mpsc::Sender<Vec<u8>> // Streaming
sessions: mpsc::Sender<Vec<u8>> // Streaming
file_transfers: Vec<Vec<u8>> // Chunked buffering
```

Expand Down Expand Up @@ -211,11 +211,7 @@ RUST_LOG=server=debug python3 examples/simple_test.py

### Not Implemented

1. **Interactive Shell**: Infrastructure exists, daemon returns "not implemented"
- Reason: `PtySystem` Sync issues
- Fix: Refactor shell manager to avoid Sync constraints

2. **File Transfer**: Protocol defined, daemon just logs
1. **File Transfer**: Protocol defined, daemon just logs
- Reason: Deferred for MVP
- Fix: Implement actual file I/O in daemon

Expand Down Expand Up @@ -278,14 +274,14 @@ Include motivation and context.
- Check daemon logs: `RUST_LOG=info ./target/release/sandd ...`

**Commands timing out:**
- Increase `timeout` parameter in `execute_command()` (in seconds)
- Increase `timeout` parameter in `exec()` (in seconds)
- Check daemon system resources: `top`, `free -h`
- Verify command actually completes when run manually
- Check daemon logs for errors

**High memory usage:**
- Monitor active shell sessions (they hold state)
- Close unused shell sessions
- Monitor active sessions (they hold state)
- Close unused sessions with `session.close()`
- Check number of connected daemons: `server.daemon_count()`

### Development Issues
Expand Down
81 changes: 46 additions & 35 deletions docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ All messages are JSON with a `type` field indicating the message type:

```json
{
"type": "execute_command",
"command_id": "uuid-here",
"type": "exec",
Comment thread
kerthcet marked this conversation as resolved.
"request_id": "uuid-here",
"command": "ls -la",
"timeout_secs": 300,
"env": {},
Expand Down Expand Up @@ -130,8 +130,8 @@ All messages are JSON with a `type` field indicating the message type:

```json
{
"type": "execute_command",
"command_id": "550e8400-e29b-41d4-a716-446655440000",
"type": "exec",
Comment thread
kerthcet marked this conversation as resolved.
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"command": "python script.py",
"timeout_secs": 300,
"env": {
Expand All @@ -142,7 +142,7 @@ All messages are JSON with a `type` field indicating the message type:
```

**Fields**:
- `command_id`: Unique identifier for tracking this command
- `request_id`: Unique identifier for tracking this request
- `command`: Shell command to execute
- `timeout_secs`: Maximum execution time (default: 300)
- `env`: Environment variables (optional)
Expand All @@ -155,7 +155,7 @@ All messages are JSON with a `type` field indicating the message type:
```json
{
"type": "command_output",
"command_id": "550e8400-e29b-41d4-a716-446655440000",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"stdout": "output text...",
"stderr": "",
"exit_code": 0,
Expand All @@ -170,88 +170,99 @@ All messages are JSON with a `type` field indicating the message type:
```json
{
"type": "command_error",
"command_id": "550e8400-e29b-41d4-a716-446655440000",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"error": "command not found"
}
```

### Interactive Shell (PTY)
### Interactive Session (PTY)

#### StartShell
#### StartSession
**Direction**: Agent → Daemon
**Purpose**: Start an interactive shell session
**Purpose**: Start an interactive session

```json
{
"type": "start_shell",
"type": "start_session",
"session_id": "550e8400-e29b-41d4-a716-446655440001",
"rows": 24,
"cols": 80,
"term": "xterm-256color"
}
```

#### ShellStarted
#### SessionStarted
**Direction**: Daemon → Agent
**Purpose**: Acknowledge shell started
**Purpose**: Acknowledge session started

```json
{
"type": "shell_started",
"type": "session_started",
"session_id": "550e8400-e29b-41d4-a716-446655440001",
"success": true,
"error": null
}
```

#### ShellInput
#### SessionInput
**Direction**: Agent → Daemon
**Purpose**: Send user input to shell
**Purpose**: Send user input to session

```json
{
"type": "shell_input",
"type": "session_input",
"session_id": "550e8400-e29b-41d4-a716-446655440001",
"data": "bHMgLWxhCg=="
}
```

**Note**: `data` is base64-encoded bytes

#### ShellOutput
#### SessionOutput
**Direction**: Daemon → Agent
**Purpose**: Stream shell output back to agent
**Purpose**: Stream session output back to agent

```json
{
"type": "shell_output",
"type": "session_output",
"session_id": "550e8400-e29b-41d4-a716-446655440001",
"data": "ZmlsZTEgIGZpbGUyICBmaWxlMwo="
}
```

**Note**: `data` is base64-encoded bytes

#### ShellResize
#### SessionResize
**Direction**: Agent → Daemon
**Purpose**: Resize terminal window

```json
{
"type": "shell_resize",
"type": "session_resize",
"session_id": "550e8400-e29b-41d4-a716-446655440001",
"rows": 50,
"cols": 120
}
```

#### ShellExit
#### SessionClose
**Direction**: Agent → Daemon
**Purpose**: Close session

```json
{
"type": "session_close",
"session_id": "550e8400-e29b-41d4-a716-446655440001"
}
```

#### SessionExit
**Direction**: Daemon → Agent
**Purpose**: Shell session terminated
**Purpose**: Session terminated

```json
{
"type": "shell_exit",
"type": "session_exit",
"session_id": "550e8400-e29b-41d4-a716-446655440001",
"exit_code": 0
}
Expand Down Expand Up @@ -367,25 +378,25 @@ All messages are JSON with a `type` field indicating the message type:

### Request/Response (Command Execution)

1. Agent generates unique `command_id`
2. Agent registers oneshot channel for this command
1. Agent generates unique `request_id`
2. Agent registers oneshot channel for this request
3. Agent sends `ExecuteCommand` message
4. Daemon executes and sends back `CommandOutput`
5. Agent resolves channel, Python receives result

**Concurrency**: Multiple commands can execute in parallel

### Streaming (Shell Sessions)
### Streaming (Interactive Sessions)

1. Agent generates unique `session_id`
2. Agent registers mpsc channel for this session
3. Agent sends `StartShell` message
3. Agent sends `StartSession` message
4. Daemon starts PTY and begins streaming output
5. Agent sends `ShellInput` as user types
6. Daemon sends `ShellOutput` continuously
7. Session ends with `ShellExit`
5. Agent sends `SessionInput` as user types
6. Daemon sends `SessionOutput` continuously
7. Session ends with `SessionExit`

**Concurrency**: Multiple shell sessions per daemon supported
**Concurrency**: Multiple sessions per daemon supported

### Chunked Transfer (File Download)

Expand Down Expand Up @@ -413,7 +424,7 @@ All messages are JSON with a `type` field indicating the message type:
- Agent detects closed connection
- Registry removes daemon
- All pending commands fail
- Shell sessions terminate
- Terminate
```

## Heartbeat & Connection Monitoring
Expand All @@ -434,4 +445,4 @@ All messages are JSON with a `type` field indicating the message type:

See `server/src/protocol.rs` for the complete Rust implementation using serde for JSON serialization.

Binary data (shell I/O, file chunks) is base64-encoded for JSON compatibility.
Binary data (session I/O, file chunks) is base64-encoded for JSON compatibility.
Loading
Loading