Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/shellsmith-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ clap = { version = "4", features = ["derive", "env"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tempfile = "3.20"
toml = "0.8"

[target.'cfg(unix)'.dependencies]
libc = "0.2"
24 changes: 23 additions & 1 deletion apps/shellsmith-mcp/src/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub struct Server {
impl Server {
pub fn new(cli: PathBuf, allowed_roots: Vec<PathBuf>) -> Self {
Self {
cli,
cli: resolve_cli_path(cli),
allowed_roots,
out: Arc::new(Mutex::new(BufWriter::new(std::io::stdout()))),
jobs: Mutex::new(BTreeMap::new()),
Expand Down Expand Up @@ -297,6 +297,11 @@ impl Server {
set_cli_working_dir(&mut process, &self.cli);
apply_sanitized_env(&mut process, env);
add_temp_env(&mut process, sync_temp.path());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
process.process_group(0);
}
let mut child = process
.spawn()
.with_context(|| format!("启动 Shellsmith CLI 失败: {}", self.cli.display()))?;
Expand Down Expand Up @@ -758,6 +763,23 @@ fn set_cli_working_dir(process: &mut Command, cli: &Path) {
}
}

fn resolve_cli_path(cli: PathBuf) -> PathBuf {
if cli.is_absolute() {
return cli;
}
let has_parent = cli
.parent()
.is_some_and(|parent| !parent.as_os_str().is_empty());
if !has_parent {
return cli;
}
let Ok(cwd) = std::env::current_dir() else {
return cli;
};
let resolved = cwd.join(cli);
resolved.canonicalize().unwrap_or(resolved)
}

fn add_temp_env(process: &mut Command, root: &Path) {
let root = root.to_string_lossy().into_owned();
process
Expand Down
73 changes: 71 additions & 2 deletions apps/shellsmith-mcp/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ struct RuntimeUiInput {
ui_packer: Option<String>,
}

#[derive(Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default)]
struct SignInput {
input: Option<String>,
output: Option<String>,
Expand Down Expand Up @@ -899,7 +899,10 @@ fn protect_apk(
env: env.clone(),
label: "加固 APK".into(),
}];
if let Some(sign) = input.sign {
if let Some(mut sign) = input.sign {
if sign.apksigner.is_none() {
sign.apksigner = input.apksigner.clone();
}
let sign_args = sign_command_args(
server,
&sign,
Expand Down Expand Up @@ -1263,6 +1266,7 @@ fn protect_ios(
require_confirm(input.confirm)?;
}
let config = server.resolve_input(&input.ios_config, "iOS 配置")?;
validate_ios_config_paths(server, &config)?;
let output = server.resolve_output(&input.output, "iOS 输出目录")?;
let mut cli_args = vec![
"protect-ios".to_string(),
Expand Down Expand Up @@ -1700,6 +1704,71 @@ fn push_path_opt(
Ok(())
}

fn resolve_config_path(base: &Path, value: &str) -> PathBuf {
let path = PathBuf::from(value);
if path.is_absolute() {
path
} else {
base.join(path)
}
}

fn ensure_config_path_allowed(server: &Arc<Server>, path: &Path, label: &str) -> Result<()> {
if path.exists() {
server.resolve_input(&path.to_string_lossy(), label)?;
} else {
server.resolve_output(&path.to_string_lossy(), label)?;
}
Ok(())
}

/// iOS 配置中的工程、entrypoint 和 Swift Confidential 路径也必须受允许目录约束。
fn validate_ios_config_paths(server: &Arc<Server>, config: &Path) -> Result<()> {
let content = fs::read_to_string(config)
.with_context(|| format!("读取 iOS 配置失败:{}", config.display()))?;
let value: toml::Value = toml::from_str(&content).context("解析 iOS 配置失败")?;
let base = config.parent().unwrap_or(Path::new("."));

let project = value
.get("project")
.and_then(|project| project.get("path"))
.and_then(toml::Value::as_str)
.context("iOS 配置缺少 project.path")?;
let project_path = resolve_config_path(base, project);
server.resolve_input(&project_path.to_string_lossy(), "iOS 工程")?;

if let Some(entrypoint) = value
.get("project")
.and_then(|project| project.get("entrypoint"))
.and_then(toml::Value::as_str)
{
let project_root = project_path.parent().unwrap_or(base);
ensure_config_path_allowed(
server,
&resolve_config_path(project_root, entrypoint),
"iOS entrypoint",
)?;
}

if let Some(confidential) = value.get("confidential") {
let enabled = confidential
.get("enabled")
.and_then(toml::Value::as_bool)
.unwrap_or(true);
if enabled {
if let Some(config_path) = confidential.get("config").and_then(toml::Value::as_str) {
ensure_config_path_allowed(
server,
&resolve_config_path(base, config_path),
"Swift Confidential 配置",
)?;
}
}
}

Ok(())
}

fn push_opt(args: &mut Vec<String>, flag: &str, value: Option<&str>) {
if let Some(value) = value {
if !value.is_empty() {
Expand Down
Loading
Loading