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
64 changes: 64 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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).
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
35 changes: 27 additions & 8 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,18 @@ pub(crate) fn load() -> Result<KrakenConfig> {
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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down
27 changes: 21 additions & 6 deletions src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down
Loading