Skip to content
Open
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
da5c06a
docs(rfc): add agent-driven policy management
zredlined Apr 30, 2026
9e20024
docs(rfc): switch policy MVP to local API
zredlined Apr 30, 2026
dd05161
docs(rfc): clarify policy advisor skill and local logs
zredlined Apr 30, 2026
145d3e6
feat(sandbox): add agent-driven policy proposal loop
zredlined May 1, 2026
6e2db2e
test(examples): add codex policy dogfood loop
zredlined May 4, 2026
8b17059
refactor(examples): make policy demo agent-agnostic
zredlined May 4, 2026
9d688e3
refactor(examples): colocate policy validation harness
zredlined May 4, 2026
4909ad5
docs(examples): add policy demo env sample
zredlined May 4, 2026
2ad4ad3
docs(examples): use placeholder env example
zredlined May 4, 2026
b709427
feat(sandbox): wire policy.local denials to OCSF JSONL log
zredlined May 4, 2026
3b2aaf1
feat(cli): show L7 protocol/method/path in rule get output
zredlined May 4, 2026
7ad05d9
refactor(examples): rewrite policy demo as Codex-default loop
zredlined May 4, 2026
ffbd130
style(sandbox,cli): apply rustfmt
zredlined May 4, 2026
5c56d3b
perf(examples): cap Codex reasoning at 'low' in policy demo
zredlined May 4, 2026
6b0a52b
fix(sandbox): harden policy.local denials endpoint
zredlined May 4, 2026
33661ce
fix(examples): redact tokens in agent log tail and validate DEMO_FILE…
zredlined May 4, 2026
6b2656c
refactor(sandbox): centralize policy.local routes and skill path
zredlined May 4, 2026
aaa8a84
feat(sandbox): switch /v1/denials to shorthand log pass-through
zredlined May 6, 2026
f48f4c8
chore(sandbox): align proto inits with main's L7 GraphQL additions
zredlined May 7, 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
108 changes: 97 additions & 11 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5220,17 +5220,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 @@ -5250,10 +5290,10 @@ fn format_timestamp_ms(ms: i64) -> String {
#[cfg(test)]
mod tests {
use super::{
TlsOptions, dockerfile_sources_supported_for_gateway, format_gateway_select_header,
format_gateway_select_items, gateway_add, gateway_auth_label, gateway_env_override_warning,
gateway_select_with, gateway_type_label, git_sync_files, http_health_check,
image_requests_gpu, inferred_provider_type, parse_cli_setting_value,
GatewayControlTarget, TlsOptions, dockerfile_sources_supported_for_gateway, format_endpoint,
format_gateway_select_header, format_gateway_select_items, gateway_add, gateway_auth_label,
gateway_env_override_warning, 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, sandbox_should_persist,
};
Expand Down Expand Up @@ -5969,4 +6009,50 @@ mod tests {
server.join().expect("server thread");
assert_eq!(status, Some(StatusCode::OK));
}
#[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