Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a477d4d
docs(rfc): add agent-driven policy management
zredlined Apr 30, 2026
95a4462
docs(rfc): switch policy MVP to local API
zredlined Apr 30, 2026
320c828
chore(deps): refresh cargo lockfile
zredlined Apr 30, 2026
91014bc
docs(rfc): clarify policy advisor skill and local logs
zredlined Apr 30, 2026
41e3f8d
feat(sandbox): add agent-driven policy proposal loop
zredlined May 1, 2026
fd54698
test(examples): add codex policy dogfood loop
zredlined May 4, 2026
3905c08
refactor(examples): make policy demo agent-agnostic
zredlined May 4, 2026
5d24a61
refactor(examples): colocate policy validation harness
zredlined May 4, 2026
ab6e803
docs(examples): add policy demo env sample
zredlined May 4, 2026
654e33c
docs(examples): use placeholder env example
zredlined May 4, 2026
5892d19
feat(sandbox): wire policy.local denials to OCSF JSONL log
zredlined May 4, 2026
5086ed1
feat(cli): show L7 protocol/method/path in rule get output
zredlined May 4, 2026
6d5da80
refactor(examples): rewrite policy demo as Codex-default loop
zredlined May 4, 2026
019de3c
style(sandbox,cli): apply rustfmt
zredlined May 4, 2026
f2f94cd
perf(examples): cap Codex reasoning at 'low' in policy demo
zredlined May 4, 2026
0313de6
fix(sandbox): harden policy.local denials endpoint
zredlined May 4, 2026
1599c8b
fix(examples): redact tokens in agent log tail and validate DEMO_FILE…
zredlined May 4, 2026
af8ad76
refactor(sandbox): centralize policy.local routes and skill path
zredlined May 4, 2026
002bd0a
chore: merge main into agent policy PR
zredlined May 4, 2026
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
107 changes: 97 additions & 10 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5762,17 +5762,57 @@ pub async fn sandbox_draft_history(server: &str, name: &str, tls: &TlsOptions) -
fn format_endpoints(rule: &openshell_core::proto::NetworkPolicyRule) -> String {
rule.endpoints
.iter()
.map(|e| {
if e.port > 0 {
format!("{}:{}", e.host, e.port)
} else {
e.host.clone()
}
})
.map(format_endpoint)
.collect::<Vec<_>>()
.join(", ")
}

/// Render an endpoint as `host:port [layer, …allows…, …denies…]` so a reader
/// can tell L4-only access apart from a method/path-scoped L7 grant. The L7
/// fields (`protocol: rest`, `rules`, `access`) materially change what gets
/// allowed; surfacing them in the default text output is what makes
/// `openshell rule get` useful for approval review.
fn format_endpoint(endpoint: &openshell_core::proto::NetworkEndpoint) -> String {
let host_port = if endpoint.port > 0 {
format!("{}:{}", endpoint.host, endpoint.port)
} else {
endpoint.host.clone()
};

let mut tags: Vec<String> = Vec::new();
let layer_tag = if endpoint.protocol.eq_ignore_ascii_case("rest") {
"L7 rest"
} else if endpoint.protocol.is_empty() {
"L4"
} else {
endpoint.protocol.as_str()
};
tags.push(layer_tag.to_string());

if !endpoint.access.is_empty() {
tags.push(format!("access={}", endpoint.access));
}

for r in &endpoint.rules {
if let Some(allow) = &r.allow {
let method = non_empty_or(&allow.method, "*");
let path = non_empty_or(&allow.path, "*");
tags.push(format!("allow {method} {path}"));
}
}
for r in &endpoint.deny_rules {
let method = non_empty_or(&r.method, "*");
let path = non_empty_or(&r.path, "*");
tags.push(format!("deny {method} {path}"));
}

format!("{host_port} [{}]", tags.join(", "))
}

fn non_empty_or<'a>(value: &'a str, fallback: &'a str) -> &'a str {
if value.is_empty() { fallback } else { value }
}

