feat: add Windows support for skills symlinks and secret key permissions

- Add Windows symlink support in skills/mod.rs with fallback chain:
  1. symlink_dir (requires admin/developer mode)
  2. mklink /J junction (works without admin)
  3. copy_dir_recursive fallback
- Add Windows file permissions in security/secrets.rs using icacls
- Add copy_dir_recursive helper function for non-Unix platforms

Fixes #28
This commit is contained in:
argenis de la rosa 2026-02-14 14:07:41 -05:00
parent 5476195a7f
commit 27b7df53da
2 changed files with 71 additions and 4 deletions

View file

@ -181,13 +181,22 @@ impl SecretStore {
fs::write(&self.key_path, hex_encode(&key)) fs::write(&self.key_path, hex_encode(&key))
.context("Failed to write secret key file")?; .context("Failed to write secret key file")?;
// Set restrictive permissions (Unix only) // Set restrictive permissions
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&self.key_path, fs::Permissions::from_mode(0o600)) fs::set_permissions(&self.key_path, fs::Permissions::from_mode(0o600))
.context("Failed to set key file permissions")?; .context("Failed to set key file permissions")?;
} }
#[cfg(windows)]
{
// On Windows, use icacls to restrict permissions to current user only
let _ = std::process::Command::new("icacls")
.arg(&self.key_path)
.args(["/inheritance:r", "/grant:r"])
.arg(format!("{}:F", std::env::var("USERNAME").unwrap_or_default()))
.output();
}
Ok(key) Ok(key)
} }

View file

@ -221,6 +221,23 @@ pub fn init_skills_dir(workspace_dir: &Path) -> Result<()> {
Ok(()) Ok(())
} }
/// Recursively copy a directory (used as fallback when symlinks aren't available)
#[cfg(any(windows, not(unix)))]
fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<()> {
std::fs::create_dir_all(dest)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dest_path)?;
} else {
std::fs::copy(&src_path, &dest_path)?;
}
}
Ok(())
}
/// Handle the `skills` CLI command /// Handle the `skills` CLI command
pub fn handle_command(command: super::SkillCommands, workspace_dir: &Path) -> Result<()> { pub fn handle_command(command: super::SkillCommands, workspace_dir: &Path) -> Result<()> {
match command { match command {
@ -303,10 +320,51 @@ pub fn handle_command(command: super::SkillCommands, workspace_dir: &Path) -> Re
dest.display() dest.display()
); );
} }
#[cfg(not(unix))] #[cfg(windows)]
{ {
// On non-unix, copy the directory // On Windows, try symlink first (requires admin or developer mode),
anyhow::bail!("Symlink not supported on this platform. Copy the skill directory manually."); // fall back to directory junction, then copy
use std::os::windows::fs::symlink_dir;
if symlink_dir(&src, &dest).is_ok() {
println!(
" {} Skill linked: {}",
console::style("").green().bold(),
dest.display()
);
} else {
// Try junction as fallback (works without admin)
let junction_result = std::process::Command::new("cmd")
.args(["/C", "mklink", "/J"])
.arg(&dest)
.arg(&src)
.output();
if junction_result.is_ok() && junction_result.unwrap().status.success() {
println!(
" {} Skill linked (junction): {}",
console::style("").green().bold(),
dest.display()
);
} else {
// Final fallback: copy the directory
copy_dir_recursive(&src, &dest)?;
println!(
" {} Skill copied: {}",
console::style("").green().bold(),
dest.display()
);
}
}
}
#[cfg(not(any(unix, windows)))]
{
// On other platforms, copy the directory
copy_dir_recursive(&src, &dest)?;
println!(
" {} Skill copied: {}",
console::style("").green().bold(),
dest.display()
);
} }
} }