Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1e22061
refactor(sandbox): extract run_networking from run_sandbox
rrhubenov May 30, 2026
26e500c
refactor(sandbox): extract run_process and lift netns to run_sandbox
rrhubenov May 30, 2026
228d5a7
chore(workspace): scaffold openshell-supervisor-networking and opensh…
rrhubenov May 30, 2026
b045ef7
refactor(core): lift DenialEvent to openshell-core
rrhubenov May 30, 2026
0550542
refactor(core): lift normalize_path to openshell-core
rrhubenov May 30, 2026
ce0c9b4
refactor(core): lift SandboxPolicy and friends to openshell-core
rrhubenov May 30, 2026
2bade76
refactor(supervisor-process): move child_env from openshell-sandbox
rrhubenov May 30, 2026
1327bba
refactor(supervisor-process): move skills from openshell-sandbox
rrhubenov May 30, 2026
8bbdbab
refactor(supervisor-networking): move mechanistic_mapper from openshe…
rrhubenov May 30, 2026
6da781d
refactor(core): lift procfs to openshell-core
rrhubenov May 30, 2026
509be1c
refactor(supervisor-networking): move identity from openshell-sandbox
rrhubenov May 30, 2026
058bc45
refactor(supervisor-process): move agent-proposals flag from openshel…
rrhubenov May 30, 2026
b90f7ee
refactor(core): lift secrets to openshell-core
rrhubenov May 30, 2026
960bf68
refactor(core): lift provider_credentials to openshell-core
rrhubenov May 30, 2026
b9dc830
style: rustfmt import ordering
rrhubenov May 30, 2026
d1d40f7
refactor(ocsf): move SandboxContext singleton from openshell-sandbox
rrhubenov May 30, 2026
57a97bb
refactor(core): lift grpc_client to openshell-core
rrhubenov May 30, 2026
df881df
refactor(supervisor-networking): move denial_aggregator from openshel…
rrhubenov May 30, 2026
3b70ad8
refactor(supervisor-process): move log_push from openshell-sandbox
rrhubenov May 30, 2026
159efcb
refactor(supervisor-process): move bypass_monitor from openshell-sandbox
rrhubenov May 30, 2026
1dfb8e8
refactor(supervisor-process): move debug_rpc from openshell-sandbox
rrhubenov May 30, 2026
db86d51
refactor(supervisor-process): move supervisor_session from openshell-…
rrhubenov May 30, 2026
8114e8d
refactor(supervisor-process): lift managed_children tracker from open…
rrhubenov May 30, 2026
0c62902
refactor(supervisor-process): move sandbox hardening from openshell-s…
rrhubenov May 31, 2026
e4f042f
refactor(core): lift proposals flag from openshell-supervisor-process
rrhubenov May 31, 2026
0c4127b
refactor(core): lift netns + nft_ruleset from openshell-sandbox
rrhubenov May 31, 2026
d0c5b72
refactor(supervisor-process): move process.rs and ssh.rs from openshe…
rrhubenov May 31, 2026
436f138
refactor(supervisor-networking): move proxy, l7, opa, policy_local fr…
rrhubenov May 31, 2026
bec10f3
refactor(sandbox): hoist policy poll loop and denial aggregator into …
rrhubenov May 31, 2026
c8ad6c9
refactor(supervisor-process): move run_process from openshell-sandbox
rrhubenov May 31, 2026
49e9b27
refactor(supervisor-networking): move bypass_monitor from supervisor-…
rrhubenov Jun 1, 2026
145a4ad
refactor(supervisor-networking): move inference route helpers from op…
rrhubenov Jun 1, 2026
0aefa69
refactor(supervisor-networking): move run_networking from openshell-s…
rrhubenov Jun 1, 2026
dd65374
fix(workspace): align Cargo deps and call sites for split crates
rrhubenov Jun 1, 2026
b1fd663
refactor(supervisor-network): rename openshell-supervisor-networking …
rrhubenov Jun 1, 2026
315d0b2
refactor(supervisor-network): own denial-aggregator flush end-to-end
rrhubenov Jun 1, 2026
0a3bbda
refactor(supervisor-network): own symlink-resolution task
rrhubenov Jun 1, 2026
22f39c1
refactor(supervisor-process): move seccomp install into run_process
rrhubenov Jun 1, 2026
dfd2aa2
refactor(supervisor-process): move check_runtime_pid_limit into run_p…
rrhubenov Jun 1, 2026
748f578
refactor(supervisor-process): move validate_sandbox_user to process c…
rrhubenov Jun 1, 2026
c05febb
refactor(supervisor-process): move prepare_filesystem to process crate
rrhubenov Jun 1, 2026
0830218
refactor(supervisor-process): move startup skill install into run_pro…
rrhubenov Jun 1, 2026
d0335cf
refactor(supervisor-network): own PolicyLocalContext construction
rrhubenov Jun 1, 2026
f525d86
feat(supervisor): add --mode flag to gate network/process leaves
rrhubenov Jun 1, 2026
76a89cf
style(supervisor-process): rustfmt long debug! line
rrhubenov Jun 1, 2026
e065e98
refactor(supervisor-network): pull DenialEvent down from core
rrhubenov Jun 1, 2026
33e00fd
refactor(supervisor-network): pull procfs down from core
rrhubenov Jun 1, 2026
8526c54
style(supervisor-network): run cargo fmt
rrhubenov Jun 2, 2026
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
79 changes: 75 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion crates/openshell-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,33 @@ repository.workspace = true
[dependencies]
prost = { workspace = true }
prost-types = { workspace = true }
tonic = { workspace = true }
tonic = { workspace = true, features = ["channel", "tls"] }
tokio = { workspace = true }
thiserror = { workspace = true }
miette = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
ipnet = "2"
base64 = { workspace = true }