/// Format a millisecond timestamp into a readable string.
fn format_timestamp_ms(ms: i64) -> String {
if ms <= 0 {
Expand All @@ -5793,9 +5833,9 @@ fn format_timestamp_ms(ms: i64) -> String {
mod tests {
use super::{
GatewayControlTarget, TlsOptions, dockerfile_sources_supported_for_gateway,
format_gateway_select_header, format_gateway_select_items, gateway_add, gateway_auth_label,
gateway_select_with, gateway_type_label, git_sync_files, http_health_check,
image_requests_gpu, inferred_provider_type, parse_cli_setting_value,
format_endpoint, format_gateway_select_header, format_gateway_select_items, gateway_add,
gateway_auth_label, gateway_select_with, gateway_type_label, git_sync_files,
http_health_check, image_requests_gpu, inferred_provider_type, parse_cli_setting_value,
parse_credential_pairs, plaintext_gateway_is_remote, provisioning_timeout_message,
ready_false_condition_message, resolve_from, resolve_gateway_control_target_from,
sandbox_should_persist, shell_escape, source_requests_gpu, validate_gateway_name,
Expand Down Expand Up @@ -6560,4 +6600,51 @@ mod tests {
"should end with single-quote: {ssh_escaped}"
);
}

#[test]
fn format_endpoint_distinguishes_l4_from_l7_rest() {
use openshell_core::proto::{L7Allow, L7DenyRule, L7Rule, NetworkEndpoint};

let l4 = NetworkEndpoint {
host: "host.example.test".to_string(),
port: 443,
..Default::default()
};
assert_eq!(format_endpoint(&l4), "host.example.test:443 [L4]");

let l7_readonly = NetworkEndpoint {
host: "host.example.test".to_string(),
port: 443,
protocol: "rest".to_string(),
access: "read-only".to_string(),
..Default::default()
};
assert_eq!(
format_endpoint(&l7_readonly),
"host.example.test:443 [L7 rest, access=read-only]"
);

let l7_scoped = NetworkEndpoint {
host: "host.example.test".to_string(),
port: 443,
protocol: "rest".to_string(),
rules: vec![L7Rule {
allow: Some(L7Allow {
method: "PUT".to_string(),
path: "/v1/example/resource".to_string(),
..Default::default()
}),
}],
deny_rules: vec![L7DenyRule {
method: "DELETE".to_string(),
path: "/v1/example/resource".to_string(),
..Default::default()
}],
..Default::default()
};
assert_eq!(
format_endpoint(&l7_scoped),
"host.example.test:443 [L7 rest, allow PUT /v1/example/resource, deny DELETE /v1/example/resource]"
);
}
}
17 changes: 11 additions & 6 deletions crates/openshell-sandbox/src/grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use miette::{IntoDiagnostic, Result, WrapErr};
use openshell_core::proto::{
DenialSummary, GetInferenceBundleRequest, GetInferenceBundleResponse, GetSandboxConfigRequest,
GetSandboxProviderEnvironmentRequest, PolicySource, PolicyStatus, ReportPolicyStatusRequest,
SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest, UpdateConfigRequest,
inference_client::InferenceClient, open_shell_client::OpenShellClient,
SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse,
UpdateConfigRequest, inference_client::InferenceClient, open_shell_client::OpenShellClient,
};
use tonic::service::interceptor::InterceptedService;
use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
Expand Down Expand Up @@ -318,15 +318,20 @@ impl CachedOpenShellClient {
})
}

/// Submit denial summaries for policy analysis.
/// Submit denial summaries and/or agent-authored proposals for policy analysis.
///
/// Returns the gateway response so callers can surface accepted/rejected
/// counts and rejection reasons (e.g., the `policy.local` API forwards
/// these to the in-sandbox agent).
pub async fn submit_policy_analysis(
&self,
sandbox_name: &str,
summaries: Vec<DenialSummary>,
proposed_chunks: Vec<openshell_core::proto::PolicyChunk>,
analysis_mode: &str,
) -> Result<()> {
self.client
) -> Result<SubmitPolicyAnalysisResponse> {
let response = self
.client
.clone()
.submit_policy_analysis(SubmitPolicyAnalysisRequest {
name: sandbox_name.to_string(),
Expand All @@ -337,7 +342,7 @@ impl CachedOpenShellClient {
.await
.into_diagnostic()?;

Ok(())
Ok(response.into_inner())
}

/// Report policy load status back to the server.
Expand Down
15 changes: 15 additions & 0 deletions crates/openshell-sandbox/src/l7/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ where
&reason,
client,
Some(&redacted_target),
Some(crate::l7::rest::DenyResponseContext {
host: Some(&ctx.host),
port: Some(ctx.port),
binary: Some(&ctx.binary_path),
}),
)
.await?;
return Ok(());
Expand Down Expand Up @@ -584,6 +589,11 @@ where
&reason,
client,
Some(&redacted_target),
Some(crate::l7::rest::DenyResponseContext {
host: Some(&ctx.host),
port: Some(ctx.port),
binary: Some(&ctx.binary_path),
}),
)
.await?;
return Ok(());
Expand Down Expand Up @@ -789,6 +799,11 @@ where
&reason,
client,
Some(&redacted_target),
Some(crate::l7::rest::DenyResponseContext {
host: Some(&ctx.host),
port: Some(ctx.port),
binary: Some(&ctx.binary_path),
}),
)
.await?;
return Ok(());
Expand Down
Loading
Loading