Skip to content

Commit 6e69360

Browse files
authored
refactor(runtime): extract portable driver and isolation foundations (#4106)
* refactor(runtime): decouple supervisor access and boundary audit validation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(runtime): keep supervisor process access boundary driven Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(cli): support environment values from caller variables Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(runtime): extract driver-independent MXC foundations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(runtime): defer UI policy support to MXC integration Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(runtime): integrate current policy and SSH launch contracts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(runtime): reconcile fixtures with updated main Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(runtime): repair stacked branch CI contracts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(runtime): update boundary fixtures for current contracts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(runtime): clarify scoped guards for launch inputs and delete Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(runtime): validate foreground attachment intent before launch Signed-off-by: Drew Newberry <anewberry@nvidia.com> --------- Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 39b13d1 commit 6e69360

47 files changed

Lines changed: 2125 additions & 573 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/build-openshell-mxc-windows/SKILL.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ Windows MSVC for the supported deliverables:
2121
It intentionally does not make Windows a Docker, Kubernetes, Podman, or VM
2222
runtime host.
2323

24+
The supervisor and supervisor-process libraries participate in native Windows
25+
checks and tests. Their gateway session, boundary attachment, and TCP readiness
26+
are portable; only the optional Unix SSH/readiness socket adapters are gated.
27+
This is compile and control-plane coverage, not Windows isolation qualification.
28+
Preserve readiness gating on authenticated gateway acceptance and reconnection.
29+
The supervisor process-access multiplexer consumes boundary-provided streams;
30+
do not restore local PID, PTY, or process-group operations there. Native process
31+
mechanics belong to the sandbox or isolation backend. Keep the optional Unix
32+
SSH access adapter separate from portable session orchestration.
33+
Shared Sandbox Protocol audit validation defaults to strict Linux evidence;
34+
concrete platform validators must be selected by the implementing backend.
35+
2436
## Current Repository Shape
2537

2638
The Windows build lane is implemented by these tracked files:

‎.agents/skills/build-openshell-mxc-windows/reference.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,6 @@ top-level workspace targets for check/test:
111111
--exclude openshell-driver-vault
112112
--exclude openshell-driver-vm
113113
--exclude openshell-sandbox
114-
--exclude openshell-supervisor
115-
--exclude openshell-supervisor-process
116114
--exclude openshell-vfio
117115
```
118116

@@ -124,6 +122,10 @@ egress proxy. The Kubernetes Secrets and Vault libraries still compile as
124122
gateway dependencies; only their standalone Unix-socket binaries and
125123
package-level tests are excluded as top-level targets.
126124

125+
The supervisor and supervisor-process packages now participate as top-level
126+
native check/test targets. Their portable session, attachment, and TCP readiness
127+
coverage does not enable a Windows isolation runtime.
128+
127129
## Common Errors
128130

129131
### Unix imports leak into Windows builds

‎Cargo.lock‎

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/openshell-cli/src/commands/common.rs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,47 @@ pub fn parse_env_pairs(items: &[String]) -> Result<HashMap<String, String>> {
891891
Ok(map)
892892
}
893893

894+
/// Resolve `--env-from KEY[=ENVVAR]` values from the CLI process environment.
895+
///
896+
/// This keeps environment values out of process arguments while preserving the
897+
/// same sandbox environment validation as `--env KEY=VALUE`.
898+
pub fn parse_env_from_pairs(items: &[String]) -> Result<HashMap<String, String>> {
899+
let mut map = HashMap::new();
900+
901+
for item in items {
902+
let (key, env_name) = match item.split_once('=') {
903+
Some((key, env_name)) => (key.trim(), env_name.trim()),
904+
None => (item.trim(), item.trim()),
905+
};
906+
if !is_valid_env_name(key) {
907+
return Err(miette::miette!(
908+
"--env-from key must match [A-Za-z_][A-Za-z0-9_]*; got '{key}'"
909+
));
910+
}
911+
if key.starts_with("OPENSHELL_") {
912+
return Err(miette::miette!(
913+
"--env-from keys starting with OPENSHELL_ are reserved; got '{key}'"
914+
));
915+
}
916+
if !is_valid_env_name(env_name) {
917+
return Err(miette::miette!(
918+
"--env-from source must match [A-Za-z_][A-Za-z0-9_]*; got '{env_name}'"
919+
));
920+
}
921+
if map.contains_key(key) {
922+
return Err(miette::miette!("duplicate --env-from sandbox key '{key}'"));
923+
}
924+
let value = std::env::var(env_name).map_err(|_| {
925+
miette::miette!(
926+
"--env-from source environment variable '{env_name}' is not set or is not valid Unicode"
927+
)
928+
})?;
929+
map.insert(key.to_string(), value);
930+
}
931+
932+
Ok(map)
933+
}
934+
894935
/// Resolve `--secret-material-env KEY[=ENVVAR]` values from the CLI process
895936
/// environment (`ENVVAR` defaults to `KEY`) so secrets never transit argv.
896937
pub fn parse_secret_material_env_pairs(items: &[String]) -> Result<HashMap<String, String>> {

‎crates/openshell-cli/src/main.rs‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,7 +1452,7 @@ enum SandboxCommands {
14521452
name: Option<String>,
14531453

14541454
/// Create the sandbox from a named sandbox template.
1455-
#[arg(long, conflicts_with_all = ["from", "gpu", "cpu", "memory", "driver_config_json", "envs"])]
1455+
#[arg(long, conflicts_with_all = ["from", "gpu", "cpu", "memory", "driver_config_json", "envs", "env_from"])]
14561456
template: Option<String>,
14571457

14581458
/// Sandbox source: a rootfs tar archive (`.tar`, `.tar.gz`, or `.tgz`)
@@ -1597,6 +1597,13 @@ enum SandboxCommands {
15971597
#[arg(long = "env", value_name = "KEY=VALUE")]
15981598
envs: Vec<String>,
15991599

1600+
/// Set a sandbox environment variable from the CLI process environment.
1601+
///
1602+
/// Format: `KEY[=ENVVAR]`. When `ENVVAR` is omitted, `KEY` is used.
1603+
/// The value does not appear in the CLI process arguments. Repeatable.
1604+
#[arg(long = "env-from", value_name = "KEY[=ENVVAR]")]
1605+
env_from: Vec<String>,
1606+
16001607
/// Suppress warnings when --env values look like credentials.
16011608
#[arg(long = "no-credential-warnings")]
16021609
no_credential_warnings: bool,
@@ -3372,6 +3379,7 @@ async fn run_async() -> Result<()> {
33723379
no_auto_providers,
33733380
labels,
33743381
envs,
3382+
env_from,
33753383
no_credential_warnings,
33763384
approval_mode,
33773385
output,
@@ -3409,7 +3417,12 @@ async fn run_async() -> Result<()> {
34093417
}
34103418

34113419
// Parse --env flags into a HashMap<String, String>.
3412-
let env_map = run::parse_env_pairs(&envs)?;
3420+
let mut env_map = run::parse_env_pairs(&envs)?;
3421+
for (key, value) in run::parse_env_from_pairs(&env_from)? {
3422+
if env_map.insert(key.clone(), value).is_some() {
3423+
return Err(miette::miette!("duplicate environment key '{key}'"));
3424+
}
3425+
}
34133426

34143427
// Parse --upload specs into [(local_path, sandbox_path, git_ignore)].
34153428
let upload_specs: Vec<(String, Option<String>, bool)> = upload

‎crates/openshell-cli/src/run.rs‎

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
//! CLI command implementations.
55
66
pub use crate::commands::common::{
7-
PolicyGetView, parse_credential_expiry_cli_value, parse_env_pairs, parse_key_value_pairs,
8-
parse_secret_material_env_pairs, warn_credential_env_vars,
7+
PolicyGetView, parse_credential_expiry_cli_value, parse_env_from_pairs, parse_env_pairs,
8+
parse_key_value_pairs, parse_secret_material_env_pairs, warn_credential_env_vars,
99
};
1010
use crate::commands::common::{
1111
ProvisioningDisplay, ProvisioningStep, confirm_global_setting_delete,
@@ -6911,12 +6911,12 @@ mod tests {
69116911
ForwardTcpConnectionError, PolicyGetView, ProvisioningStep, build_sandbox_resource_limits,
69126912
format_endpoint, format_log_line, forward_requires_session_token, git_sync_files,
69136913
has_main_process_result, parse_cli_setting_value, parse_credential_expiry_cli_value,
6914-
parse_driver_config_json, parse_secret_material_env_pairs, policy_revision_list_json,
6915-
policy_revision_to_json, proto_execution_timeout, provisioning_timeout_message,
6916-
ready_false_condition_message, relay_local_socket, resolve_from,
6917-
rootfs_tar_sources_supported_for_gateway, sandbox_should_persist, sandbox_upload_plan,
6918-
service_endpoint_to_json, service_status_error, service_url_for_gateway,
6919-
workspace_member_to_json,
6914+
parse_driver_config_json, parse_env_from_pairs, parse_secret_material_env_pairs,
6915+
policy_revision_list_json, policy_revision_to_json, proto_execution_timeout,
6916+
provisioning_timeout_message, ready_false_condition_message, relay_local_socket,
6917+
resolve_from, rootfs_tar_sources_supported_for_gateway, sandbox_should_persist,
6918+
sandbox_upload_plan, service_endpoint_to_json, service_status_error,
6919+
service_url_for_gateway, workspace_member_to_json,
69206920
};
69216921
use openshell_core::proto::TcpForwardFrame;
69226922

@@ -7181,6 +7181,57 @@ mod tests {
71817181
});
71827182
}
71837183

7184+
#[test]
7185+
fn parse_env_from_pairs_reads_named_and_same_name_environment_variables() {
7186+
Environment::new()
7187+
.set("NAV_PARSE_ENV_FROM_NAMED", "named-value")
7188+
.set("NAV_PARSE_ENV_FROM_SAME", "same-name-value")
7189+
.run(|| {
7190+
let parsed = parse_env_from_pairs(&[
7191+
"SANDBOX_NAMED=NAV_PARSE_ENV_FROM_NAMED".to_string(),
7192+
"NAV_PARSE_ENV_FROM_SAME".to_string(),
7193+
])
7194+
.expect("parse");
7195+
assert_eq!(
7196+
parsed.get("SANDBOX_NAMED"),
7197+
Some(&"named-value".to_string())
7198+
);
7199+
assert_eq!(
7200+
parsed.get("NAV_PARSE_ENV_FROM_SAME"),
7201+
Some(&"same-name-value".to_string())
7202+
);
7203+
});
7204+
}
7205+
7206+
#[test]
7207+
fn parse_env_from_pairs_rejects_missing_invalid_reserved_and_duplicate_keys() {
7208+
Environment::new()
7209+
.remove("NAV_PARSE_ENV_FROM_MISSING")
7210+
.set("NAV_PARSE_ENV_FROM_PRESENT", "value")
7211+
.run(|| {
7212+
for (input, expected) in [
7213+
("TARGET=NAV_PARSE_ENV_FROM_MISSING", "is not set"),
7214+
("1BAD=NAV_PARSE_ENV_FROM_PRESENT", "key must match"),
7215+
("TARGET=BAD-NAME", "source must match"),
7216+
("OPENSHELL_RESERVED=NAV_PARSE_ENV_FROM_PRESENT", "reserved"),
7217+
] {
7218+
let error =
7219+
parse_env_from_pairs(&[input.to_string()]).expect_err("must reject");
7220+
assert!(
7221+
error.to_string().contains(expected),
7222+
"unexpected error: {error}"
7223+
);
7224+
}
7225+
7226+
let error = parse_env_from_pairs(&[
7227+
"TARGET=NAV_PARSE_ENV_FROM_PRESENT".to_string(),
7228+
"TARGET=NAV_PARSE_ENV_FROM_PRESENT".to_string(),
7229+
])
7230+
.expect_err("duplicate must reject");
7231+
assert!(error.to_string().contains("duplicate --env-from"));
7232+
});
7233+
}
7234+
71847235
#[test]
71857236
fn parse_secret_material_env_pairs_reads_value_from_named_environment_variable() {
71867237
Environment::new()

‎crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3333,6 +3333,16 @@ async fn run_cli_sandbox_create_with_xdg(
33333333
xdg_dir: &TempDir,
33343334
name: &str,
33353335
extra_args: &[&str],
3336+
) -> std::process::Output {
3337+
run_cli_sandbox_create_with_xdg_and_env(server, xdg_dir, name, extra_args, &[]).await
3338+
}
3339+
3340+
async fn run_cli_sandbox_create_with_xdg_and_env(
3341+
server: &TestServer,
3342+
xdg_dir: &TempDir,
3343+
name: &str,
3344+
extra_args: &[&str],
3345+
environment: &[(&str, &str)],
33363346
) -> std::process::Output {
33373347
let mut cmd = tokio::process::Command::new(env!("CARGO_BIN_EXE_openshell"));
33383348
for (key, _) in std::env::vars().filter(|(k, _)| k.starts_with("OPENSHELL_")) {
@@ -3351,6 +3361,7 @@ async fn run_cli_sandbox_create_with_xdg(
33513361
"--no-auto-providers",
33523362
])
33533363
.args(extra_args)
3364+
.envs(environment.iter().copied())
33543365
.env("XDG_CONFIG_HOME", xdg_dir.path())
33553366
.env("HOME", xdg_dir.path())
33563367
.env("OPENSHELL_PROVISION_TIMEOUT", "5")
@@ -3488,6 +3499,47 @@ async fn sandbox_create_upload_warns_and_reaches_ssh_outside_git_repository() {
34883499
.await;
34893500
}
34903501

3502+
async fn run_cli_sandbox_create_with_env(
3503+
server: &TestServer,
3504+
name: &str,
3505+
extra_args: &[&str],
3506+
environment: &[(&str, &str)],
3507+
) -> std::process::Output {
3508+
let xdg_dir = helpers::tempdir().unwrap();
3509+
prepare_cli_xdg(server, &xdg_dir);
3510+
run_cli_sandbox_create_with_xdg_and_env(server, &xdg_dir, name, extra_args, environment).await
3511+
}
3512+
3513+
#[tokio::test]
3514+
async fn sandbox_create_env_from_reaches_request() {
3515+
let server = run_server().await;
3516+
let value = "qualification-value-not-in-argv";
3517+
3518+
let output = run_cli_sandbox_create_with_env(
3519+
&server,
3520+
"env-from-test",
3521+
&["--env-from", "SANDBOX_VALUE=HOST_VALUE", "--output=json"],
3522+
&[("HOST_VALUE", value)],
3523+
)
3524+
.await;
3525+
assert!(
3526+
output.status.success(),
3527+
"sandbox create failed: {}",
3528+
String::from_utf8_lossy(&output.stderr)
3529+
);
3530+
3531+
let requests = create_requests(&server).await;
3532+
let environment = &requests[0]
3533+
.spec
3534+
.as_ref()
3535+
.expect("spec should be present")
3536+
.environment;
3537+
assert_eq!(
3538+
environment.get("SANDBOX_VALUE").map(String::as_str),
3539+
Some(value)
3540+
);
3541+
}
3542+
34913543
async fn run_cli_sandbox_template_create(
34923544
server: &TestServer,
34933545
name: &str,

‎crates/openshell-core/src/paths.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ pub fn openshell_state_dir() -> Result<PathBuf> {
5757
Ok(xdg_state_dir()?.join("openshell"))
5858
}
5959

60+
/// Platform default for gateway audit files. Sink configuration and writing
61+
/// remain portable; only the directory convention depends on the host.
62+
pub fn gateway_audit_log_dir() -> PathBuf {
63+
#[cfg(target_os = "windows")]
64+
{
65+
if let Some(directory) = std::env::var_os("ProgramData") {
66+
return PathBuf::from(directory).join("OpenShell").join("logs");
67+
}
68+
std::env::temp_dir().join("openshell").join("logs")
69+
}
70+
#[cfg(not(target_os = "windows"))]
71+
{
72+
openshell_state_dir()
73+
.unwrap_or_else(|_| std::env::temp_dir().join("openshell"))
74+
.join("logs")
75+
}
76+
}
77+
6078
/// Resolve the XDG data base directory.
6179
///
6280
/// Returns `$XDG_DATA_HOME` if set, otherwise `$HOME/.local/share`.

‎crates/openshell-core/src/provider_credentials.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,18 @@ impl ProviderCredentialState {
442442
.requires_stable_placeholders
443443
}
444444

445+
/// Whether this snapshot contains endpoint-bound material that must be
446+
/// resolved by a network proxy rather than exposed to the child process.
447+
#[must_use]
448+
pub fn requires_proxy_resolution(&self) -> bool {
449+
let inner = self
450+
.inner
451+
.read()
452+
.expect("provider credential state poisoned");
453+
!inner.static_credential_bindings.is_empty()
454+
|| !inner.current.dynamic_credentials.is_empty()
455+
}
456+
445457
/// Remove a key from the credential snapshot's child env.
446458
///
447459
/// Used when a sandbox-side service (e.g., metadata server) fails to start

‎crates/openshell-isolation-interface/src/contract.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ pub trait BoundaryLoopbackConnector: Send + Sync {
857857
/// that owns a connection or one of its executable ancestors. A missing digest
858858
/// is `None`, never an empty value; binary-scoped policy cannot authorize an
859859
/// executable whose digest is unavailable.
860-
#[derive(Debug, Clone, PartialEq, Eq)]
860+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
861861
pub struct ExecutableIdentity {
862862
/// Absolute executable path in the workload filesystem namespace.
863863
pub path: PathBuf,
@@ -871,7 +871,7 @@ pub struct ExecutableIdentity {
871871
///
872872
/// How a backend resolves identity is private to that backend; the shape and
873873
/// fail-closed semantics do not change.
874-
#[derive(Debug, Clone)]
874+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
875875
pub struct BinaryIdentity {
876876
/// Executable that owns the accepted connection.
877877
pub executable: ExecutableIdentity,

0 commit comments

Comments
 (0)