Skip to content
Open
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
10 changes: 10 additions & 0 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,14 @@ enum SandboxCommands {
#[arg(long = "env", value_name = "KEY=VALUE")]
envs: Vec<String>,

/// Run in permissive mode: log but do not enforce network policy denials.
/// Connections that would be blocked are allowed through and logged as
/// draft policy proposals. Filesystem (Landlock) restrictions are skipped.
/// Seccomp and process-identity controls remain enforced.
/// Use with --policy to provide protocol hints for richer L7 learning.
#[arg(long)]
permissive: bool,

/// Approval mode for agent-authored policy proposals.
///
/// `manual` (default): every proposal lands in the draft inbox for
Expand Down Expand Up @@ -2565,6 +2573,7 @@ async fn main() -> Result<()> {
no_auto_providers,
labels,
envs,
permissive,
approval_mode,
command,
} => {
Expand Down Expand Up @@ -2651,6 +2660,7 @@ async fn main() -> Result<()> {
&labels_map,
&env_map,
&approval_mode,
permissive,
&tls,
))
.await?;
Expand Down
23 changes: 23 additions & 0 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,7 @@ pub async fn sandbox_create(
labels: &HashMap<String, String>,
environment: &HashMap<String, String>,
approval_mode: &str,
permissive: bool,
tls: &TlsOptions,
) -> Result<()> {
if editor.is_some() && !command.is_empty() {
Expand Down Expand Up @@ -1798,6 +1799,27 @@ pub async fn sandbox_create(
.await?;

let policy = load_sandbox_policy(policy)?;

if permissive {
let has_protocol_endpoints = policy.as_ref().is_some_and(|p| {
p.network_policies
.values()
.any(|rule| rule.endpoints.iter().any(|ep| !ep.protocol.is_empty()))
});
if !has_protocol_endpoints {
eprintln!(
"\n{} Permissive mode: no HTTP endpoints declared in policy.\n \
L7 audit will be limited to host:port only.\n \
To capture HTTP method/path detail, add endpoint protocol hints:\n\n \
endpoints:\n \
- host: api.example.com\n \
port: 443\n \
protocol: rest\n",
"⚠".yellow(),
);
}
}

let resource_limits = build_sandbox_resource_limits(cpu, memory)?;
let driver_config = driver_config_json
.map(parse_driver_config_json)
Expand All @@ -1822,6 +1844,7 @@ pub async fn sandbox_create(
policy,
providers: configured_providers,
template,
permissive,
..SandboxSpec::default()
}),
name: name.unwrap_or_default().to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,7 @@ async fn sandbox_create_keeps_command_sessions_by_default() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -841,6 +842,7 @@ async fn sandbox_create_sends_cpu_and_memory_limits_only() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -919,6 +921,7 @@ async fn sandbox_create_sends_driver_config_json() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -994,6 +997,7 @@ async fn sandbox_create_does_not_infer_command_providers_when_v2_enabled() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -1053,6 +1057,7 @@ async fn sandbox_create_returns_vm_error_without_waiting_for_timeout() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -1108,6 +1113,7 @@ async fn sandbox_create_keeps_waiting_while_vm_progress_arrives() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -1155,6 +1161,7 @@ async fn sandbox_create_times_out_when_only_logs_arrive() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -1198,6 +1205,7 @@ async fn sandbox_create_deletes_command_sessions_with_no_keep() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -1245,6 +1253,7 @@ async fn sandbox_create_deletes_shell_sessions_with_no_keep() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -1292,6 +1301,7 @@ async fn sandbox_create_keeps_sandbox_with_hidden_keep_flag() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -1339,6 +1349,7 @@ async fn sandbox_create_keeps_sandbox_with_forwarding() {
&HashMap::new(),
&HashMap::new(),
"manual",
false,
&tls,
)
.await
Expand Down Expand Up @@ -1382,6 +1393,7 @@ async fn sandbox_create_sends_environment_variables() {
&HashMap::new(),
&env_map,
"manual",
false,
&tls,
)
.await
Expand Down
42 changes: 27 additions & 15 deletions crates/openshell-sandbox/src/grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,21 +546,6 @@ async fn connect_inference(endpoint: &str) -> Result<InferenceClient<AuthedChann
Ok(InferenceClient::new(channel))
}

/// Fetch sandbox policy from `OpenShell` server via gRPC.
///
/// Returns `Ok(Some(policy))` when the server has a policy configured,
/// or `Ok(None)` when the sandbox was created without a policy (the sandbox
/// should discover one from disk or use the restrictive default).
pub async fn fetch_policy(endpoint: &str, sandbox_id: &str) -> Result<Option<ProtoSandboxPolicy>> {
debug!(endpoint = %endpoint, sandbox_id = %sandbox_id, "Connecting to OpenShell server");

let mut client = connect(endpoint).await?;

debug!("Connected, fetching sandbox policy");

fetch_policy_with_client(&mut client, sandbox_id).await
}

/// Fetch sandbox policy using an existing client connection.
async fn fetch_policy_with_client(
client: &mut OpenShellClient<AuthedChannel>,
Expand All @@ -585,6 +570,33 @@ async fn fetch_policy_with_client(
})?))
}

/// Fetch sandbox policy and permissive flag from the config response.
pub async fn fetch_policy_and_permissive(
endpoint: &str,
sandbox_id: &str,
) -> Result<(Option<ProtoSandboxPolicy>, bool)> {
let mut client = connect(endpoint).await?;

let response = client
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_id: sandbox_id.to_string(),
})
.await
.into_diagnostic()?;

let inner = response.into_inner();
let permissive = inner.permissive;

if inner.version == 0 && inner.policy.is_none() {
return Ok((None, permissive));
}

let policy = inner
.policy
.ok_or_else(|| miette::miette!("Server returned non-zero version but empty policy"))?;
Ok((Some(policy), permissive))
}

/// Sync a locally-discovered policy using an existing client connection.
async fn sync_policy_with_client(
client: &mut OpenShellClient<AuthedChannel>,
Expand Down
2 changes: 2 additions & 0 deletions crates/openshell-sandbox/src/l7/graphql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,8 @@ network_policies:
cmdline_paths: Vec::new(),
secret_resolver: None,
activity_tx: None,
permissive: false,
denial_tx: None,
};
let request_info = crate::l7::L7RequestInfo {
action: req.action,
Expand Down
Loading
Loading