This guide is for developers working on SandD itself.
- Rust: Install from https://rustup.rs/
- Python 3.8+: With pip
- Maturin:
pip install maturin
SandD/
├── server/ # Rust server with PyO3 bindings
│ ├── src/
│ │ ├── lib.rs # Python API bindings
│ │ ├── server.rs # WebSocket server (axum)
│ │ ├── registry.rs # Daemon connection registry
│ │ └── protocol.rs # Message protocol
│ └── Cargo.toml
│
├── sandd/ # Rust daemon binary
│ ├── src/
│ │ ├── main.rs # Daemon entry point
│ │ ├── executor.rs # Command execution
│ │ ├── shell.rs # Shell (not implemented)
│ │ └── protocol.rs # Message protocol
│ └── Cargo.toml
│
├── python/sandd/ # Python package wrapper
│ └── __init__.py
│
└── examples/ # Usage examples
# Build everything
./test_build.sh
# Or step by step:
make dev # Build Python package (debug)
make daemon-build # Build daemon (debug)make release # Python package (optimized)
make daemon-release # Daemon binary (optimized)# Python package
maturin develop --release -m server/Cargo.toml
# Daemon
cargo build --package sandd --releaseEdit files in server/src/, sandd/src/, or python/sandd/
# If you changed server/
make dev
# If you changed daemon/
make daemon-build
# If you changed Python wrapper only
# (no rebuild needed, it's just a wrapper)# Terminal 1: Start test agent
python3 examples/simple_test.py
# Terminal 2: Start daemon
./target/debug/sandd --server-url ws://127.0.0.1:8765/wspython3 examples/agent_example.pySee ARCHITECTURE.md for detailed design.
Key concepts:
Python → Registry → Channel → handle_websocket → WebSocket → Daemon
(cmd_tx) (bridge) (cmd_rx) (network)
Why channels? No WebSocket type conflicts, lock-free, idiomatic async Rust.
Outgoing (Python → Daemon):
command_tx: mpsc::UnboundedSender<Message> // Stored in registryIncoming (Daemon → Python):
pending_commands: oneshot::Sender<Result> // Request/Response
shell_sessions: mpsc::Sender<Vec<u8>> // Streaming
file_transfers: Vec<Vec<u8>> // Chunked bufferingcargo test --workspacepytest tests/ # (when tests are added)Use examples/agent_example.py to test all features.
-
Add to
protocol.rs(both server and daemon):Message::MyNewCommand { field: String }
-
Handle in
server/src/server.rs:Message::MyNewCommand { field } => { // Forward to daemon or handle }
-
Handle in
sandd/src/main.rs:Message::MyNewCommand { field } => { // Execute and respond }
-
Add method to
Serverinserver/src/lib.rs:#[pymethods] impl Server { fn my_method(&self, arg: String) -> PyResult<()> { // Implementation } }
-
Add wrapper in
python/sandd/__init__.py:def my_method(self, arg: str) -> None: """User-friendly docstring""" self._server.my_method(arg)
Enable Rust logs:
RUST_LOG=debug ./target/debug/sandd --server-url ws://127.0.0.1:8765/wsPython side:
import logging
logging.basicConfig(level=logging.DEBUG)Check WebSocket traffic:
# In server
RUST_LOG=server=debug python3 examples/simple_test.py-
Interactive Shell: Infrastructure exists, daemon returns "not implemented"
- Reason:
PtySystemSync issues - Fix: Refactor shell manager to avoid Sync constraints
- Reason:
-
File Transfer: Protocol defined, daemon just logs
- Reason: Deferred for MVP
- Fix: Implement actual file I/O in daemon
- PyO3
non_local_definitionswarning: Safe to ignore (PyO3 macro limitation) - Unused imports/variables: Run
cargo fixto clean up
At 200 concurrent daemons:
- Memory: ~2-3 GB
- CPU (idle): ~5%
- CPU (100 cmds/sec): ~15-25%
- Command latency p99: <20ms
- Run tests:
cargo test --workspace - Check formatting:
cargo fmt --all - Check lints:
cargo clippy --all - Test manually with examples
- Update docs if adding features
Add feature: brief description
More detailed explanation if needed.
Include motivation and context.
- Keep dependencies minimal
- Prefer well-maintained crates
- Check licensing compatibility (MIT)
- Update version in
pyproject.tomlandCargo.tomlfiles - Update
CHANGELOG.md(when added) - Build release:
make release && make daemon-release - Test release build
- Tag:
git tag v0.x.0 - Build wheel:
maturin build --release -m server/Cargo.toml - Publish:
maturin publish(when ready)
Daemon won't connect:
- Check agent URL is reachable:
curl -v ws://agent-host:8765/ws - Verify firewall allows outbound WebSocket connections
- Check agent server is running:
ps aux | grep python - Check daemon logs:
RUST_LOG=info ./target/release/sandd ...
Commands timing out:
- Increase
timeoutparameter inexecute_command()(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
- Check number of connected daemons:
server.daemon_count()
Symptom: PanicException: there is no reactor running
Cause: Trying to use tokio::runtime::Handle::current() from Python thread
Fix: Store runtime handle in the struct, use it for block_on()
Symptom: expected tokio_tungstenite::WebSocket, found axum::WebSocket
Solution: Use channels, don't store WebSocket directly in registry
Symptom: Maturin can't find package in workspace
Fix: Use -m server/Cargo.toml flag
WebSocket-based JSON protocol for agent-daemon communication.
For complete protocol specification, see PROTOCOL.md.
- Check ARCHITECTURE.md for design details
- Check PROTOCOL.md for protocol specification
- Check STATUS.md for implementation status
- Check QUICKSTART.md for usage examples