Skip to content

Commit 9ac725f

Browse files
authored
fix(cli): sandbox get returns currently active runtime policy (#880)
* feat(cli): Allow printing only the policy when retrieving sandbox info * feat(cli): Indicate policy source and revision when retrieving sandboxes * cargo fmt
1 parent b39f5aa commit 9ac725f

5 files changed

Lines changed: 107 additions & 15 deletions

File tree

.agents/skills/openshell-cli/cli-reference.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ Create a sandbox, wait for readiness, then connect or execute the trailing comma
181181

182182
### `openshell sandbox get <name>`
183183

184-
Show sandbox details (id, name, namespace, phase, policy).
184+
Show sandbox details (id, name, namespace, phase) and the **active** policy from the gateway (same source whether policy is sandbox-scoped or global). Metadata includes **Policy source** (`sandbox` or `global`) and **Revision** (global policy row when source is global, otherwise sandbox policy row).
185+
186+
| Flag | Description |
187+
|------|-------------|
188+
| `--policy-only` | Print only the active policy YAML to stdout (same policy as above; use for scripts and piping) |
185189

186190
### `openshell sandbox list`
187191

crates/openshell-cli/src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,10 @@ enum SandboxCommands {
12081208
/// Sandbox name (defaults to last-used sandbox).
12091209
#[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))]
12101210
name: Option<String>,
1211+
1212+
/// Print only the active policy YAML (same policy as the default view; stdout only).
1213+
#[arg(long)]
1214+
policy_only: bool,
12111215
},
12121216

12131217
/// List sandboxes.
@@ -2461,9 +2465,9 @@ async fn main() -> Result<()> {
24612465
| SandboxCommands::Download { .. } => {
24622466
unreachable!()
24632467
}
2464-
SandboxCommands::Get { name } => {
2468+
SandboxCommands::Get { name, policy_only } => {
24652469
let name = resolve_sandbox_name(name, &ctx.name)?;
2466-
run::sandbox_get(endpoint, &name, &tls).await?;
2470+
run::sandbox_get(endpoint, &name, policy_only, &tls).await?;
24672471
}
24682472
SandboxCommands::List {
24692473
limit,

crates/openshell-cli/src/run.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2734,7 +2734,16 @@ pub async fn sandbox_sync_command(
27342734
}
27352735

27362736
/// Fetch a sandbox by name.
2737-
pub async fn sandbox_get(server: &str, name: &str, tls: &TlsOptions) -> Result<()> {
2737+
///
2738+
/// Policy always comes from [`GetSandboxConfig`] (effective active policy, sandbox
2739+
/// or global). With `policy_only`, prints only that YAML to stdout; otherwise
2740+
/// prints sandbox metadata and the same policy with formatted YAML.
2741+
pub async fn sandbox_get(
2742+
server: &str,
2743+
name: &str,
2744+
policy_only: bool,
2745+
tls: &TlsOptions,
2746+
) -> Result<()> {
27382747
let mut client = grpc_client(server, tls).await?;
27392748

27402749
let response = client
@@ -2748,16 +2757,61 @@ pub async fn sandbox_get(server: &str, name: &str, tls: &TlsOptions) -> Result<(
27482757
.sandbox
27492758
.ok_or_else(|| miette::miette!("sandbox missing from response"))?;
27502759

2760+
let config = client
2761+
.get_sandbox_config(GetSandboxConfigRequest {
2762+
sandbox_id: sandbox.id.clone(),
2763+
})
2764+
.await
2765+
.into_diagnostic()?
2766+
.into_inner();
2767+
2768+
if policy_only {
2769+
let Some(ref policy) = config.policy else {
2770+
return Err(miette::miette!(
2771+
"no active policy configured for this sandbox"
2772+
));
2773+
};
2774+
let yaml_str = openshell_policy::serialize_sandbox_policy(policy)
2775+
.wrap_err("failed to serialize policy to YAML")?;
2776+
print!("{yaml_str}");
2777+
return Ok(());
2778+
}
2779+
27512780
println!("{}", "Sandbox:".cyan().bold());
27522781
println!();
27532782
println!(" {} {}", "Id:".dimmed(), sandbox.id);
27542783
println!(" {} {}", "Name:".dimmed(), sandbox.name);
27552784
println!(" {} {}", "Namespace:".dimmed(), sandbox.namespace);
27562785
println!(" {} {}", "Phase:".dimmed(), phase_name(sandbox.phase));
27572786

2758-
if let Some(spec) = &sandbox.spec
2759-
&& let Some(policy) = &spec.policy
2760-
{
2787+
let policy_from_global = config.policy_source == PolicySource::Global as i32;
2788+
println!(
2789+
" {} {}",
2790+
"Policy source:".dimmed(),
2791+
if policy_from_global {
2792+
"global"
2793+
} else {
2794+
"sandbox"
2795+
}
2796+
);
2797+
let revision = if policy_from_global {
2798+
if config.global_policy_version > 0 {
2799+
Some(config.global_policy_version)
2800+
} else if config.version > 0 {
2801+
Some(config.version)
2802+
} else {
2803+
None
2804+
}
2805+
} else if config.version > 0 {
2806+
Some(config.version)
2807+
} else {
2808+
None
2809+
};
2810+
if let Some(rev) = revision {
2811+
println!(" {} {}", "Revision:".dimmed(), rev);
2812+
}
2813+
2814+
if let Some(ref policy) = config.policy {
27612815
println!();
27622816
print_sandbox_policy(policy);
27632817
}

crates/openshell-cli/tests/sandbox_name_fallback_integration.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use openshell_core::proto::{
1212
GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse,
1313
GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest,
1414
HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse,
15-
ListSandboxesRequest, ListSandboxesResponse, ProviderResponse, Sandbox, SandboxResponse,
16-
SandboxStreamEvent, ServiceStatus, UpdateProviderRequest, WatchSandboxRequest,
15+
ListSandboxesRequest, ListSandboxesResponse, ProviderResponse, Sandbox, SandboxPolicy,
16+
SandboxResponse, SandboxStreamEvent, ServiceStatus, UpdateProviderRequest, WatchSandboxRequest,
1717
};
1818
use rcgen::{
1919
BasicConstraints, Certificate, CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair,
@@ -134,9 +134,20 @@ impl OpenShell for TestOpenShell {
134134

135135
async fn get_sandbox_config(
136136
&self,
137-
_request: tonic::Request<GetSandboxConfigRequest>,
137+
request: tonic::Request<GetSandboxConfigRequest>,
138138
) -> Result<Response<GetSandboxConfigResponse>, Status> {
139-
Ok(Response::new(GetSandboxConfigResponse::default()))
139+
let req = request.into_inner();
140+
assert_eq!(
141+
req.sandbox_id, "test-id",
142+
"sandbox_get --policy-only should pass the id from GetSandbox"
143+
);
144+
Ok(Response::new(GetSandboxConfigResponse {
145+
policy: Some(SandboxPolicy {
146+
version: 1,
147+
..Default::default()
148+
}),
149+
..Default::default()
150+
}))
140151
}
141152

142153
async fn get_gateway_config(
@@ -432,7 +443,7 @@ async fn run_server() -> TestServer {
432443
async fn sandbox_get_sends_correct_name() {
433444
let ts = run_server().await;
434445

435-
run::sandbox_get(&ts.endpoint, "my-sandbox", &ts.tls)
446+
run::sandbox_get(&ts.endpoint, "my-sandbox", false, &ts.tls)
436447
.await
437448
.expect("sandbox_get should succeed");
438449

@@ -444,6 +455,19 @@ async fn sandbox_get_sends_correct_name() {
444455
);
445456
}
446457

458+
/// `sandbox_get` with `policy_only` calls `GetSandboxConfig` and prints YAML from the response.
459+
#[tokio::test]
460+
async fn sandbox_get_policy_only_round_trip() {
461+
let ts = run_server().await;
462+
463+
run::sandbox_get(&ts.endpoint, "my-sandbox", true, &ts.tls)
464+
.await
465+
.expect("sandbox_get with policy_only should succeed");
466+
467+
let recorded = ts.openshell.state.last_get_name.lock().await.clone();
468+
assert_eq!(recorded.as_deref(), Some("my-sandbox"));
469+
}
470+
447471
/// End-to-end: save a last-used sandbox, load it back, then call `sandbox_get`
448472
/// with the resolved name. This validates the persistence + gRPC wiring.
449473
#[tokio::test]
@@ -462,7 +486,7 @@ async fn sandbox_get_with_persisted_last_sandbox() {
462486
assert_eq!(resolved, "persisted-sb");
463487

464488
// Call sandbox_get with the resolved name.
465-
run::sandbox_get(&ts.endpoint, &resolved, &ts.tls)
489+
run::sandbox_get(&ts.endpoint, &resolved, false, &ts.tls)
466490
.await
467491
.expect("sandbox_get should succeed");
468492

@@ -484,7 +508,7 @@ async fn explicit_name_takes_precedence_over_persisted() {
484508
// Persist one name, but supply a different one explicitly.
485509
save_last_sandbox("my-cluster", "old-sandbox").expect("save should succeed");
486510

487-
run::sandbox_get(&ts.endpoint, "explicit-sandbox", &ts.tls)
511+
run::sandbox_get(&ts.endpoint, "explicit-sandbox", false, &ts.tls)
488512
.await
489513
.expect("sandbox_get should succeed");
490514

docs/sandboxes/manage-sandboxes.mdx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,18 @@ List all sandboxes:
110110
openshell sandbox list
111111
```
112112

113-
Get detailed information about a specific sandbox:
113+
Get detailed information about a specific sandbox. The output lists **Policy source** (`sandbox` or `global`), **Revision** (the active policy’s row version for that source), and the formatted active policy YAML:
114114

115115
```shell
116116
openshell sandbox get my-sandbox
117117
```
118118

119+
Print only that policy YAML for scripting (same effective policy, no metadata):
120+
121+
```shell
122+
openshell sandbox get my-sandbox --policy-only
123+
```
124+
119125
Stream sandbox logs to monitor agent activity and diagnose policy decisions:
120126

121127
```shell

0 commit comments

Comments
 (0)