Skip to content
Merged
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
3 changes: 1 addition & 2 deletions .github/workflows/kube-workflow-init.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:

jobs:
init:
# Use a released version for stable.
uses: kerthcet/github-workflow-as-kube/.github/workflows/workflow-as-kubernetes-init.yaml@main
secrets:
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
6 changes: 1 addition & 5 deletions .github/workflows/kube-workflow.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
name: Event Workflow

env:
GH_DEBUG: api

on:
issues:
types:
Expand All @@ -19,7 +16,6 @@ on:

jobs:
event-handler:
# Use a released version for stable.
uses: kerthcet/github-workflow-as-kube/.github/workflows/workflow-as-kubernetes.yaml@main
secrets:
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
# SandD - Sandbox Daemon
<div align="center">

Remote execution system for sandbox agents. Rust-powered server with Python API, designed for secure command execution in isolated environments.
# SandD

**Sandbox Daemon for Secure Remote Command Execution**

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![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/)

Rust-powered WebSocket server with Python API for secure command execution in isolated environments.

[Features](#features) • [Quick Start](#quick-start) • [Architecture](#architecture) • [Documentation](./docs)

</div>

---

## Features

Expand Down Expand Up @@ -128,7 +142,7 @@ result = server.execute_command(
result = server.execute_command(
"worker-1",
"python long_script.py",
timeout_secs=600,
timeout=600,
cwd="/opt/app"
)
```
Expand Down
2 changes: 1 addition & 1 deletion docs/DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ Include motivation and context.
- Check daemon logs: `RUST_LOG=info ./target/release/sandd ...`

**Commands timing out:**
- Increase `timeout_secs` parameter in `execute_command()`
- Increase `timeout` parameter in `execute_command()` (in seconds)
- Check daemon system resources: `top`, `free -h`
- Verify command actually completes when run manually
- Check daemon logs for errors
Expand Down
69 changes: 47 additions & 22 deletions docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

WebSocket-based JSON protocol for communication between agent and daemon.

## Protocol Versioning

SandD uses WebSocket subprotocol negotiation for versioning via the `Sec-WebSocket-Protocol` header:

**Client (Daemon) Request:**
```
GET /ws HTTP/1.1
Upgrade: websocket
Sec-WebSocket-Protocol: sandd.v1
```

**Server (Agent) Response:**
```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Protocol: sandd.v1
```

**Current Version:** `sandd.v1`

**Benefits:**
- Protocol-native versioning mechanism
- Client can propose multiple versions: `Sec-WebSocket-Protocol: sandd.v1, sandd.v2`
- Server selects best supported version

## Connection Architecture

```
Expand Down Expand Up @@ -45,7 +70,7 @@ All messages are JSON with a `type` field indicating the message type:
### Connection Management

#### Register
**Direction**: Daemon → Agent
**Direction**: Daemon → Agent
**Purpose**: Daemon registers itself when connecting

```json
Expand All @@ -66,7 +91,7 @@ All messages are JSON with a `type` field indicating the message type:
```

#### RegisterAck
**Direction**: Agent → Daemon
**Direction**: Agent → Daemon
**Purpose**: Acknowledge successful registration

```json
Expand All @@ -78,7 +103,7 @@ All messages are JSON with a `type` field indicating the message type:
```

#### Heartbeat
**Direction**: Daemon → Agent
**Direction**: Daemon → Agent
**Purpose**: Keep connection alive (sent every 30 seconds)

```json
Expand All @@ -88,7 +113,7 @@ All messages are JSON with a `type` field indicating the message type:
```

#### Pong
**Direction**: Agent → Daemon
**Direction**: Agent → Daemon
**Purpose**: Response to heartbeat (optional)

```json
Expand All @@ -100,7 +125,7 @@ All messages are JSON with a `type` field indicating the message type:
### Command Execution

#### ExecuteCommand
**Direction**: Agent → Daemon
**Direction**: Agent → Daemon
**Purpose**: Execute a shell command

```json
Expand All @@ -124,7 +149,7 @@ All messages are JSON with a `type` field indicating the message type:
- `cwd`: Working directory (optional)

#### CommandOutput
**Direction**: Daemon → Agent
**Direction**: Daemon → Agent
**Purpose**: Return command execution results

```json
Expand All @@ -139,7 +164,7 @@ All messages are JSON with a `type` field indicating the message type:
```

#### CommandError
**Direction**: Daemon → Agent
**Direction**: Daemon → Agent
**Purpose**: Report command execution error

```json
Expand All @@ -153,7 +178,7 @@ All messages are JSON with a `type` field indicating the message type:
### Interactive Shell (PTY)

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

```json
Expand All @@ -167,7 +192,7 @@ All messages are JSON with a `type` field indicating the message type:
```

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

```json
Expand All @@ -180,7 +205,7 @@ All messages are JSON with a `type` field indicating the message type:
```

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

```json
Expand All @@ -194,7 +219,7 @@ All messages are JSON with a `type` field indicating the message type:
**Note**: `data` is base64-encoded bytes

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

```json
Expand All @@ -208,7 +233,7 @@ All messages are JSON with a `type` field indicating the message type:
**Note**: `data` is base64-encoded bytes

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

```json
Expand All @@ -221,7 +246,7 @@ All messages are JSON with a `type` field indicating the message type:
```

#### ShellExit
**Direction**: Daemon → Agent
**Direction**: Daemon → Agent
**Purpose**: Shell session terminated

```json
Expand All @@ -235,7 +260,7 @@ All messages are JSON with a `type` field indicating the message type:
### File Transfer

#### FileUploadStart
**Direction**: Agent → Daemon
**Direction**: Agent → Daemon
**Purpose**: Begin uploading a file to daemon

```json
Expand All @@ -252,7 +277,7 @@ All messages are JSON with a `type` field indicating the message type:
- `mode`: Unix file permissions (e.g., 420 = 0644 octal), optional

#### FileUploadChunk
**Direction**: Agent → Daemon
**Direction**: Agent → Daemon
**Purpose**: Send file data chunk

```json
Expand All @@ -264,13 +289,13 @@ All messages are JSON with a `type` field indicating the message type:
}
```

**Note**:
**Note**:
- `data` is base64-encoded bytes
- Chunks are typically 64KB
- `offset` tracks position in file

#### FileUploadComplete
**Direction**: Daemon → Agent
**Direction**: Daemon → Agent
**Purpose**: Acknowledge file upload completion

```json
Expand All @@ -283,7 +308,7 @@ All messages are JSON with a `type` field indicating the message type:
```

#### FileDownloadStart
**Direction**: Agent → Daemon
**Direction**: Agent → Daemon
**Purpose**: Request file download from daemon

```json
Expand All @@ -295,7 +320,7 @@ All messages are JSON with a `type` field indicating the message type:
```

#### FileDownloadChunk
**Direction**: Daemon → Agent
**Direction**: Daemon → Agent
**Purpose**: Send file data chunk

```json
Expand All @@ -308,12 +333,12 @@ All messages are JSON with a `type` field indicating the message type:
}
```

**Note**:
**Note**:
- `is_last`: true on final chunk
- Agent buffers chunks until `is_last = true`

#### FileDownloadError
**Direction**: Daemon → Agent
**Direction**: Daemon → Agent
**Purpose**: Report file download error

```json
Expand All @@ -327,7 +352,7 @@ All messages are JSON with a `type` field indicating the message type:
### Error Handling

