Skip to content

Commit 4fdafa6

Browse files
committed
feat(cli): add json output for policy get
1 parent cade0bb commit 4fdafa6

3 files changed

Lines changed: 208 additions & 12 deletions

File tree

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,10 +1587,14 @@ enum PolicyCommands {
15871587
#[arg(long = "rev", default_value_t = 0)]
15881588
rev: u32,
15891589

1590-
/// Print the full policy as YAML.
1590+
/// Include the full policy payload in the output.
15911591
#[arg(long)]
15921592
full: bool,
15931593

1594+
/// Print policy revision metadata and optional full policy as JSON.
1595+
#[arg(long)]
1596+
json: bool,
1597+
15941598
/// Show the global policy revision.
15951599
#[arg(long)]
15961600
global: bool,
@@ -2267,13 +2271,16 @@ async fn main() -> Result<()> {
22672271
name,
22682272
rev,
22692273
full,
2274+
json,
22702275
global,
22712276
} => {
22722277
if global {
2273-
run::sandbox_policy_get_global(&ctx.endpoint, rev, full, &tls).await?;
2278+
run::sandbox_policy_get_global(&ctx.endpoint, rev, full, json, &tls)
2279+
.await?;
22742280
} else {
22752281
let name = resolve_sandbox_name(name, &ctx.name)?;
2276-
run::sandbox_policy_get(&ctx.endpoint, &name, rev, full, &tls).await?;
2282+
run::sandbox_policy_get(&ctx.endpoint, &name, rev, full, json, &tls)
2283+
.await?;
22772284
}
22782285
}
22792286
PolicyCommands::List {
@@ -3935,6 +3942,26 @@ mod tests {
39353942
}
39363943
}
39373944

3945+
#[test]
3946+
fn policy_get_accepts_json_flag() {
3947+
let cli = Cli::try_parse_from(["openshell", "policy", "get", "demo", "--full", "--json"])
3948+
.expect("policy get --json should parse");
3949+
3950+
match cli.command {
3951+
Some(Commands::Policy {
3952+
command:
3953+
Some(PolicyCommands::Get {
3954+
name, full, json, ..
3955+
}),
3956+
}) => {
3957+
assert_eq!(name.as_deref(), Some("demo"));
3958+
assert!(full);
3959+
assert!(json);
3960+
}
3961+
other => panic!("expected policy get command, got: {other:?}"),
3962+
}
3963+
}
3964+
39383965
#[test]
39393966
fn settings_delete_global_parses_yes_flag() {
39403967
let cli = Cli::try_parse_from([

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

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ use openshell_core::proto::{
4646
ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderProfile,
4747
ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest,
4848
RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, SandboxPhase, SandboxPolicy,
49-
SandboxSpec, SandboxTemplate, ServiceEndpointResponse, SetClusterInferenceRequest,
50-
SettingScope, SettingValue, TcpForwardFrame, TcpForwardInit, TcpRelayTarget,
51-
UpdateConfigRequest, UpdateProviderRequest, WatchSandboxRequest, exec_sandbox_event,
52-
setting_value, tcp_forward_init,
49+
SandboxPolicyRevision, SandboxSpec, SandboxTemplate, ServiceEndpointResponse,
50+
SetClusterInferenceRequest, SettingScope, SettingValue, TcpForwardFrame, TcpForwardInit,
51+
TcpRelayTarget, UpdateConfigRequest, UpdateProviderRequest, WatchSandboxRequest,
52+
exec_sandbox_event, setting_value, tcp_forward_init,
5353
};
5454
use openshell_core::settings::{self, SettingValueKind};
5555
use openshell_core::{ObjectId, ObjectName};
@@ -6086,6 +6086,7 @@ pub async fn sandbox_policy_get(
60866086
name: &str,
60876087
version: u32,
60886088
full: bool,
6089+
json: bool,
60896090
tls: &TlsOptions,
60906091
) -> Result<()> {
60916092
let mut client = grpc_client(server, tls).await?;
@@ -6101,6 +6102,17 @@ pub async fn sandbox_policy_get(
61016102

61026103
let inner = status_resp.into_inner();
61036104
if let Some(rev) = inner.revision {
6105+
if json {
6106+
print_policy_get_json(
6107+
"sandbox",
6108+
Some(name),
6109+
Some(inner.active_version),
6110+
&rev,
6111+
full,
6112+
)?;
6113+
return Ok(());
6114+
}
6115+
61046116
let status = PolicyStatus::try_from(rev.status).unwrap_or(PolicyStatus::Unspecified);
61056117
println!("Version: {}", rev.version);
61066118
println!("Hash: {}", rev.policy_hash);
@@ -6137,6 +6149,7 @@ pub async fn sandbox_policy_get_global(
61376149
server: &str,
61386150
version: u32,
61396151
full: bool,
6152+
json: bool,
61406153
tls: &TlsOptions,
61416154
) -> Result<()> {
61426155
let mut client = grpc_client(server, tls).await?;
@@ -6152,6 +6165,11 @@ pub async fn sandbox_policy_get_global(
61526165

61536166
let inner = status_resp.into_inner();
61546167
if let Some(rev) = inner.revision {
6168+
if json {
6169+
print_policy_get_json("global", None, None, &rev, full)?;
6170+
return Ok(());
6171+
}
6172+
61556173
let status = PolicyStatus::try_from(rev.status).unwrap_or(PolicyStatus::Unspecified);
61566174
println!("Scope: global");
61576175
println!("Version: {}", rev.version);
@@ -6181,6 +6199,96 @@ pub async fn sandbox_policy_get_global(
61816199
Ok(())
61826200
}
61836201

6202+
fn print_policy_get_json(
6203+
scope: &str,
6204+
name: Option<&str>,
6205+
active_version: Option<u32>,
6206+
revision: &SandboxPolicyRevision,
6207+
include_policy: bool,
6208+
) -> Result<()> {
6209+
let value = policy_get_json_value(scope, name, active_version, revision, include_policy)?;
6210+
println!(
6211+
"{}",
6212+
serde_json::to_string_pretty(&value).into_diagnostic()?
6213+
);
6214+
Ok(())
6215+
}
6216+
6217+
fn policy_get_json_value(
6218+
scope: &str,
6219+
name: Option<&str>,
6220+
active_version: Option<u32>,
6221+
revision: &SandboxPolicyRevision,
6222+
include_policy: bool,
6223+
) -> Result<serde_json::Value> {
6224+
let mut revision_obj = serde_json::Map::new();
6225+
revision_obj.insert("version".to_string(), serde_json::json!(revision.version));
6226+
revision_obj.insert("hash".to_string(), serde_json::json!(revision.policy_hash));
6227+
revision_obj.insert(
6228+
"status".to_string(),
6229+
serde_json::json!(policy_status_json_name(revision.status)),
6230+
);
6231+
revision_obj.insert(
6232+
"created_at_ms".to_string(),
6233+
serde_json::json!(revision.created_at_ms),
6234+
);
6235+
revision_obj.insert(
6236+
"loaded_at_ms".to_string(),
6237+
serde_json::json!(revision.loaded_at_ms),
6238+
);
6239+
if !revision.load_error.is_empty() {
6240+
revision_obj.insert(
6241+
"load_error".to_string(),
6242+
serde_json::json!(revision.load_error),
6243+
);
6244+
}
6245+
if include_policy {
6246+
let policy = revision
6247+
.policy
6248+
.as_ref()
6249+
.map(sandbox_policy_json_value)
6250+
.transpose()?
6251+
.unwrap_or(serde_json::Value::Null);
6252+
revision_obj.insert("policy".to_string(), policy);
6253+
}
6254+
6255+
let mut root = serde_json::Map::new();
6256+
root.insert("scope".to_string(), serde_json::json!(scope));
6257+
if let Some(name) = name {
6258+
root.insert("name".to_string(), serde_json::json!(name));
6259+
}
6260+
if let Some(active_version) = active_version {
6261+
root.insert(
6262+
"active_version".to_string(),
6263+
serde_json::json!(active_version),
6264+
);
6265+
}
6266+
root.insert(
6267+
"revision".to_string(),
6268+
serde_json::Value::Object(revision_obj),
6269+
);
6270+
6271+
Ok(serde_json::Value::Object(root))
6272+
}
6273+
6274+
fn policy_status_json_name(status: i32) -> &'static str {
6275+
match PolicyStatus::try_from(status).unwrap_or(PolicyStatus::Unspecified) {
6276+
PolicyStatus::Unspecified => "unspecified",
6277+
PolicyStatus::Pending => "pending",
6278+
PolicyStatus::Loaded => "loaded",
6279+
PolicyStatus::Failed => "failed",
6280+
PolicyStatus::Superseded => "superseded",
6281+
}
6282+
}
6283+
6284+
fn sandbox_policy_json_value(policy: &SandboxPolicy) -> Result<serde_json::Value> {
6285+
let yaml_str = openshell_policy::serialize_sandbox_policy(policy)
6286+
.wrap_err("failed to serialize policy to YAML")?;
6287+
serde_yml::from_str(&yaml_str)
6288+
.into_diagnostic()
6289+
.wrap_err("failed to convert policy to JSON")
6290+
}
6291+
61846292
pub async fn sandbox_policy_list(
61856293
server: &str,
61866294
name: &str,
@@ -6232,7 +6340,7 @@ pub async fn sandbox_policy_list_global(server: &str, limit: u32, tls: &TlsOptio
62326340
Ok(())
62336341
}
62346342

6235-
fn print_policy_revision_table(revisions: &[openshell_core::proto::SandboxPolicyRevision]) {
6343+
fn print_policy_revision_table(revisions: &[SandboxPolicyRevision]) {
62366344
println!(
62376345
"{:<8} {:<14} {:<12} {:<24} ERROR",
62386346
"VERSION", "HASH", "STATUS", "CREATED"
@@ -6711,7 +6819,7 @@ mod tests {
67116819
git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
67126820
inferred_provider_type, package_managed_tls_dirs, parse_cli_setting_value,
67136821
parse_credential_expiry_cli_value, parse_credential_expiry_pairs, parse_credential_pairs,
6714-
plaintext_gateway_is_remote, progress_step_from_metadata,
6822+
plaintext_gateway_is_remote, policy_get_json_value, progress_step_from_metadata,
67156823
provider_profile_allows_refresh_bootstrap, provisioning_timeout_message,
67166824
ready_false_condition_message, refresh_status_header, refresh_status_row, resolve_from,
67176825
sandbox_should_persist, service_expose_status_error, service_url_for_gateway,
@@ -6733,9 +6841,10 @@ mod tests {
67336841
PROGRESS_STEP_STARTING_SANDBOX,
67346842
};
67356843
use openshell_core::proto::{
6736-
Provider, ProviderCredentialRefresh, ProviderCredentialRefreshStatus,
6737-
ProviderCredentialRefreshStrategy, ProviderProfile, ProviderProfileCredential,
6738-
SandboxCondition, SandboxStatus, datamodel::v1::ObjectMeta,
6844+
FilesystemPolicy, PolicyStatus, Provider, ProviderCredentialRefresh,
6845+
ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderProfile,
6846+
ProviderProfileCredential, SandboxCondition, SandboxPolicy, SandboxPolicyRevision,
6847+
SandboxStatus, datamodel::v1::ObjectMeta,
67396848
};
67406849

67416850
struct EnvVarGuard {
@@ -7112,6 +7221,60 @@ mod tests {
71127221
assert!(build_sandbox_resource_limits(None, Some("1.5Gi")).is_err());
71137222
}
71147223

7224+
#[test]
7225+
fn policy_get_json_includes_metadata_and_full_policy_when_requested() {
7226+
let revision = SandboxPolicyRevision {
7227+
version: 7,
7228+
policy_hash: "sha256:test".to_string(),
7229+
status: PolicyStatus::Loaded as i32,
7230+
created_at_ms: 10,
7231+
loaded_at_ms: 20,
7232+
policy: Some(SandboxPolicy {
7233+
version: 1,
7234+
filesystem: Some(FilesystemPolicy {
7235+
include_workdir: true,
7236+
read_only: vec!["/usr".to_string()],
7237+
read_write: vec!["/sandbox".to_string()],
7238+
}),
7239+
..Default::default()
7240+
}),
7241+
..Default::default()
7242+
};
7243+
7244+
let value = policy_get_json_value("sandbox", Some("demo"), Some(7), &revision, true)
7245+
.expect("policy JSON should render");
7246+
7247+
assert_eq!(value["scope"].as_str(), Some("sandbox"));
7248+
assert_eq!(value["name"].as_str(), Some("demo"));
7249+
assert_eq!(value["active_version"].as_u64(), Some(7));
7250+
assert_eq!(value["revision"]["version"].as_u64(), Some(7));
7251+
assert_eq!(value["revision"]["hash"].as_str(), Some("sha256:test"));
7252+
assert_eq!(value["revision"]["status"].as_str(), Some("loaded"));
7253+
assert_eq!(value["revision"]["policy"]["version"].as_u64(), Some(1));
7254+
assert_eq!(
7255+
value["revision"]["policy"]["filesystem_policy"]["read_write"][0].as_str(),
7256+
Some("/sandbox")
7257+
);
7258+
}
7259+
7260+
#[test]
7261+
fn policy_get_json_omits_policy_without_full() {
7262+
let revision = SandboxPolicyRevision {
7263+
version: 3,
7264+
policy_hash: "sha256:test".to_string(),
7265+
status: PolicyStatus::Pending as i32,
7266+
..Default::default()
7267+
};
7268+
7269+
let value = policy_get_json_value("global", None, None, &revision, false)
7270+
.expect("policy JSON should render");
7271+
7272+
assert_eq!(value["scope"].as_str(), Some("global"));
7273+
assert!(value.get("name").is_none());
7274+
assert!(value.get("active_version").is_none());
7275+
assert!(value["revision"].get("policy").is_none());
7276+
}
7277+
71157278
#[test]
71167279
fn inferred_provider_type_returns_type_for_known_command() {
71177280
let result = inferred_provider_type(&["claude".to_string(), "--help".to_string()]);

‎docs/sandboxes/policies.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ The following steps outline the hot-reload policy update workflow.
144144
openshell policy get <name> --full > current-policy.yaml
145145
```
146146

147+
Use JSON output when you want to inspect the policy with `jq`:
148+
149+
```shell
150+
openshell policy get <name> --full --json | jq '.revision.policy.network_policies'
151+
```
152+
147153
5. Edit the YAML: add or adjust `network_policies` entries, binaries, `access`, or `rules`.
148154

149155
6. Push the updated policy when you need a full replacement. Exit codes: 0 = loaded, 1 = validation failed, 124 = timeout.

0 commit comments

Comments
 (0)