[target.'cfg(target_os = "linux")'.dependencies]
openshell-ocsf = { path = "../openshell-ocsf" }
uuid = { workspace = true }
libc = "0.2"
tempfile = "3"
nix = { workspace = true }

[features]
## Include test-only settings (dummy_bool, dummy_int) in the registry.
## Off by default so production builds have an empty registry.
## Enabled by e2e tests and during development.
dev-settings = []
## Expose proposals::test_helpers (`ProposalsFlagGuard`) to downstream test
## code in other crates. Enabled by openshell-sandbox and
## openshell-supervisor-network dev builds.
test-helpers = []

[build-dependencies]
tonic-build = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ use std::collections::HashMap;
use std::sync::{Arc, OnceLock, RwLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use miette::{IntoDiagnostic, Result, WrapErr};
use openshell_core::proto::{
use crate::proto::{
DenialSummary, GetDraftPolicyRequest, GetInferenceBundleRequest, GetInferenceBundleResponse,
GetSandboxConfigRequest, GetSandboxProviderEnvironmentRequest, IssueSandboxTokenRequest,
PolicyChunk, PolicySource, PolicyStatus, RefreshSandboxTokenRequest, ReportPolicyStatusRequest,
SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse,
UpdateConfigRequest, inference_client::InferenceClient, open_shell_client::OpenShellClient,
};
use openshell_core::sandbox_env;
use crate::sandbox_env;
use miette::{IntoDiagnostic, Result, WrapErr};
use tonic::Status;
use tonic::metadata::AsciiMetadataValue;
use tonic::service::interceptor::InterceptedService;
Expand Down Expand Up @@ -674,7 +674,7 @@ pub struct SettingsPollResult {
pub config_revision: u64,
pub policy_source: PolicySource,
/// Effective settings keyed by name.
pub settings: HashMap<String, openshell_core::proto::EffectiveSetting>,
pub settings: HashMap<String, crate::proto::EffectiveSetting>,
/// When `policy_source` is `Global`, the version of the global policy revision.
pub global_policy_version: u32,
pub provider_env_revision: u64,
Expand Down
7 changes: 7 additions & 0 deletions crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@ pub mod driver_utils;
pub mod error;
pub mod forward;
pub mod gpu;
pub mod grpc_client;
pub mod image;
pub mod inference;
pub mod metadata;
pub mod net;
#[cfg(target_os = "linux")]
pub mod netns;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

netns/nft_ruleset probably don't belong in openshell-core. These modules are privileged Linux supervisor runtime implementation: they create namespaces, manage veths, invoke ip/nsenter/nft, install bypass rules, and emit sandbox OCSF events. That makes core own sandbox enforcement machinery, not just shared types/config.

Can we keep this out of core? The process leaf appears to only need the namespace fd for setns, so one option is for the network/orchestrator side to own the NetworkNamespace RAII handle and pass an Option<i32> fd into run_process. If more sharing is needed, a small supervisor-runtime/netns crate would preserve the no process <-> network dependency rule without expanding openshell-core into privileged runtime code.

pub mod paths;
pub mod policy;
pub mod progress;
pub mod proposals;
pub mod proto;
pub mod provider_credentials;
pub mod sandbox_env;
pub mod secrets;
pub mod settings;
pub mod time;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//! the sandbox to the host. This ensures the sandboxed process can only
//! communicate through the proxy running on the host side of the veth.

mod nft_ruleset;

use miette::{IntoDiagnostic, Result};
use std::net::IpAddr;
use std::os::unix::io::RawFd;
Expand Down Expand Up @@ -71,7 +73,7 @@ impl NetworkNamespace {
.unwrap();

openshell_ocsf::ocsf_emit!(
openshell_ocsf::ConfigStateChangeBuilder::new(crate::ocsf_ctx())
openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Informational)
.status(openshell_ocsf::StatusId::Success)
.state(openshell_ocsf::StateId::Enabled, "creating")
Expand Down Expand Up @@ -165,7 +167,7 @@ impl NetworkNamespace {
};

openshell_ocsf::ocsf_emit!(
openshell_ocsf::ConfigStateChangeBuilder::new(crate::ocsf_ctx())
openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Informational)
.status(openshell_ocsf::StatusId::Success)
.state(openshell_ocsf::StateId::Enabled, "created")
Expand Down Expand Up @@ -262,7 +264,7 @@ impl NetworkNamespace {
pub fn install_bypass_rules(&self, proxy_port: u16) -> Result<()> {
let Some(nft_path) = find_nft() else {
openshell_ocsf::ocsf_emit!(
openshell_ocsf::ConfigStateChangeBuilder::new(crate::ocsf_ctx())
openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Medium)
.status(openshell_ocsf::StatusId::Failure)
.state(openshell_ocsf::StateId::Disabled, "degraded")
Expand All @@ -287,15 +289,12 @@ impl NetworkNamespace {
// before reject rules in the chain so packets are logged before being
// rejected. If the kernel lacks nft_log support, fall back to the
// reject-only ruleset.
let ruleset_with_log = super::nft_ruleset::generate_bypass_ruleset(
&host_ip_str,
proxy_port,
Some(&log_prefix),
);
let ruleset_with_log =
nft_ruleset::generate_bypass_ruleset(&host_ip_str, proxy_port, Some(&log_prefix));

if let Err(e) = run_nft_netns(&self.name, &nft_path, &ruleset_with_log) {
openshell_ocsf::ocsf_emit!(
openshell_ocsf::ConfigStateChangeBuilder::new(crate::ocsf_ctx())
openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Low)
.status(openshell_ocsf::StatusId::Failure)
.state(openshell_ocsf::StateId::Other, "degraded")
Expand All @@ -307,11 +306,11 @@ impl NetworkNamespace {
);

let ruleset_no_log =
super::nft_ruleset::generate_bypass_ruleset(&host_ip_str, proxy_port, None);
nft_ruleset::generate_bypass_ruleset(&host_ip_str, proxy_port, None);

if let Err(e) = run_nft_netns(&self.name, &nft_path, &ruleset_no_log) {
openshell_ocsf::ocsf_emit!(
openshell_ocsf::ConfigStateChangeBuilder::new(crate::ocsf_ctx())
openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Medium)
.status(openshell_ocsf::StatusId::Failure)
.state(openshell_ocsf::StateId::Disabled, "failed")
Expand All @@ -326,7 +325,7 @@ impl NetworkNamespace {
}

openshell_ocsf::ocsf_emit!(
openshell_ocsf::ConfigStateChangeBuilder::new(crate::ocsf_ctx())
openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Informational)
.status(openshell_ocsf::StatusId::Success)
.state(openshell_ocsf::StateId::Enabled, "installed")
Expand Down Expand Up @@ -369,7 +368,7 @@ impl Drop for NetworkNamespace {
}

openshell_ocsf::ocsf_emit!(
openshell_ocsf::ConfigStateChangeBuilder::new(crate::ocsf_ctx())
openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Informational)
.status(openshell_ocsf::StatusId::Success)
.state(openshell_ocsf::StateId::Disabled, "cleaned_up")
Expand Down
46 changes: 44 additions & 2 deletions crates/openshell-core/src/paths.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Centralized XDG config directory resolution and permission helpers.
//! Path utilities: XDG config directory resolution, permission helpers, and
//! lexical path normalization.
//!
//! All `OpenShell` crates should use [`xdg_config_dir`] from this module instead
//! of reimplementing the XDG lookup. The permission helpers ensure that
//! sensitive files (private keys, tokens) and the directories containing them
//! are created with restrictive modes.
//! are created with restrictive modes. [`normalize_path`] performs purely
//! lexical normalization (no filesystem access, no symlink resolution).

use miette::{IntoDiagnostic, Result, WrapErr};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -126,6 +128,33 @@ pub fn is_file_permissions_too_open(path: &Path) -> bool {
std::fs::metadata(path).is_ok_and(|m| m.permissions().mode() & 0o077 != 0)
}

/// Normalize a filesystem path by collapsing redundant separators
/// and removing trailing slashes, without requiring the path to exist on disk.
///
/// This is a lexical normalization only — it does NOT resolve symlinks or
/// check the filesystem. `..` components are preserved verbatim; callers that
/// need to reject parent traversal must validate separately.
pub fn normalize_path(path: &str) -> String {
use std::path::Component;

let p = Path::new(path);
let mut normalized = PathBuf::new();
for component in p.components() {
match component {
Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
#[allow(clippy::path_buf_push_overwrite)]
Component::RootDir => normalized.push("/"),
Component::CurDir => {} // skip "."
Component::ParentDir => {
// Keep ".." — validation will catch it separately
normalized.push("..");
}
Component::Normal(c) => normalized.push(c),
}
}
normalized.to_string_lossy().to_string()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -201,4 +230,17 @@ mod tests {
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(!is_file_permissions_too_open(&file));
}

#[test]
fn normalize_path_collapses_separators() {
assert_eq!(normalize_path("/usr//lib"), "/usr/lib");
assert_eq!(normalize_path("/usr/./lib"), "/usr/lib");
assert_eq!(normalize_path("/tmp/"), "/tmp");
}

#[test]
fn normalize_path_preserves_parent_dir() {
// normalize_path preserves ".." — validation catches it separately
assert_eq!(normalize_path("/usr/../etc"), "/usr/../etc");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

//! Sandbox policy configuration.

use openshell_core::proto::{
use crate::paths::normalize_path;
use crate::proto::{
FilesystemPolicy as ProtoFilesystemPolicy, LandlockPolicy as ProtoLandlockPolicy,
ProcessPolicy as ProtoProcessPolicy, SandboxPolicy as ProtoSandboxPolicy,
};
Expand Down Expand Up @@ -125,12 +126,12 @@ impl From<ProtoFilesystemPolicy> for FilesystemPolicy {
read_only: proto
.read_only
.into_iter()
.map(|p| PathBuf::from(openshell_policy::normalize_path(&p)))
.map(|p| PathBuf::from(normalize_path(&p)))
.collect(),
read_write: proto
.read_write
.into_iter()
.map(|p| PathBuf::from(openshell_policy::normalize_path(&p)))
.map(|p| PathBuf::from(normalize_path(&p)))
.collect(),
include_workdir: proto.include_workdir,
}
Expand Down
Loading
Loading