diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..acef965 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing to kraken-cli + +Thank you for your interest in contributing. This document explains how to submit changes and report issues. + +## How to Contribute + +Open PRs and issues here on GitHub. The maintainers review submissions and merge accepted changes into future releases. Response times vary. Not every PR will be accepted, and some may require changes before they can be merged. + +## Issues and Feature Requests + +Use [GitHub Issues](https://github.com/krakenfx/kraken-cli/issues) for bug reports and feature requests. + +**Bug reports.** Include: + +- The command you ran (redact any API keys or secrets). +- The output you received (use `-o json` for structured output). +- What you expected instead. +- Your OS and architecture (`uname -a`). +- The CLI version (`kraken --version`). + +**Feature requests.** Describe what you want the CLI to do and why. Include a usage example if possible. Prefix the title with `Feature request:` so it is easy to find. + +## Submitting Changes + +1. Fork the repository. +2. Create a branch from `main`. +3. Make your changes. +4. Run `cargo build` and `cargo test` locally. +5. Open a pull request against `main`. + +### PR Guidelines + +- Keep PRs focused. One logical change per PR. +- Write clear commit messages. Describe what changed and why. +- Add or update tests for new behavior. +- Do not include unrelated formatting or refactoring changes. +- Enable "Allow edits from maintainers" on your PR so the team can make small adjustments before merging. + +### What Makes a Good PR + +- Bug fixes with a reproducer or test case. +- New commands or flags that extend existing command groups. +- Documentation improvements. +- Test coverage for untested code paths. + +### What Will Likely Be Declined + +- Large architectural changes without prior discussion. Open an issue first. +- Changes that break backward compatibility without strong justification. +- Features that require exchange infrastructure changes outside the CLI. + +## Code Style + +- Follow existing patterns in the codebase. +- Run `cargo clippy -- -D warnings` before submitting. +- No nightly-only features. The crate must build on stable Rust. + +## License + +By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE). + +## Security + +If you discover a security vulnerability, do **not** open a public issue. Follow the responsible disclosure process described in [DISCLAIMER.md](DISCLAIMER.md). diff --git a/README.md b/README.md index f852e56..f896715 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Try these with your AI agent: - [Commands](#commands) - [API Keys & Configuration](#api-keys--configuration) - [Verifying Binaries](#verifying-binaries) +- [Contributing](#contributing) - [License & Disclaimer](#license--disclaimer) ## Installation @@ -732,6 +733,10 @@ cargo test # run tests cargo clippy -- -D warnings # lint ``` +## Contributing + +Bug reports, feature requests, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + ## License & Disclaimer MIT. See [LICENSE](LICENSE). diff --git a/src/config.rs b/src/config.rs index 090930a..f65b67d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -147,15 +147,18 @@ pub(crate) fn load() -> Result { Ok(cfg) } -/// Save configuration to disk with 0600 permissions. +/// Save configuration to disk atomically with 0600 permissions. +/// +/// On Unix the file is written to a temporary path with mode 0600 from +/// creation, then renamed into place. This eliminates the window where +/// credentials could be read by other local users. pub(crate) fn save(cfg: &KrakenConfig) -> Result<()> { let dir = config_dir()?; fs::create_dir_all(&dir)?; let path = dir.join("config.toml"); let contents = toml::to_string_pretty(cfg) .map_err(|e| KrakenError::Config(format!("TOML serialize error: {e}")))?; - fs::write(&path, &contents)?; - set_permissions_0600(&path)?; + atomic_write_restricted(&path, contents.as_bytes())?; Ok(()) } @@ -352,15 +355,31 @@ pub(crate) fn mask_string(s: &str) -> String { } #[cfg(unix)] -fn set_permissions_0600(path: &Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - let perms = fs::Permissions::from_mode(0o600); - fs::set_permissions(path, perms)?; +fn atomic_write_restricted(path: &Path, data: &[u8]) -> Result<()> { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + let dir = path + .parent() + .ok_or_else(|| KrakenError::Config("config path has no parent directory".into()))?; + let tmp_path = dir.join(".config.tmp"); + + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&tmp_path)?; + file.write_all(data)?; + file.sync_all()?; + + fs::rename(&tmp_path, path)?; Ok(()) } #[cfg(not(unix))] -fn set_permissions_0600(_path: &Path) -> Result<()> { +fn atomic_write_restricted(path: &Path, data: &[u8]) -> Result<()> { + fs::write(path, data)?; Ok(()) } diff --git a/src/telemetry.rs b/src/telemetry.rs index 51c9683..5fe0395 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -164,8 +164,7 @@ fn write_instance_id(path: &Path, value: &str) -> std::io::Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } - fs::write(path, value)?; - set_permissions_0600(path)?; + atomic_write_restricted(path, value.as_bytes())?; Ok(()) } @@ -248,13 +247,29 @@ fn is_uuid_like(value: &str) -> bool { } #[cfg(unix)] -fn set_permissions_0600(path: &Path) -> std::io::Result<()> { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions(path, fs::Permissions::from_mode(0o600)) +fn atomic_write_restricted(path: &Path, data: &[u8]) -> std::io::Result<()> { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + let dir = path.parent().unwrap_or(path); + let tmp_path = dir.join(".instance_id.tmp"); + + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&tmp_path)?; + file.write_all(data)?; + file.sync_all()?; + + fs::rename(&tmp_path, path)?; + Ok(()) } #[cfg(not(unix))] -fn set_permissions_0600(_path: &Path) -> std::io::Result<()> { +fn atomic_write_restricted(path: &Path, data: &[u8]) -> std::io::Result<()> { + fs::write(path, data)?; Ok(()) }