Skip to content

Commit 1f5feb7

Browse files
committed
feat: Add LLM provider abstraction, enhance prompt handling, and improve runtime logging
- Introduced `LlmProvider` abstraction to support configurable LLMs (Claude default, Ollama, or custom). - Enhanced prompt generation with embedded inline data for better context without file dependencies. - Added per-request timeout support for fine-grained execution control. - Improved runtime vsock logging for debugging (debug instead of warn for retries/errors). - Updated VSock connection retry logic (increased initial wait to handle larger initramfs for production images). - Enhanced guest networking to map SLIRP gateway IP to localhost, enabling host services access from the guest.
1 parent 15e9b38 commit 1f5feb7

File tree

13 files changed

+317
-67
lines changed

13 files changed

+317
-67
lines changed

examples/claude_in_voidbox_example.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//! 2. Run:
1313
//! ANTHROPIC_API_KEY=sk-ant-xxx \
1414
//! VOID_BOX_KERNEL=/boot/vmlinuz-$(uname -r) \
15-
//! VOID_BOX_INITRAMFS=target/void-box-rootfs.cpio.gz \
15+
//! VOID_BOX_INITRAMFS=/tmp/void-box-rootfs.cpio.gz \
1616
//! cargo run --example claude_in_voidbox_example
1717
//!
1818
//! With OTel export (requires `--features opentelemetry`):

examples/trading_pipeline.rs

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,32 @@
2323
//! cargo run --example trading_pipeline
2424
//!
2525
//! KVM mode (requires kernel + initramfs):
26-
//! VOID_BOX_KERNEL=/boot/vmlinuz-$(uname -r) \
27-
//! VOID_BOX_INITRAMFS=/tmp/void-box-test-rootfs.cpio.gz \
28-
//! cargo run --example trading_pipeline
26+
//! 1. Build the guest initramfs:
27+
//! ```
28+
//! CLAUDE_CODE_BIN=$(which claude) BUSYBOX=/usr/bin/busybox \
29+
//! scripts/build_guest_image.sh
30+
//! ```
31+
//! 2. Run with Anthropic API:
32+
//! ```
33+
//! ANTHROPIC_API_KEY=sk-ant-xxx \
34+
//! VOID_BOX_KERNEL=/boot/vmlinuz-$(uname -r) \
35+
//! VOID_BOX_INITRAMFS=/tmp/void-box-rootfs.cpio.gz \
36+
//! cargo run --example trading_pipeline
37+
//! ```
38+
//! 3. Or run with Ollama (local LLM):
39+
//! ```
40+
//! ollama pull phi4-mini
41+
//! OLLAMA_MODEL=phi4-mini \
42+
//! VOID_BOX_KERNEL=/boot/vmlinuz-$(uname -r) \
43+
//! VOID_BOX_INITRAMFS=/tmp/void-box-rootfs.cpio.gz \
44+
//! cargo run --example trading_pipeline
45+
//! ```
2946
3047
use std::error::Error;
3148
use std::path::PathBuf;
3249

3350
use void_box::agent_box::AgentBox;
51+
use void_box::llm::LlmProvider;
3452
use void_box::pipeline::Pipeline;
3553
use void_box::skill::Skill;
3654

@@ -49,6 +67,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
4967
println!("╚══════════════════════════════════════════════════════════════╝");
5068
println!();
5169

70+
// ---- LLM Provider: Claude (default) or Ollama (opt-in) ----
71+
72+
let llm = detect_llm_provider();
73+
println!("[llm] {}", llm);
74+
5275
// ---- Skills: declared capabilities ----
5376

