Skip to content

Commit fa3e1d8

Browse files
committed
feat: Add path normalization, directory ownership checks, and sandbox write restrictions
- Introduced allowed path check with normalization for `/workspace` and `/home` roots to enhance security in guest file operations. - Added guards to restrict writes and directory creations to predefined roots (`/workspace`, `/home`) using `is_allowed_guest_path`. - Implemented functions to normalize and sanitize paths, ensuring traversal protection and consistent resolution. - Enhanced file and directory creation with auto-chown (`uid 1000`) on directories owned by root. - Improved chown operation safety by limiting it to allowed paths only. - Centralized path constants for claudio and sandbox operations (`/home/sandbox/.claude`). - Added extensive tests to validate path normalization, allowed guest paths, and directory ownership policies.
1 parent 1f5feb7 commit fa3e1d8

9 files changed

Lines changed: 1268 additions & 232 deletions

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
/target
22
/artifacts
33
/tmp
4+
package-lock.json
5+
package.json
6+
node_modules

‎claudio/src/main.rs‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
//! ## Skill & MCP awareness
66
//!
77
//! On startup, claudio scans the guest filesystem for provisioned skills and MCP config:
8-
//! - `/root/.claude/skills/*.md` -- SKILL.md files provisioned by AgentBox
9-
//! - `/root/.claude/mcp.json` -- MCP server configuration
8+
//! - `/home/sandbox/.claude/skills/*.md` -- SKILL.md files provisioned by AgentBox
9+
//! - `/home/sandbox/.claude/mcp.json` -- MCP server configuration
1010
//!
1111
//! Discovered skills and MCP servers are:
1212
//! 1. Reported in the system event (`"skills":[...],"mcp_servers":[...]`)
@@ -35,6 +35,8 @@ use std::path::Path;
3535
use std::thread;
3636
use std::time::Duration;
3737

