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
20 changes: 20 additions & 0 deletions docs/src/content/docs/configuration/file-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ definitions:
- ~/.aws/credentials
```

## Path Resolution

Relative paths in `definitions.paths`, `definitions.sandbox.*.fs.writable`, and `definitions.sandbox.*.fs.deny` are resolved relative to the **parent directory of the configuration file** that defines them. This ensures consistent behavior regardless of the current working directory when running `runok exec`.

Paths are classified into three types:

| Path type | Example | Resolution |
| -------------- | ---------------- | ---------------------------------------------- |
| Absolute path | `/etc/shadow` | Used as-is |
| Home directory | `~/.ssh/**` | `~` expanded to `$HOME` |
| Relative path | `.env*`, `./tmp` | Joined with the config file's parent directory |

Path resolution happens **before merging**, so each configuration file's relative paths are resolved using its own parent directory. For example:

- Paths in `~/.config/runok/runok.yml` are resolved relative to `~/.config/runok/`
- Paths in `<project>/runok.yml` are resolved relative to `<project>/`
- Paths in a preset loaded via `extends` are resolved relative to the preset file's directory

`.` and `..` components are normalized logically without filesystem access, so glob patterns (e.g. `*.env*`, `**/.git`) are preserved correctly.

## Validation

After merging, runok validates the final configuration:
Expand Down
37 changes: 30 additions & 7 deletions src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,20 @@ impl DefaultConfigLoader {
.find(|path| path.exists())
}

fn find_and_parse(dir: &Path, filenames: &[&str]) -> Result<Option<Config>, ConfigError> {
/// Read, parse, and resolve paths in a config file using its own base_dir.
/// Resolving paths before merging prevents global config paths from being
/// incorrectly re-resolved with the local base_dir.
fn find_parse_and_resolve(
dir: &Path,
filenames: &[&str],
) -> Result<Option<Config>, ConfigError> {
Self::find_config(dir, filenames)
.map(|p| Self::read_and_parse(&p))
.map(|p| {
let mut config = Self::read_and_parse(&p)?;
let base_dir = p.parent().unwrap_or(dir);
super::path_resolver::resolve_config_paths(&mut config, base_dir)?;
Ok(config)
})
.transpose()
}

Expand All @@ -95,19 +106,20 @@ impl DefaultConfigLoader {

impl ConfigLoader for DefaultConfigLoader {
fn load(&self, cwd: &Path) -> Result<Config, ConfigError> {
// Resolve paths in each config file with its own base_dir before merging
let (global, global_local_override) = match &self.global_dir {
Some(dir) => (
Self::find_and_parse(dir, CONFIG_FILENAMES)?,
Self::find_and_parse(dir, LOCAL_OVERRIDE_FILENAMES)?,
Self::find_parse_and_resolve(dir, CONFIG_FILENAMES)?,
Self::find_parse_and_resolve(dir, LOCAL_OVERRIDE_FILENAMES)?,
),
None => (None, None),
};

let project_dir = self.find_project_dir(cwd);
let (local, local_override) = match &project_dir {
Some(dir) => (
Self::find_and_parse(dir, CONFIG_FILENAMES)?,
Self::find_and_parse(dir, LOCAL_OVERRIDE_FILENAMES)?,
Self::find_parse_and_resolve(dir, CONFIG_FILENAMES)?,
Self::find_parse_and_resolve(dir, LOCAL_OVERRIDE_FILENAMES)?,
),
None => (None, None),
};
Expand Down Expand Up @@ -424,7 +436,18 @@ mod tests {
let defs = config.definitions.unwrap();

let paths = defs.paths.unwrap();
assert_eq!(paths["sensitive"], vec![".env*", "~/.ssh/**"]);
// .env* is resolved relative to the global config's base_dir
let global_env = format!("{}/.env*", env.global_dir.display());
// ~/ is expanded using the HOME environment variable
let sensitive = &paths["sensitive"];
assert_eq!(sensitive[0], global_env);
assert!(
!sensitive[1].starts_with("~/"),
"tilde should be expanded: {}",
sensitive[1]
);
assert!(sensitive[1].ends_with("/.ssh/**"));
// Absolute paths are kept as-is
assert_eq!(paths["logs"], vec!["/var/log/**"]);

assert_eq!(defs.wrappers.unwrap(), vec!["sudo <cmd>", "bash -c <cmd>"]);
Expand Down
2 changes: 2 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ mod error;
pub(crate) mod git_client;
mod loader;
mod model;
pub mod path_resolver;
mod preset;
pub(crate) mod preset_remote;

pub use error::*;
pub use loader::*;
pub use model::*;
pub use path_resolver::{PathResolveError, expand_tilde, resolve_config_paths, resolve_path};
pub use preset::*;
Loading
Loading