Skip to main content

hjkl_engine/
policy.rs

1//! Process-global execution policy for non-TUI / RPC modes.
2//!
3//! Interactive TUI keeps full vim parity (shell-out, unrestricted paths). The
4//! non-TUI entry points (`--embed`, `--nvim-api`, `--headless`) may take
5//! commands from a remote or automated caller that is not the local user, so
6//! they can tighten this policy at startup. Mirrors the one-shot global pattern
7//! used by the clipboard-disable path (`host::disable_clipboard_for_rpc`).
8//!
9//! Flags are set once, before any editor is built, and only ever flip from the
10//! permissive default to the restrictive state — never back — so a plain
11//! `Relaxed` atomic is sufficient.
12
13use std::sync::atomic::{AtomicBool, Ordering};
14
15/// When `true`, shell-out commands (`:!cmd`, `:[range]!cmd`, `:r !cmd`, and the
16/// engine range filter) are refused. Default `false` (allowed, as in vim).
17static SHELL_DISABLED: AtomicBool = AtomicBool::new(false);
18
19/// Refuse shell-out for the rest of the process. Call once at RPC/headless
20/// startup, before building any editor.
21pub fn disable_shell() {
22    SHELL_DISABLED.store(true, Ordering::Relaxed);
23}
24
25/// True if shell-out has been disabled for this process.
26pub fn shell_disabled() -> bool {
27    SHELL_DISABLED.load(Ordering::Relaxed)
28}
29
30/// When `true`, file I/O paths are confined to the current working directory
31/// subtree: absolute paths and paths containing a `..` component are refused.
32/// Default `false` (unrestricted, as in vim). The RPC entry points enable this
33/// so a remote/automated caller cannot read or write arbitrary filesystem
34/// locations via `:w`/`:e`/`:r`.
35static FS_RESTRICTED: AtomicBool = AtomicBool::new(false);
36
37/// Confine file I/O to the working-directory subtree for the rest of the
38/// process. Call once at RPC startup, before building any editor.
39pub fn restrict_fs() {
40    FS_RESTRICTED.store(true, Ordering::Relaxed);
41}
42
43/// True if filesystem access has been confined for this process.
44pub fn fs_restricted() -> bool {
45    FS_RESTRICTED.load(Ordering::Relaxed)
46}
47
48/// True if `path` would escape a confined working directory: it is absolute, or
49/// contains a parent-dir (`..`), root, or prefix component.
50pub fn path_escapes(path: &std::path::Path) -> bool {
51    use std::path::Component;
52    path.components().any(|c| {
53        matches!(
54            c,
55            Component::ParentDir | Component::RootDir | Component::Prefix(_)
56        )
57    })
58}
59
60/// `Err` with a uniform message when `path` is refused under a confined
61/// filesystem policy; `Ok(())` when access is allowed (policy off, or the path
62/// stays within the working directory).
63pub fn check_fs_path(path: &std::path::Path) -> Result<(), String> {
64    if fs_restricted() && path_escapes(path) {
65        return Err(format!(
66            "path {} is outside the working directory (blocked in RPC mode)",
67            path.display()
68        ));
69    }
70    Ok(())
71}