38+
const CLAUDE_HOME: &str = "/home/sandbox/.claude";
39+
3840
fn main() {
3941
// Parse command line args to extract the prompt (mimic claude-code CLI)
4042
let args: Vec<String> = env::args().collect();
@@ -72,11 +74,11 @@ fn extract_prompt(args: &[String]) -> String {
7274

7375
/// Skills and MCP servers discovered in the guest filesystem.
7476
struct DiscoveredSkills {
75-
/// Names of SKILL.md files found in /root/.claude/skills/
77+
/// Names of SKILL.md files found in /home/sandbox/.claude/skills/
7678
skill_files: Vec<String>,
7779
/// First line (title) of each discovered SKILL.md
7880
skill_titles: Vec<String>,
79-
/// Names of MCP servers found in /root/.claude/mcp.json
81+
/// Names of MCP servers found in /home/sandbox/.claude/mcp.json
8082
mcp_servers: Vec<String>,
8183
/// MCP server commands (for simulating tool calls)
8284
mcp_commands: Vec<String>,
@@ -92,8 +94,9 @@ impl DiscoveredSkills {
9294
mcp_commands: Vec::new(),
9395
};
9496

95-
// Scan /root/.claude/skills/*.md
96-
let skills_dir = Path::new("/root/.claude/skills");
97+
// Scan /home/sandbox/.claude/skills/*.md
98+
let skills_dir_path = format!("{}/skills", CLAUDE_HOME);
99+
let skills_dir = Path::new(&skills_dir_path);
97100
if skills_dir.is_dir() {
98101
if let Ok(entries) = fs::read_dir(skills_dir) {
99102
let mut files: Vec<_> = entries
@@ -125,8 +128,9 @@ impl DiscoveredSkills {
125128
}
126129
}
127130

128-
// Read /root/.claude/mcp.json
129-
let mcp_path = Path::new("/root/.claude/mcp.json");
131+
// Read /home/sandbox/.claude/mcp.json
132+
let mcp_path_path = format!("{}/mcp.json", CLAUDE_HOME);
133+
let mcp_path = Path::new(&mcp_path_path);
130134
if mcp_path.is_file() {
131135
if let Ok(content) = fs::read_to_string(mcp_path) {
132136
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
@@ -514,7 +518,7 @@ fn tool_content(
514518
if !discovered.skill_files.is_empty() {
515519
let skill_idx = index % discovered.skill_files.len();
516520
let skill_name = &discovered.skill_files[skill_idx];
517-
let skill_path = format!("/root/.claude/skills/{}.md", skill_name);
521+
let skill_path = format!("{}/skills/{}.md", CLAUDE_HOME, skill_name);
518522
let input = serde_json::json!({
519523
"file_path": &skill_path,
520524
});

‎examples/ollama_local.rs‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//! Example: Run an AgentBox with a local Ollama model.
2+
//!
3+
//! This demonstrates using `LlmProvider::Ollama` so that `claude-code` in
4+
//! the guest VM talks to a local Ollama instance instead of the Anthropic API.
5+
//!
6+
//! ## Prerequisites
7+
//!
8+
//! 1. Install Ollama: https://ollama.com
9+
//! 2. Pull a model: `ollama pull phi4-mini`
10+
//! 3. Ensure Ollama is running: `ollama serve`
11+
//! 4. Build the guest initramfs:
12+
//! ```
13+
//! CLAUDE_CODE_BIN=$(which claude) BUSYBOX=/usr/bin/busybox \
14+
//! scripts/build_guest_image.sh
15+
//! ```
16+
//!
17+
//! ## Run
18+
//!
19+
//! ```bash
20+
//! OLLAMA_MODEL=phi4-mini \
21+
//! VOID_BOX_KERNEL=/boot/vmlinuz-$(uname -r) \
22+
//! VOID_BOX_INITRAMFS=/tmp/void-box-rootfs.cpio.gz \
23+
//! cargo run --example ollama_local
24+
//! ```
25+
//!
26+
//! ## How it works
27+
//!
28+
//! The guest VM reaches Ollama through SLIRP networking:
29+
//!
30+
//! ```text
31+
//! Guest VM Host
32+
//! ┌──────────────┐ ┌──────────────┐
33+
//! │ claude-code │──SLIRP──────>│ Ollama:11434 │
34+
//! │ (stream-json) │ 10.0.2.2 │ (localhost) │
35+
//! └──────────────┘ └──────────────┘
36+
//! ```
37+
//!
38+
//! The SLIRP gateway IP (10.0.2.2) is transparently mapped to 127.0.0.1
39+
//! on the host, so `ANTHROPIC_BASE_URL=http://10.0.2.2:11434` reaches
40+
//! the host's Ollama process.
41+
42+
use std::path::PathBuf;
43+
44+
use void_box::agent_box::AgentBox;
45+
use void_box::llm::LlmProvider;
46+
use void_box::skill::Skill;
47+
48+
#[tokio::main]
49+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
50+
// -- Configuration --
51+
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen3-coder".into());
52+
53+
println!("=== void-box: Ollama Local LLM Example ===");
54+
println!("Model: {}", model);
55+
println!();
56+
57+
// -- Build the AgentBox --
58+
let mut builder = AgentBox::new("ollama_demo")
59+
.llm(LlmProvider::ollama(&model))
60+
.skill(Skill::agent("claude-code"))
61+
.memory_mb(256)
62+
.prompt("Write a short Python script that prints the first 10 Fibonacci numbers. Save it to /workspace/fib.py");
63+
64+
// Use KVM if kernel/initramfs are available, otherwise mock mode
65+
if let (Ok(kernel), Ok(initramfs)) = (
66+
std::env::var("VOID_BOX_KERNEL"),
67+
std::env::var("VOID_BOX_INITRAMFS"),
68+
) {
69+
let kernel = PathBuf::from(&kernel);
70+
let initramfs = PathBuf::from(&initramfs);
71+
if !kernel.as_os_str().is_empty()
72+
&& kernel.exists()
73+
&& !initramfs.as_os_str().is_empty()
74+
&& initramfs.exists()
75+
{
76+
println!("Mode: KVM (real VM)");
77+
println!("Kernel: {}", kernel.display());
78+
println!("Initramfs: {}", initramfs.display());
79+
builder = builder.kernel(kernel).initramfs(initramfs);
80+
} else {
81+
println!("Mode: Mock (KVM artifacts not found)");
82+
builder = builder.mock();
83+
}
84+
} else {
85+
println!("Mode: Mock (set VOID_BOX_KERNEL and VOID_BOX_INITRAMFS for KVM)");
86+
builder = builder.mock();
87+
}
88+
89+
let agent_box = builder.build()?;
90+
91+
println!("LLM Provider: {}", LlmProvider::ollama(&model));
92+
println!();
93+
94+
// -- Run --
95+
println!("--- Running agent ---");
96+
let result = agent_box.run(None).await?;
97+
98+
// -- Results --
99+
println!();
100+
println!("=== Results ===");
101+
println!("Box: {}", result.box_name);
102+
println!("Session: {}", result.claude_result.session_id);
103+
println!("Model: {}", result.claude_result.model);
104+
println!("Error: {}", result.claude_result.is_error);
105+
println!(
106+
"Tokens: {} in / {} out",
107+
result.claude_result.input_tokens, result.claude_result.output_tokens
108+
);
109+
println!("Cost: ${:.4}", result.claude_result.total_cost_usd);
110+
println!("Duration: {}ms", result.claude_result.duration_ms);
111+
println!("Tool calls: {}", result.claude_result.tool_calls.len());
112+
for tc in &result.claude_result.tool_calls {
113+
println!(" - {}", tc.tool_name);
114+
}
115+
println!();
116+
println!("Result text:");
117+
println!("{}", result.claude_result.result_text);
118+
119+
if let Some(ref output) = result.file_output {
120+
println!();
121+
println!("File output ({} bytes):", output.len());
122+
println!("{}", String::from_utf8_lossy(output));
123+
}
124+
125+
Ok(())
126+
}

0 commit comments

Comments
 (0)