diff --git a/.agents/skills/openshell-cli/cli-reference.md b/.agents/skills/openshell-cli/cli-reference.md index c9c5450a0..bdbb35572 100644 --- a/.agents/skills/openshell-cli/cli-reference.md +++ b/.agents/skills/openshell-cli/cli-reference.md @@ -181,7 +181,11 @@ Create a sandbox, wait for readiness, then connect or execute the trailing comma ### `openshell sandbox get ` -Show sandbox details (id, name, namespace, phase, policy). +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). + +| Flag | Description | +|------|-------------| +| `--policy-only` | Print only the active policy YAML to stdout (same policy as above; use for scripts and piping) | ### `openshell sandbox list` diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index 8474afd15..6239978d7 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -1208,6 +1208,10 @@ enum SandboxCommands { /// Sandbox name (defaults to last-used sandbox). #[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))] name: Option, + + /// Print only the active policy YAML (same policy as the default view; stdout only). + #[arg(long)] + policy_only: bool, }, /// List sandboxes. @@ -2461,9 +2465,9 @@ async fn main() -> Result<()> { | SandboxCommands::Download { .. } => { unreachable!() } - SandboxCommands::Get { name } => { + SandboxCommands::Get { name, policy_only } => { let name = resolve_sandbox_name(name, &ctx.name)?; - run::sandbox_get(endpoint, &name, &tls).await?; + run::sandbox_get(endpoint, &name, policy_only, &tls).await?; } SandboxCommands::List { limit, diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 60e28ac7e..ba25488b8 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -2734,7 +2734,16 @@ pub async fn sandbox_sync_command( } /// Fetch a sandbox by name. -pub async fn sandbox_get(server: &str, name: &str, tls: &TlsOptions) -> Result<()> { +/// +/// Policy always comes from [`GetSandboxConfig`] (effective active policy, sandbox +/// or global). With `policy_only`, prints only that YAML to stdout; otherwise +/// prints sandbox metadata and the same policy with formatted YAML. +pub async fn sandbox_get( + server: &str, + name: &str, + policy_only: bool, + tls: &TlsOptions, +) -> Result<()> { let mut client = grpc_client(server, tls).await?; let response = client @@ -2748,6 +2757,26 @@ pub async fn sandbox_get(server: &str, name: &str, tls: &TlsOptions) -> Result<( .sandbox .ok_or_else(|| miette::miette!("sandbox missing from response"))?; + let config = client + .get_sandbox_config(GetSandboxConfigRequest { + sandbox_id: sandbox.id.clone(), + }) + .await + .into_diagnostic()? + .into_inner(); + + if policy_only { + let Some(ref policy) = config.policy else { + return Err(miette::miette!( + "no active policy configured for this sandbox" + )); + }; + let yaml_str = openshell_policy::serialize_sandbox_policy(policy) + .wrap_err("failed to serialize policy to YAML")?; + print!("{yaml_str}"); + return Ok(()); + } + println!("{}", "Sandbox:".cyan().bold()); println!(); println!(" {} {}", "Id:".dimmed(), sandbox.id); @@ -2755,9 +2784,34 @@ pub async fn sandbox_get(server: &str, name: &str, tls: &TlsOptions) -> Result<( println!(" {} {}", "Namespace:".dimmed(), sandbox.namespace); println!(" {} {}", "Phase:".dimmed(), phase_name(sandbox.phase)); - if let Some(spec) = &sandbox.spec - && let Some(policy) = &spec.policy - { + let policy_from_global = config.policy_source == PolicySource::Global as i32; + println!( + " {} {}", + "Policy source:".dimmed(), + if policy_from_global { + "global" + } else { + "sandbox" + } + ); + let revision = if policy_from_global { + if config.global_policy_version > 0 { + Some(config.global_policy_version) + } else if config.version > 0 { + Some(config.version) + } else { + None + } + } else if config.version > 0 { + Some(config.version) + } else { + None + }; + if let Some(rev) = revision { + println!(" {} {}", "Revision:".dimmed(), rev); + } + + if let Some(ref policy) = config.policy { println!(); print_sandbox_policy(policy); } diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index fbadec4c3..885f659d0 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -12,8 +12,8 @@ use openshell_core::proto::{ GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse, - ListSandboxesRequest, ListSandboxesResponse, ProviderResponse, Sandbox, SandboxResponse, - SandboxStreamEvent, ServiceStatus, UpdateProviderRequest, WatchSandboxRequest, + ListSandboxesRequest, ListSandboxesResponse, ProviderResponse, Sandbox, SandboxPolicy, + SandboxResponse, SandboxStreamEvent, ServiceStatus, UpdateProviderRequest, WatchSandboxRequest, }; use rcgen::{ BasicConstraints, Certificate, CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair, @@ -134,9 +134,20 @@ impl OpenShell for TestOpenShell { async fn get_sandbox_config( &self, - _request: tonic::Request, + request: tonic::Request, ) -> Result, Status> { - Ok(Response::new(GetSandboxConfigResponse::default())) + let req = request.into_inner(); + assert_eq!( + req.sandbox_id, "test-id", + "sandbox_get --policy-only should pass the id from GetSandbox" + ); + Ok(Response::new(GetSandboxConfigResponse { + policy: Some(SandboxPolicy { + version: 1, + ..Default::default() + }), + ..Default::default() + })) } async fn get_gateway_config( @@ -432,7 +443,7 @@ async fn run_server() -> TestServer { async fn sandbox_get_sends_correct_name() { let ts = run_server().await; - run::sandbox_get(&ts.endpoint, "my-sandbox", &ts.tls) + run::sandbox_get(&ts.endpoint, "my-sandbox", false, &ts.tls) .await .expect("sandbox_get should succeed"); @@ -444,6 +455,19 @@ async fn sandbox_get_sends_correct_name() { ); } +/// `sandbox_get` with `policy_only` calls `GetSandboxConfig` and prints YAML from the response. +#[tokio::test] +async fn sandbox_get_policy_only_round_trip() { + let ts = run_server().await; + + run::sandbox_get(&ts.endpoint, "my-sandbox", true, &ts.tls) + .await + .expect("sandbox_get with policy_only should succeed"); + + let recorded = ts.openshell.state.last_get_name.lock().await.clone(); + assert_eq!(recorded.as_deref(), Some("my-sandbox")); +} + /// End-to-end: save a last-used sandbox, load it back, then call `sandbox_get` /// with the resolved name. This validates the persistence + gRPC wiring. #[tokio::test] @@ -462,7 +486,7 @@ async fn sandbox_get_with_persisted_last_sandbox() { assert_eq!(resolved, "persisted-sb"); // Call sandbox_get with the resolved name. - run::sandbox_get(&ts.endpoint, &resolved, &ts.tls) + run::sandbox_get(&ts.endpoint, &resolved, false, &ts.tls) .await .expect("sandbox_get should succeed"); @@ -484,7 +508,7 @@ async fn explicit_name_takes_precedence_over_persisted() { // Persist one name, but supply a different one explicitly. save_last_sandbox("my-cluster", "old-sandbox").expect("save should succeed"); - run::sandbox_get(&ts.endpoint, "explicit-sandbox", &ts.tls) + run::sandbox_get(&ts.endpoint, "explicit-sandbox", false, &ts.tls) .await .expect("sandbox_get should succeed"); diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index 1be05e81e..6d10efbd0 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -110,12 +110,18 @@ List all sandboxes: openshell sandbox list ``` -Get detailed information about a specific sandbox: +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: ```shell openshell sandbox get my-sandbox ``` +Print only that policy YAML for scripting (same effective policy, no metadata): + +```shell +openshell sandbox get my-sandbox --policy-only +``` + Stream sandbox logs to monitor agent activity and diagnose policy decisions: ```shell