#### Error
**Direction**: Either
**Direction**: Either
**Purpose**: Generic error message

```json
Expand Down
44 changes: 34 additions & 10 deletions examples/simple_command_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,42 @@
while True:
daemons = server.list_daemons()
stats = server.get_stats()

print(f"\rConnected: {stats.total_daemons} | Platforms: {stats.by_platform}", end="", flush=True)

if daemons and len(daemons) > 0:
daemon_id = daemons[0]
try:
result = server.execute_command(daemon_id, "echo test", timeout_secs=5)
if result.success:
print(f"\n✓ Command test passed on {daemon_id}")
except Exception as e:
print(f"\n✗ Command failed: {e}")

for daemon_id in daemons:
try:
# Test 1: Python script
result = server.execute_command(
daemon_id,
"python3 -c 'import sys; print(f\"Python {sys.version_info.major}.{sys.version_info.minor}\")'",
timeout=5
)
if result.success:
print(f"\n✓ Python test passed on {daemon_id}: {result.stdout.strip()}")
else:
print(f"\n✗ Python test failed on {daemon_id}: exit_code={result.exit_code}")

# Test 2: Wrong Python script (intentional error)
result = server.execute_command(
daemon_id,
"python3 -c 'undefined_variable'",
timeout=5
)
if not result.success:
print(f"✓ Error handling test passed on {daemon_id}, stderr: {result.stderr.strip()}")
else:
print(f"✗ Error handling test failed on {daemon_id}: expected error but got success")

# Test 3: Echo command
result = server.execute_command(daemon_id, "echo 'Hello from daemon!'", timeout=5)
if result.success:
print(f"✓ Echo test passed on {daemon_id}: {result.stdout.strip()}")

except Exception as e:
print(f"\n✗ Command failed on {daemon_id}: {e}")

time.sleep(2)
except KeyboardInterrupt:
print("\n\nShutting down...")
Expand Down
6 changes: 3 additions & 3 deletions python/sandd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def execute_command(
self,
daemon_id: str,
command: str,
timeout_secs: int = 300,
timeout: int = 300,
env: Optional[Dict[str, str]] = None,
cwd: Optional[str] = None,
) -> CommandResult:
Expand All @@ -161,7 +161,7 @@ def execute_command(
Args:
daemon_id: Target daemon ID
command: Command to execute (shell string)
timeout_secs: Execution timeout in seconds (default: 300)
timeout: Execution timeout in seconds (default: 300)
env: Environment variables to set
cwd: Working directory

Expand All @@ -179,7 +179,7 @@ def execute_command(
... print(result.stdout)
"""
result = self._server.execute_command(
daemon_id, command, timeout_secs, env, cwd
daemon_id, command, timeout, env, cwd
)
return CommandResult(result)

Expand Down
Loading
Loading