Merge remote-tracking branch 'origin/rcc/sandbox' into integration/dori-cleanroom

# Conflicts:
#	rust/crates/commands/src/lib.rs
#	rust/crates/runtime/src/config.rs
#	rust/crates/runtime/src/lib.rs
#	rust/crates/rusty-claude-cli/src/main.rs
This commit is contained in:
YeonGyu-Kim
2026-04-02 10:42:15 +09:00
4 changed files with 124 additions and 5 deletions

View File

@@ -60,6 +60,13 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
argument_hint: None,
resume_supported: true,
},
SlashCommandSpec {
name: "sandbox",
aliases: &[],
summary: "Show sandbox isolation status",
argument_hint: None,
resume_supported: true,
},
SlashCommandSpec {
name: "compact",
aliases: &[],
@@ -229,6 +236,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
pub enum SlashCommand {
Help,
Status,
Sandbox,
Compact,
Bughunter {
scope: Option<String>,
@@ -300,6 +308,7 @@ impl SlashCommand {
Some(match command {
"help" => Self::Help,
"status" => Self::Status,
"sandbox" => Self::Sandbox,
"compact" => Self::Compact,
"bughunter" => Self::Bughunter {
scope: remainder_after_command(trimmed, command),
@@ -1188,6 +1197,7 @@ pub fn handle_slash_command(
| SlashCommand::Ultraplan { .. }
| SlashCommand::Teleport { .. }
| SlashCommand::DebugToolCall
| SlashCommand::Sandbox
| SlashCommand::Model { .. }
| SlashCommand::Permissions { .. }
| SlashCommand::Clear { .. }
@@ -1287,6 +1297,7 @@ mod tests {
fn parses_supported_slash_commands() {
assert_eq!(SlashCommand::parse("/help"), Some(SlashCommand::Help));
assert_eq!(SlashCommand::parse(" /status "), Some(SlashCommand::Status));
assert_eq!(SlashCommand::parse("/sandbox"), Some(SlashCommand::Sandbox));
assert_eq!(
SlashCommand::parse("/bughunter runtime"),
Some(SlashCommand::Bughunter {
@@ -1416,6 +1427,7 @@ mod tests {
assert!(help.contains("works with --resume SESSION.json"));
assert!(help.contains("/help"));
assert!(help.contains("/status"));
assert!(help.contains("/sandbox"));
assert!(help.contains("/compact"));
assert!(help.contains("/bughunter [scope]"));
assert!(help.contains("/commit"));
@@ -1436,14 +1448,15 @@ mod tests {
assert!(help.contains("/version"));
assert!(help.contains("/export [file]"));
assert!(help.contains("/session [list|switch <session-id>]"));
assert!(help.contains("/sandbox"));
assert!(help.contains(
"/plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
));
assert!(help.contains("aliases: /plugins, /marketplace"));
assert!(help.contains("/agents"));
assert!(help.contains("/skills"));
assert_eq!(slash_command_specs().len(), 25);
assert_eq!(resume_supported_slash_commands().len(), 13);
assert_eq!(slash_command_specs().len(), 27);
assert_eq!(resume_supported_slash_commands().len(), 14);
}
#[test]
@@ -1490,6 +1503,7 @@ mod tests {
let session = Session::new();
assert!(handle_slash_command("/unknown", &session, CompactionConfig::default()).is_none());
assert!(handle_slash_command("/status", &session, CompactionConfig::default()).is_none());
assert!(handle_slash_command("/sandbox", &session, CompactionConfig::default()).is_none());
assert!(
handle_slash_command("/bughunter", &session, CompactionConfig::default()).is_none()
);

View File

@@ -76,6 +76,12 @@ pub use remote::{
RemoteSessionContext, UpstreamProxyBootstrap, UpstreamProxyState, DEFAULT_REMOTE_BASE_URL,
DEFAULT_SESSION_TOKEN_PATH, DEFAULT_SYSTEM_CA_BUNDLE, NO_PROXY_HOSTS, UPSTREAM_PROXY_ENV_KEYS,
};
pub use sandbox::{
build_linux_sandbox_command, detect_container_environment, detect_container_environment_from,
resolve_sandbox_status, resolve_sandbox_status_for_request, ContainerEnvironment,
FilesystemIsolationMode, LinuxSandboxCommand, SandboxConfig, SandboxDetectionInputs,
SandboxRequest, SandboxStatus,
};
pub use session::{ContentBlock, ConversationMessage, MessageRole, Session, SessionError};
pub use usage::{
format_usd, pricing_for_model, ModelPricing, TokenUsage, UsageCostEstimate, UsageTracker,

View File

@@ -34,6 +34,10 @@ use runtime::{
parse_oauth_callback_request_target, save_oauth_credentials, ApiClient, ApiRequest,
AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock,
ConversationMessage, ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig,
parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials,
ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource,
ContentBlock, ConversationMessage, ConversationRuntime, MessageRole,
OAuthAuthorizationRequest, OAuthConfig,
OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, RuntimeError,
Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
};
@@ -656,6 +660,7 @@ struct StatusContext {
memory_file_count: usize,
project_root: Option<PathBuf>,
git_branch: Option<String>,
sandbox_status: runtime::SandboxStatus,
}
#[derive(Debug, Clone, Copy)]
@@ -928,6 +933,18 @@ fn run_resume_command(
)),
})
}
SlashCommand::Sandbox => {
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
let runtime_config = loader.load()?;
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(format_sandbox_report(&resolve_sandbox_status(
runtime_config.sandbox(),
&cwd,
))),
})
}
SlashCommand::Cost => {
let usage = UsageTracker::from_session(session).cumulative_usage();
Ok(ResumeCommandOutcome {
@@ -1237,6 +1254,10 @@ impl LiveCli {
self.run_debug_tool_call()?;
false
}
SlashCommand::Sandbox => {
Self::print_sandbox_status();
false
}
SlashCommand::Compact => {
self.compact()?;
false
@@ -1319,6 +1340,18 @@ impl LiveCli {
);
}
fn print_sandbox_status() {
let cwd = env::current_dir().expect("current dir");
let loader = ConfigLoader::default_for(&cwd);
let runtime_config = loader
.load()
.unwrap_or_else(|_| runtime::RuntimeConfig::empty());
println!(
"{}",
format_sandbox_report(&resolve_sandbox_status(runtime_config.sandbox(), &cwd))
);
}
fn set_model(&mut self, model: Option<String>) -> Result<bool, Box<dyn std::error::Error>> {
let Some(model) = model else {
println!(
@@ -1922,6 +1955,7 @@ fn status_context(
let project_context = ProjectContext::discover_with_git(&cwd, DEFAULT_DATE)?;
let (project_root, git_branch) =
parse_git_status_metadata(project_context.git_status.as_deref());
let sandbox_status = resolve_sandbox_status(runtime_config.sandbox(), &cwd);
Ok(StatusContext {
cwd,
session_path: session_path.map(Path::to_path_buf),
@@ -1930,6 +1964,7 @@ fn status_context(
memory_file_count: project_context.instruction_files.len(),
project_root,
git_branch,
sandbox_status,
})
}
@@ -1982,6 +2017,7 @@ fn format_status_report(
context.discovered_config_files,
context.memory_file_count,
),
format_sandbox_report(&context.sandbox_status),
]
.join(
"
@@ -1990,6 +2026,49 @@ fn format_status_report(
)
}
fn format_sandbox_report(status: &runtime::SandboxStatus) -> String {
format!(
"Sandbox
Enabled {}
Active {}
Supported {}
In container {}
Requested ns {}
Active ns {}
Requested net {}
Active net {}
Filesystem mode {}
Filesystem active {}
Allowed mounts {}
Markers {}
Fallback reason {}",
status.enabled,
status.active,
status.supported,
status.in_container,
status.requested.namespace_restrictions,
status.namespace_active,
status.requested.network_isolation,
status.network_active,
status.filesystem_mode.as_str(),
status.filesystem_active,
if status.allowed_mounts.is_empty() {
"<none>".to_string()
} else {
status.allowed_mounts.join(", ")
},
if status.container_markers.is_empty() {
"<none>".to_string()
} else {
status.container_markers.join(", ")
},
status
.fallback_reason
.clone()
.unwrap_or_else(|| "<none>".to_string()),
)
}
fn render_config_report(section: Option<&str>) -> Result<String, Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
@@ -4249,6 +4328,7 @@ mod tests {
assert!(help.contains("REPL"));
assert!(help.contains("/help"));
assert!(help.contains("/status"));
assert!(help.contains("/sandbox"));
assert!(help.contains("/model [model]"));
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
assert!(help.contains("/clear [--confirm]"));
@@ -4279,8 +4359,8 @@ mod tests {
assert_eq!(
names,
vec![
"help", "status", "compact", "clear", "cost", "config", "memory", "init", "diff",
"version", "export", "agents", "skills",
"help", "status", "sandbox", "compact", "clear", "cost", "config", "memory",
"init", "diff", "version", "export", "agents", "skills",
]
);
}
@@ -4400,6 +4480,7 @@ mod tests {
memory_file_count: 4,
project_root: Some(PathBuf::from("/tmp")),
git_branch: Some("main".to_string()),
sandbox_status: runtime::SandboxStatus::default(),
},
);
assert!(status.contains("Status"));
@@ -4996,3 +5077,17 @@ mod tests {
assert!(!String::from_utf8(out).expect("utf8").contains("step 1"));
}
}
#[cfg(test)]
mod sandbox_report_tests {
use super::format_sandbox_report;
#[test]
fn sandbox_report_renders_expected_fields() {
let report = format_sandbox_report(&runtime::SandboxStatus::default());
assert!(report.contains("Sandbox"));
assert!(report.contains("Enabled"));
assert!(report.contains("Filesystem mode"));
assert!(report.contains("Fallback reason"));
}
}

View File

@@ -225,7 +225,11 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
"timeout": { "type": "integer", "minimum": 1 },
"description": { "type": "string" },
"run_in_background": { "type": "boolean" },
"dangerouslyDisableSandbox": { "type": "boolean" }
"dangerouslyDisableSandbox": { "type": "boolean" },
"namespaceRestrictions": { "type": "boolean" },
"isolateNetwork": { "type": "boolean" },
"filesystemMode": { "type": "string", "enum": ["off", "workspace-only", "allow-list"] },
"allowedMounts": { "type": "array", "items": { "type": "string" } }
},
"required": ["command"],
"additionalProperties": false