Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/workflows/kube-workflow-init.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ jobs:
init:
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 }}
40 changes: 40 additions & 0 deletions .github/workflows/rust-ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Rust CI

on:
pull_request:
types:
- opened
- synchronize

env:
CARGO_TERM_COLOR: always

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
components: rustfmt, clippy

- name: Run lint
run: make lint

test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable

- name: Run tests
run: make test
23 changes: 21 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
RUFF := .venv/bin/ruff

.PHONY: help build install dev test clean daemon-build daemon-release

help:
Expand All @@ -20,8 +22,15 @@ release:
dev:
maturin develop -m server/Cargo.toml

test:
pytest tests/
test: lint
@echo "Running Rust tests (daemon)..."
cargo test --package sandd
@echo ""
@echo "Running Rust tests (server protocol)..."
cargo test --package sandbox-server --lib
@echo ""
@echo "Running Python tests..."
pytest python/tests/

daemon-build:
Comment thread
kerthcet marked this conversation as resolved.
Outdated
cargo build --package sandd
Expand All @@ -36,3 +45,13 @@ clean:
rm -rf target/
rm -rf python/sandd.egg-info/
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true

.PHONY: lint
lint: $(RUFF)
$(RUFF) check .
Comment thread
kerthcet marked this conversation as resolved.

$(RUFF):
@echo "Installing ruff..."
@python3 -m venv .venv || true
@.venv/bin/pip install --quiet ruff
@echo "Ruff installed successfully"
2 changes: 1 addition & 1 deletion examples/agent_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def main():

# Get server stats
stats = server.get_stats()
print(f"Server stats:")
print("Server stats:")
print(f" Total daemons: {stats.total_daemons}")
print(f" By platform: {stats.by_platform}")
print(f" Oldest connection: {stats.oldest_connection_secs}s")
Expand Down
1 change: 1 addition & 0 deletions python/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Python tests for SandD
55 changes: 55 additions & 0 deletions python/tests/test_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Unit tests for SandD Server"""
import pytest
from sandd import Server, ServerStats


def test_server_initialization():
"""Test server can be initialized with default parameters"""
server = Server()
assert server.address == "0.0.0.0:8765"
assert server.daemon_count() == 0


def test_server_custom_address():
"""Test server can be initialized with custom host and port"""
server = Server(host="127.0.0.1", port=9999)
assert server.address == "127.0.0.1:9999"


def test_list_daemons_empty():
"""Test listing daemons returns empty list when none connected"""
server = Server()
daemons = server.list_daemons()
assert isinstance(daemons, list)
assert len(daemons) == 0


def test_execute_command_daemon_not_found():
"""Test executing command on non-existent daemon raises ValueError"""
server = Server()
with pytest.raises(ValueError, match="not found"):
server.execute_command("non-existent-daemon", "echo test")


def test_wait_for_daemon_timeout():
"""Test wait_for_daemon returns False on timeout"""
server = Server()
result = server.wait_for_daemon("non-existent", timeout=0.1, poll_interval=0.05)
assert result is False


def test_server_repr():
"""Test server string representation"""
server = Server(host="localhost", port=8080)
repr_str = repr(server)
assert "localhost:8080" in repr_str
assert "daemons=0" in repr_str


def test_get_stats():
"""Test getting server statistics"""
server = Server()
stats = server.get_stats()
assert isinstance(stats, ServerStats)
assert stats.total_daemons == 0
assert isinstance(stats.by_platform, dict)
45 changes: 24 additions & 21 deletions sandd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,7 @@ async fn main() -> Result<()> {
Err(e) => error!("Connection error: {}", e),
}

warn!(
"Reconnecting in {} seconds...",
args.reconnect_interval
);
warn!("Reconnecting in {} seconds...", args.reconnect_interval);
tokio::time::sleep(Duration::from_secs(args.reconnect_interval)).await;
}
}
Expand All @@ -81,7 +78,7 @@ async fn connect_and_serve(
let mut request = server_url.into_client_request()?;
request.headers_mut().insert(
"Sec-WebSocket-Protocol",
tokio_tungstenite::tungstenite::http::HeaderValue::from_static("sandd.v1")
tokio_tungstenite::tungstenite::http::HeaderValue::from_static("sandd.v1"),
);

let (ws_stream, response) = match tokio_tungstenite::connect_async(request).await {
Expand Down Expand Up @@ -184,13 +181,7 @@ async fn connect_and_serve(
};

// Handle message inline
if let Err(e) = handle_message(
message,
ws_tx_clone.clone(),
executor.clone(),
)
.await
{
if let Err(e) = handle_message(message, ws_tx_clone.clone(), executor.clone()).await {
error!("Error handling message: {}", e);
}
}
Expand Down Expand Up @@ -245,13 +236,11 @@ where

let json = serde_json::to_string(&response)?;
let mut tx = ws_tx.lock().await;
tx.send(WsMessage::Text(json)).await.map_err(|e| anyhow::anyhow!("{}", e))?;
tx.send(WsMessage::Text(json)).await?
} else {
// Normal shell execution
debug!("Executing command: {}", command);
let result = executor
.execute(&command, timeout_secs, env, cwd)
.await;
let result = executor.execute(&command, timeout_secs, env, cwd).await;

let response = match result {
Ok(output) => Message::CommandOutput {
Expand All @@ -269,7 +258,9 @@ where

let json = serde_json::to_string(&response)?;
let mut tx = ws_tx.lock().await;
tx.send(WsMessage::Text(json)).await.map_err(|e| anyhow::anyhow!("{}", e))?;
tx.send(WsMessage::Text(json))
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
}
}

Expand All @@ -279,7 +270,10 @@ where
cols: _,
term: _,
} => {
debug!("Starting shell session: {} (not implemented in MVP)", request_id);
debug!(
"Starting shell session: {} (not implemented in MVP)",
request_id
);

// TODO: Shell functionality disabled for MVP due to PtySystem Sync issues
let response = Message::ShellStarted {
Expand All @@ -290,10 +284,15 @@ where

let json = serde_json::to_string(&response)?;
let mut tx = ws_tx.lock().await;
tx.send(WsMessage::Text(json)).await.map_err(|e| anyhow::anyhow!("{}", e))?;
tx.send(WsMessage::Text(json))
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
}

Message::ShellInput { request_id: _, data: _ } => {
Message::ShellInput {
request_id: _,
data: _,
} => {
debug!("Shell input (not implemented)");
// TODO: Shell functionality disabled for MVP
}
Expand Down Expand Up @@ -324,7 +323,11 @@ where
offset,
} => {
// In a full implementation, write chunks to file
debug!("Received file chunk: {} bytes at offset {}", data.len(), offset);
debug!(
"Received file chunk: {} bytes at offset {}",
data.len(),
offset
);
}

Message::FileDownloadStart { request_id, path } => {
Expand Down
1 change: 0 additions & 1 deletion server/src/protocol.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Protocol messages exchanged between agent and daemon
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
4 changes: 2 additions & 2 deletions server/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::protocol::Message;
use crate::registry::{CommandResult, DaemonConnection, DaemonRegistry};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use axum::{
extract::{
ws::{WebSocket, WebSocketUpgrade},
Expand All @@ -14,7 +14,7 @@ use axum::{
use futures_util::{SinkExt, StreamExt};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, oneshot};
use tokio::sync::mpsc;
use tracing::{debug, error, info, warn};

pub struct SandboxServer {
Expand Down
Loading