5477
let reasoning = Skill::agent("claude-code")
@@ -79,60 +102,78 @@ async fn main() -> Result<(), Box<dyn Error>> {
79102
println!("--- Defining Boxes ---");
80103
println!();
81104

82-
let data_box = make_box("data_analyst", use_kvm)
105+
let data_box = make_box("data_analyst", use_kvm, &llm)
83106
.skill(data_skill)
84107
.skill(reasoning.clone())
85108
.prompt(
86-
"You are a financial data analyst. Generate realistic 30-day OHLCV data \
87-
for AAPL, NVDA, MSFT, and GOOGL. Include mock news headlines for each symbol. \
88-
Write a Python script to generate the data and run it. \
89-
Follow the schema from your financial-data-analysis skill."
109+
"You are a financial data analyst. Here is recent market data (Feb 2026):\n\n\
110+
AAPL: price $227, P/E 34, RSI 62, 52w range $170-$243, EPS $2.40 beat est. $2.36, \
111+
Services revenue missed ($23.1B vs $23.5B est.), iPhone revenue +4% YoY.\n\
112+
NVDA: price $138, P/E 55, RSI 71, 52w range $78-$153, data center revenue +95% YoY, \
113+
new Blackwell GPU ramping, China export restrictions tightening.\n\
114+
MSFT: price $442, P/E 36, RSI 58, 52w range $385-$470, Azure grew +29%, \
115+
Copilot revenue accelerating, gaming flat YoY.\n\
116+
GOOGL: price $192, P/E 24, RSI 55, 52w range $152-$207, Search +12%, \
117+
Cloud +28%, DOJ antitrust ruling pending.\n\n\
118+
For each symbol, write a brief data summary with key metrics and recent catalysts.\n\
119+
Do NOT write or run code. Do NOT output JSON or templates.\n\
120+
Write plain text with clear sections per symbol."
90121
)
91122
.build()?;
92123

93124
println!(" [1] {} -- {} skills", data_box.name, data_box.skills.len());
94125

95126
// ---- Box 2: Quant Analyst ----
96127

97-
let quant_box = make_box("quant_analyst", use_kvm)
128+
let quant_box = make_box("quant_analyst", use_kvm, &llm)
98129
.skill(quant_skill)
99130
.skill(reasoning.clone())
100131
.prompt(
101-
"You are a quantitative analyst. Read the market data from /workspace/input.json. \
102-
Compute technical indicators (SMA, RSI, MACD, Bollinger Bands) for each symbol. \
103-
Generate composite trading signals. \
104-
Follow the methodology from your quant-technical-analysis skill."
132+
"You are a quantitative analyst. Read the data summary from /workspace/input.json.\n\n\
133+
For each symbol (AAPL, NVDA, MSFT, GOOGL), provide:\n\
134+
- RSI interpretation (overbought >70, neutral 30-70, oversold <30)\n\
135+
- P/E relative to sector average (Tech sector avg ~28)\n\
136+
- A composite signal: BULLISH, NEUTRAL, or BEARISH\n\n\
137+
Write plain text. Do NOT output JSON or templates.\n\
138+
Use the actual numbers from the input data."
105139
)
106140
.build()?;
107141

108142
println!(" [2] {} -- {} skills", quant_box.name, quant_box.skills.len());
109143

110144
// ---- Box 3: Research Analyst (pure reasoning, no special skills) ----
111145

112-
let sentiment_box = make_box("research_analyst", use_kvm)
146+
let sentiment_box = make_box("research_analyst", use_kvm, &llm)
113147
.skill(reasoning.clone())
114148
.prompt(
115-
"You are a research analyst. Read the technical signals from /workspace/input.json. \
116-
For each symbol, assess the market sentiment considering the technical indicators, \
117-
recent price action, and any news context. Score sentiment from -1.0 (very bearish) \
118-
to +1.0 (very bullish). Provide brief reasoning for each score."
149+
"You are a research analyst. Read the quant analysis from /workspace/input.json.\n\n\
150+
For each symbol (AAPL, NVDA, MSFT, GOOGL):\n\
151+
- Score sentiment from -1.0 (very bearish) to +1.0 (very bullish)\n\
152+
- Write 2 sentences explaining your score\n\n\
153+
Consider the technical signals, fundamentals, and catalysts from the input.\n\
154+
Write plain text. Do NOT output JSON or templates.\n\
155+
Example: AAPL: +0.3 (mildly bullish). The earnings beat suggests..."
119156
)
120157
.build()?;
121158

122159
println!(" [3] {} -- {} skills (pure reasoning)", sentiment_box.name, sentiment_box.skills.len());
123160

124161
// ---- Box 4: Portfolio Strategist ----
125162

126-
let strategy_box = make_box("portfolio_strategist", use_kvm)
163+
let strategy_box = make_box("portfolio_strategist", use_kvm, &llm)
127164
.skill(risk_skill)
128165
.skill(reasoning.clone())
129-
.memory_mb(512)
130166
.prompt(
131-
"You are a portfolio strategist. Read the analysis from /workspace/input.json \
132-
which contains technical signals and sentiment scores. \
133-
Generate specific trade recommendations with position sizing, entry/exit prices, \
134-
stop loss levels, and risk management. \
135-
Follow the framework from your portfolio-risk-management skill."
167+
"You are a portfolio strategist managing a $100,000 portfolio.\n\
168+
Read the sentiment analysis from /workspace/input.json.\n\n\
169+
For each symbol (AAPL, NVDA, MSFT, GOOGL) produce a trade recommendation:\n\
170+
- ACTION: BUY, SELL, or HOLD\n\
171+
- ALLOCATION: percentage of portfolio (must sum to <=100%)\n\
172+
- ENTRY PRICE: target buy price\n\
173+
- STOP LOSS: price to cut losses (set 5-10% below entry)\n\
174+
- RATIONALE: one sentence\n\n\
175+
Keep at least 20% in cash. Write plain text. Do NOT output JSON or templates.\n\
176+
Use real numbers from the analysis, not placeholders."
136177
)
137178
.build()?;
138179

@@ -180,8 +221,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
180221
r.total_cost_usd,
181222
);
182223
if !r.result_text.is_empty() {
183-
let preview = if r.result_text.len() > 120 {
184-
format!("{}...", &r.result_text[..120])
224+
let preview = if r.result_text.len() > 500 {
225+
format!("{}...", &r.result_text[..500])
185226
} else {
186227
r.result_text.clone()
187228
};
@@ -207,8 +248,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
207248
}
208249

209250
/// Create an AgentBox builder pre-configured for the current environment.
210-
fn make_box(name: &str, use_kvm: bool) -> AgentBox {
211-
let mut ab = AgentBox::new(name);
251+
fn make_box(name: &str, use_kvm: bool, llm: &LlmProvider) -> AgentBox {
252+
let mut ab = AgentBox::new(name).llm(llm.clone()).memory_mb(1024);
253+
254+
// Allow per-stage timeout override via STAGE_TIMEOUT_SECS env var
255+
if let Ok(secs) = std::env::var("STAGE_TIMEOUT_SECS") {
256+
if let Ok(s) = secs.parse::<u64>() {
257+
ab = ab.timeout_secs(s);
258+
}
259+
}
212260

213261
if use_kvm {
214262
if let Some(kernel) = kvm_kernel() {
@@ -224,6 +272,37 @@ fn make_box(name: &str, use_kvm: bool) -> AgentBox {
224272
ab
225273
}
226274

275+
/// Detect the LLM provider from environment variables.
276+
///
277+
/// - `OLLAMA_MODEL=qwen3-coder` -> Ollama with that model
278+
/// - `LLM_BASE_URL=...` -> Custom provider
279+
/// - Otherwise -> Claude (default)
280+
fn detect_llm_provider() -> LlmProvider {
281+
// Check for Ollama
282+
if let Ok(model) = std::env::var("OLLAMA_MODEL") {
283+
if !model.is_empty() {
284+
return LlmProvider::ollama(model);
285+
}
286+
}
287+
288+
// Check for custom endpoint
289+
if let Ok(base_url) = std::env::var("LLM_BASE_URL") {
290+
if !base_url.is_empty() {
291+
let mut provider = LlmProvider::custom(base_url);
292+
if let Ok(key) = std::env::var("LLM_API_KEY") {
293+
provider = provider.api_key(key);
294+
}
295+
if let Ok(model) = std::env::var("LLM_MODEL") {
296+
provider = provider.model(model);
297+
}
298+
return provider;
299+
}
300+
}
301+
302+
// Default: Claude
303+
LlmProvider::Claude
304+
}
305+
227306
/// Check if KVM artifacts are available.
228307
fn is_kvm_available() -> bool {
229308
std::path::Path::new("/dev/kvm").exists()

guest-agent/src/main.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,13 @@ fn execute_command(request: &ExecRequest) -> ExecResponse {
471471
cmd.env("PATH", &path);
472472
}
473473

474-
// Set environment variables from request (may override PATH above)
474+
// Child processes run as uid=1000 (sandbox user) but inherit HOME=/root
475+
// from init. Since /root is not writable by uid=1000, set HOME to the
476+
// sandbox user's home directory so tools like claude-code can write to
477+
// $HOME/.claude/ for config and cache.
478+
cmd.env("HOME", "/home/sandbox");
479+
480+
// Set environment variables from request (may override PATH and HOME above)
475481
for (key, value) in &request.env {
476482
cmd.env(key, value);
477483
}

scripts/build_guest_image.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,36 @@ if [[ -n "${CLAUDE_CODE_BIN:-}" && -f "$CLAUDE_CODE_BIN" ]]; then
4848
echo "[void-box] Installing real claude-code from \$CLAUDE_CODE_BIN at /usr/local/bin/claude-code..."
4949
cp "$CLAUDE_CODE_BIN" "$OUT_DIR/usr/local/bin/claude-code"
5050
chmod +x "$OUT_DIR/usr/local/bin/claude-code"
51+
52+
# If the binary is dynamically linked, copy its shared libraries into the
53+
# initramfs so the kernel's ELF loader can find them at runtime.
54+
if file -L "$CLAUDE_CODE_BIN" | grep -q "dynamically linked"; then
55+
echo "[void-box] Detected dynamically linked binary -- copying shared libraries..."
56+
# Use ldd to discover required libraries and their host paths
57+
ldd "$CLAUDE_CODE_BIN" 2>/dev/null | while read -r line; do
58+
# Parse lines like: libc.so.6 => /lib64/libc.so.6 (0x...)
59+
# or: /lib64/ld-linux-x86-64.so.2 (0x...)
60+
lib_path=""
61+
if echo "$line" | grep -q "=>"; then
62+
lib_path=$(echo "$line" | awk '{print $3}')
63+
elif echo "$line" | grep -q "^[[:space:]]*/"; then
64+
lib_path=$(echo "$line" | awk '{print $1}')
65+
fi
66+
67+
# Skip virtual libraries (linux-vdso) and empty paths
68+
if [[ -z "$lib_path" || "$lib_path" == "linux-vdso"* || ! -f "$lib_path" ]]; then
69+
continue
70+
fi
71+
72+
# Preserve the original directory structure in the initramfs
73+
lib_dir=$(dirname "$lib_path")
74+
mkdir -p "$OUT_DIR$lib_dir"
75+
if [[ ! -f "$OUT_DIR$lib_path" ]]; then
76+
cp -L "$lib_path" "$OUT_DIR$lib_path"
77+
echo " -> $lib_path"
78+
fi
79+
done
80+
fi
5181
elif [[ -f "$ROOT_DIR/scripts/guest/claude-code-mock.sh" ]]; then
5282
echo "[void-box] Installing claude-code mock at /usr/local/bin/claude-code..."
5383
cp "$ROOT_DIR/scripts/guest/claude-code-mock.sh" "$OUT_DIR/usr/local/bin/claude-code"

0 commit comments

Comments
 (0)