chore: merge updated configuration snapshot base

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
This commit is contained in:
Piotr Mlocek
2026-09-16 11:35:35 -07:00
42 changed files with 2444 additions and 1043 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ inputs:
mise-version:
description: mise release to install
required: false
default: v2026.4.25
default: v2026.9.9
runs:
using: composite
+1 -1
View File
@@ -8,7 +8,7 @@ inputs:
mise-version:
description: mise release to install
required: false
default: v2026.4.25
default: v2026.9.9
podman-major:
description: Expected Podman major version
required: true
+1 -1
View File
@@ -8,7 +8,7 @@ inputs:
mise-version:
description: mise release to install
required: false
default: v2026.4.25
default: v2026.9.9
rust-cache-key:
description: Shared key for Rust test artifacts
required: false
+1 -1
View File
@@ -8,7 +8,7 @@ inputs:
version:
description: mise release to install
required: false
default: v2026.4.25
default: v2026.9.9
runs:
using: composite
+1 -1
View File
@@ -56,7 +56,7 @@ on:
description: "mise version to install on the bare Kubernetes e2e runner"
required: false
type: string
default: "v2026.4.25"
default: "v2026.9.9"
gateway-artifact:
description: "Optional prebuilt gateway artifact used to compose a local test image"
required: false
+1
View File
@@ -44,6 +44,7 @@ These pipelines connect skills into end-to-end workflows. Individual skill files
| `crates/openshell-isolation-interface/` | Isolation backend interface | RFC 0012 `IsolationBackend` trait and types; the supervisor-facing runtime contract |
| `crates/openshell-sandbox-backend/` | OpenShell sandbox backend | `OpenShellRuntimeBackend` and the authenticated OpenShell Sandbox Protocol shared with `openshell-sandbox` |
| `crates/openshell-policy/` | Policy engine | Filesystem, network, and process constraints |
| `crates/openshell-policy-schema/` | Authored policy schema | Dependency-light YAML/JSON representation, bounded parsing, and pure authored-language semantics |
| `crates/openshell-bootstrap/` | Gateway metadata | Gateway registration metadata, auth token storage, mTLS bundle storage |
| `crates/openshell-gateway-interceptors/` | Gateway interceptors | Intercepts and transforms configured gRPC requests at the gateway routing boundary |
| `crates/openshell-ocsf/` | OCSF logging | OCSF v1.8.0 event types, builders, shorthand/JSONL formatters, tracing layers |
+1
View File
@@ -458,6 +458,7 @@ These are the primary `mise` tasks for day-to-day development:
| Path | Purpose |
| --------------- | --------------------------------------------- |
| `crates/` | Rust crates |
| `crates/openshell-policy-schema/` | Canonical authored policy DTOs and bounded YAML/JSON parser |
| `python/` | Python SDK and bindings |
| `sdk/go/` | Go SDK (types, gRPC clients, converters) |
| `sdk/typescript/` | TypeScript SDK (Connect client and generated protobuf bindings) |
Generated
+14 -1
View File
@@ -3988,6 +3988,7 @@ dependencies = [
"openshell-policy",
"openshell-providers",
"openshell-sdk",
"openshell-server",
"openshell-tui",
"owo-colors",
"prost-types",
@@ -4047,6 +4048,7 @@ dependencies = [
"miette",
"nix 0.29.0",
"openshell-extension-core",
"openshell-policy-schema",
"prost",
"prost-types",
"protoc-bin-vendored",
@@ -4445,13 +4447,23 @@ version = "0.0.0"
dependencies = [
"hickory-proto",
"miette",
"noyalib",
"openshell-core",
"openshell-policy-schema",
"prost-types",
"serde",
"serde_json",
]
[[package]]
name = "openshell-policy-schema"
version = "0.0.0"
dependencies = [
"miette",
"noyalib",
"serde",
"serde_json",
]
[[package]]
name = "openshell-prover"
version = "0.0.0"
@@ -4460,6 +4472,7 @@ dependencies = [
"include_dir",
"miette",
"noyalib",
"openshell-policy-schema",
"owo-colors",
"serde",
"z3",
+19
View File
@@ -21,6 +21,25 @@ For the field-by-field YAML reference, use
Filesystem and process policy are startup-time controls. Network policy is
dynamic and can be hot-reloaded when the new policy validates successfully.
### Authored policy boundary
`openshell-policy-schema` is the sole owner of the authored YAML and JSON
representation. It preserves authored distinctions such as an absent
`filesystem_policy` versus an explicitly empty object, rejects duplicate keys,
and applies parser budgets while noyalib constructs the document. It also owns
pure language semantics such as access presets, MCP revision vocabulary,
effective ports and rule names, protocol classification, and lexical policy
path normalization.
Consumers project that syntax into purpose-specific models. `openshell-policy`
owns protobuf conversion, composition, merge behavior, raw-protobuf checks, and
validation that depends on runtime components. The existing prover retains its
risk model but uses the same fail-closed parser as the runtime. The parser
requires `version: 1` and rejects managed annotations and every unknown field
before any consumer-specific projection runs. There is no permissive parsing
profile: unsupported policy fields always invalidate the document. Middleware `config`, query and persisted-query names, and recursive MCP
parameter names are open user-data maps rather than schema extensions.
Before applying Landlock, the supervisor enriches baseline filesystem paths that
the runtime needs. Missing baseline paths are skipped so one absent runtime path
does not weaken the whole ruleset. When GPU devices are present, GPU baseline
+1
View File
@@ -91,6 +91,7 @@ nix = { workspace = true }
[dev-dependencies]
futures = { workspace = true }
openshell-server = { path = "../openshell-server", features = ["test-support"] }
rcgen = { workspace = true }
reqwest = { workspace = true }
serde_json = { workspace = true }
+39 -6
View File
@@ -1477,11 +1477,6 @@ pub async fn sandbox_sync_command(
Ok(())
}
/// Fetch a sandbox by name.
///
/// 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,
@@ -1490,6 +1485,44 @@ pub async fn sandbox_get(
workspace: &str,
tls: &TlsOptions,
) -> Result<()> {
let mut stdout = Vec::new();
sandbox_get_to_writer(
server,
name,
policy_only,
output,
workspace,
tls,
&mut stdout,
)
.await?;
if !stdout.is_empty() {
std::io::stdout()
.lock()
.write_all(&stdout)
.into_diagnostic()?;
}
Ok(())
}
/// Fetch a sandbox by name.
///
/// 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.
#[doc(hidden)]
pub async fn sandbox_get_to_writer<W>(
server: &str,
name: &str,
policy_only: bool,
output: &str,
workspace: &str,
tls: &TlsOptions,
stdout: &mut W,
) -> Result<()>
where
W: Write + Send,
{
let mut client = grpc_client(server, tls).await?;
let response = client
@@ -1524,7 +1557,7 @@ pub async fn sandbox_get(
};
let yaml_str = openshell_policy::serialize_sandbox_policy(policy)
.wrap_err("failed to serialize policy to YAML")?;
print!("{yaml_str}");
stdout.write_all(yaml_str.as_bytes()).into_diagnostic()?;
return Ok(());
}
@@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
mod helpers;
use helpers::{build_ca, build_client_cert, build_server_cert};
use openshell_cli::run;
use openshell_cli::tls::{TlsOptions, grpc_client};
use openshell_core::proto::open_shell_server::OpenShellServer;
use openshell_core::proto::{CreateSandboxRequest, SandboxSpec};
use openshell_server::test_support::{authenticate_as_dev_user, gateway_service_with_driver};
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio_stream::wrappers::TcpListenerStream;
use tonic::transport::{Certificate as TlsCertificate, Identity, Server, ServerTlsConfig};
struct TestServer {
endpoint: String,
tls: TlsOptions,
_dir: TempDir,
}
async fn run_server() -> TestServer {
let (ca, ca_key) = build_ca();
let (server_cert, server_key) = build_server_cert(&ca, &ca_key);
let (client_cert, client_key) = build_client_cert(&ca, &ca_key);
let ca_cert = ca.pem();
let tls_config = ServerTlsConfig::new()
.identity(Identity::from_pem(server_cert, server_key))
.client_ca_root(TlsCertificate::from_pem(ca_cert.clone()));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let service = gateway_service_with_driver("docker").await;
tokio::spawn(async move {
Server::builder()
.tls_config(tls_config)
.unwrap()
.add_service(OpenShellServer::with_interceptor(
service,
authenticate_as_dev_user,
))
.serve_with_incoming(TcpListenerStream::new(listener))
.await
.unwrap();
});
let dir = tempfile::tempdir().unwrap();
let ca_path = dir.path().join("ca.crt");
let cert_path = dir.path().join("tls.crt");
let key_path = dir.path().join("tls.key");
std::fs::write(&ca_path, ca_cert).unwrap();
std::fs::write(&cert_path, client_cert).unwrap();
std::fs::write(&key_path, client_key).unwrap();
TestServer {
endpoint: format!("https://localhost:{}", addr.port()),
tls: TlsOptions::new(Some(ca_path), Some(cert_path), Some(key_path)),
_dir: dir,
}
}
#[tokio::test]
async fn policy_only_preserves_all_process_identity_combinations_through_gateway() {
let server = run_server().await;
let cases = [
("neither", "process: {}", None),
(
"user-only",
"process:\n run_as_user: \"1500\"",
Some(("1500", "")),
),
(
"group-only",
"process:\n run_as_group: \"1600\"",
Some(("", "1600")),
),
(
"both",
"process:\n run_as_user: \"1500\"\n run_as_group: \"1600\"",
Some(("1500", "1600")),
),
];
for (name, process_yaml, expected) in cases {
let authored = format!("version: 1\n{process_yaml}\n");
let policy = openshell_policy::parse_sandbox_policy(&authored)
.unwrap_or_else(|error| panic!("{name}: authored policy should parse: {error}"));
let mut client = grpc_client(&server.endpoint, &server.tls).await.unwrap();
client
.create_sandbox(CreateSandboxRequest {
name: name.to_string(),
spec: Some(SandboxSpec {
policy: Some(policy),
..Default::default()
}),
workspace_scope: Some(openshell_core::proto::workspace_selector("default")),
..Default::default()
})
.await
.unwrap_or_else(|error| panic!("{name}: gateway create should succeed: {error}"));
let mut output = Vec::new();
run::sandbox_get_to_writer(
&server.endpoint,
name,
true,
"table",
"default",
&server.tls,
&mut output,
)
.await
.unwrap_or_else(|error| panic!("{name}: --policy-only should succeed: {error}"));
let output = String::from_utf8(output).unwrap();
let exported = openshell_policy::parse_sandbox_policy(&output)
.unwrap_or_else(|error| panic!("{name}: exported YAML should parse: {error}"));
match expected {
None => {
assert!(
exported.process.is_none(),
"{name}: empty process must be omitted"
);
assert!(!output.contains("process:"), "{name}: {output}");
}
Some((user, group)) => {
let process = exported
.process
.expect("partial process should be retained");
assert_eq!(process.run_as_user, user, "{name}: {output}");
assert_eq!(process.run_as_group, group, "{name}: {output}");
}
}
}
}
@@ -193,7 +193,7 @@ impl OpenShell for TestOpenShell {
);
Ok(Response::new(GetSandboxConfigResponse {
policy: Some(SandboxPolicy {
version: 9,
version: 1,
network_policies: [
(
"user_api".to_string(),
@@ -464,7 +464,7 @@ impl OpenShell for TestOpenShell {
assert!(!req.global);
let policy = SandboxPolicy {
version: 7,
version: 1,
network_policies: std::iter::once((
"api".to_string(),
NetworkPolicyRule {
+1
View File
@@ -13,6 +13,7 @@ repository.workspace = true
[dependencies]
async-trait = "0.1"
openshell-extension-core = { path = "../openshell-extension-core" }
openshell-policy-schema = { path = "../openshell-policy-schema" }
glob = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }
+8 -145
View File
@@ -4,15 +4,13 @@
//! `OpenShell`-owned MCP protocol revisions and immutable batch-shape metadata.
use std::collections::BTreeSet;
use std::fmt;
use std::str::FromStr;
use crate::proto::{McpOptions, ProviderProfile};
/// Fixed `OpenShell` batch-member bound for the MCP `2025-03-26` wire profile.
///
/// The bound is distinct from the separately enforced request-body byte limit.
pub const MAX_MCP_LEGACY_BATCH_MESSAGES: usize = 64;
pub use openshell_policy_schema::{
DEFAULT_MCP_PROTOCOL_VERSION, MAX_MCP_LEGACY_BATCH_MESSAGES, McpProtocolVersion,
McpWireProfile, ParseMcpProtocolVersionError,
};
/// Return whether a policy protocol name denotes MCP.
///
@@ -23,28 +21,6 @@ pub fn is_mcp_protocol(protocol: &str) -> bool {
protocol.trim().eq_ignore_ascii_case("mcp")
}
/// Stable MCP protocol revisions accepted by `OpenShell` policy configuration.
///
/// Variant order is the canonical semantic order used when normalizing policy
/// allowlists.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum McpProtocolVersion {
/// MCP protocol revision `2025-03-26`.
V2025_03_26,
/// MCP protocol revision `2025-06-18`.
V2025_06_18,
/// MCP protocol revision `2025-11-25`.
V2025_11_25,
}
/// Pinned MCP protocol revision used when policy authoring omits a revision.
///
/// This value is assigned directly rather than derived from
/// [`McpProtocolVersion::ALL`] or its order. Adding support for a newer
/// revision therefore cannot silently change versionless policies.
pub const DEFAULT_MCP_PROTOCOL_VERSION: McpProtocolVersion = McpProtocolVersion::V2025_11_25;
/// Normalize only the MCP option fields in a provider profile.
///
/// MCP protocol matching is case-insensitive. Missing or empty version state
@@ -94,116 +70,6 @@ pub fn normalize_provider_profile_mcp_fields(profile: &mut ProviderProfile) {
}
}
impl McpProtocolVersion {
/// Every supported MCP protocol revision in canonical semantic order.
pub const ALL: &'static [Self] = &[Self::V2025_03_26, Self::V2025_06_18, Self::V2025_11_25];
/// Return the exact protocol identifier used on the MCP wire.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::V2025_03_26 => "2025-03-26",
Self::V2025_06_18 => "2025-06-18",
Self::V2025_11_25 => "2025-11-25",
}
}
/// Return the fixed batch-shape metadata for this protocol revision.
#[must_use]
pub const fn wire_profile(self) -> McpWireProfile {
match self {
Self::V2025_03_26 => McpWireProfile {
version: self,
allows_json_rpc_batches: true,
max_batch_messages: Some(MAX_MCP_LEGACY_BATCH_MESSAGES),
},
Self::V2025_06_18 | Self::V2025_11_25 => McpWireProfile {
version: self,
allows_json_rpc_batches: false,
max_batch_messages: None,
},
}
}
}
impl fmt::Display for McpProtocolVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for McpProtocolVersion {
type Err = ParseMcpProtocolVersionError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"2025-03-26" => Ok(Self::V2025_03_26),
"2025-06-18" => Ok(Self::V2025_06_18),
"2025-11-25" => Ok(Self::V2025_11_25),
_ => Err(ParseMcpProtocolVersionError {
value: value.to_string(),
}),
}
}
}
/// Error returned when a string is not an exact supported MCP revision.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseMcpProtocolVersionError {
value: String,
}
impl ParseMcpProtocolVersionError {
/// Return the exact input that failed MCP revision parsing.
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
impl fmt::Display for ParseMcpProtocolVersionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"unsupported MCP protocol version '{}'",
self.value
)
}
}
impl std::error::Error for ParseMcpProtocolVersionError {}
/// Immutable batch-shape metadata for one exact MCP protocol revision.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct McpWireProfile {
version: McpProtocolVersion,
allows_json_rpc_batches: bool,
max_batch_messages: Option<usize>,
}
impl McpWireProfile {
/// Return the protocol revision governed by this profile.
#[must_use]
pub const fn version(self) -> McpProtocolVersion {
self.version
}
/// Return whether the revision permits a top-level JSON-RPC batch array.
#[must_use]
pub const fn allows_json_rpc_batches(self) -> bool {
self.allows_json_rpc_batches
}
/// Return the maximum batch members when batching is permitted.
///
/// `None` means the profile forbids batches; it never represents an
/// unbounded batch.
#[must_use]
pub const fn max_batch_messages(self) -> Option<usize> {
self.max_batch_messages
}
}
#[cfg(test)]
mod tests {
use prost::Message;
@@ -394,13 +260,10 @@ mod tests {
"draft",
"latest",
] {
let error = unsupported.parse::<McpProtocolVersion>();
assert_eq!(
error,
Err(ParseMcpProtocolVersionError {
value: unsupported.to_string(),
})
);
let error = unsupported
.parse::<McpProtocolVersion>()
.expect_err("unsupported MCP revision must be rejected");
assert_eq!(error.value(), unsupported);
}
}
+3 -35
View File
@@ -140,41 +140,9 @@ pub fn is_file_permissions_too_open(path: &Path) -> bool {
std::fs::metadata(path).is_ok_and(|m| m.permissions().mode() & 0o077 != 0)
}
/// Normalize a filesystem path by collapsing redundant separators
/// and removing trailing slashes, without requiring the path to exist on disk.
///
/// This is a lexical normalization only — it does NOT resolve symlinks or
/// check the filesystem. `..` components are preserved verbatim; callers that
/// need to reject parent traversal must validate separately. The normalized
/// representation always uses `/` so sandbox policy paths are host-independent.
pub fn normalize_path(path: &str) -> String {
use std::path::Component;
let p = Path::new(path);
let mut normalized = PathBuf::new();
for component in p.components() {
match component {
Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
#[allow(clippy::path_buf_push_overwrite)]
Component::RootDir => normalized.push("/"),
Component::CurDir => {} // skip "."
Component::ParentDir => {
// Keep ".." — validation will catch it separately
normalized.push("..");
}
Component::Normal(c) => normalized.push(c),
}
}
let normalized = normalized.to_string_lossy();
#[cfg(target_os = "windows")]
{
normalized.replace('\\', "/")
}
#[cfg(not(target_os = "windows"))]
{
normalized.into_owned()
}
}
/// Compatibility re-export; authored policy path normalization is owned by
/// `openshell-policy-schema`.
pub use openshell_policy_schema::normalize_path;
#[cfg(test)]
mod tests {
+20
View File
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
[package]
name = "openshell-policy-schema"
description = "Canonical authored policy schema for OpenShell"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
miette = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yml = { workspace = true }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,9 +13,9 @@ repository.workspace = true
[dependencies]
hickory-proto = { workspace = true }
openshell-core = { path = "../openshell-core", default-features = false }
openshell-policy-schema = { path = "../openshell-policy-schema" }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yml = { workspace = true }
miette = { workspace = true }
prost-types = { workspace = true }
+232 -431
View File
@@ -5,9 +5,9 @@
//!
//! Provides bidirectional YAML↔proto conversion for sandbox policies.
//!
//! The serde types here are the **single canonical representation** of the YAML
//! policy schema. Both parsing (YAML→proto) and serialization (proto→YAML) use
//! these types, ensuring round-trip fidelity.
//! The canonical authored representation and bounded parser live in
//! `openshell-policy-schema`; this crate adapts that representation to the
//! runtime protobuf model and owns runtime-dependent validation.
mod compose;
mod l7_validate;
@@ -31,7 +31,6 @@ use openshell_core::proto::{
LandlockPolicy, McpOptions, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, ProcessPolicy,
SandboxPolicy,
};
use serde::{Deserialize, Deserializer, Serialize};
pub use compose::{
PROVIDER_RULE_NAME_PREFIX, ProviderPolicyLayer, compose_effective_policy,
@@ -49,238 +48,24 @@ pub use middleware::middleware_host_matches;
pub use middleware::validate_json as validate_network_middleware_json;
pub use middleware::validate_json_with_config as validate_network_middleware_json_with_config;
// ---------------------------------------------------------------------------
// YAML serde types (canonical — used for both parsing and serialization)
// ---------------------------------------------------------------------------
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct PolicyFile {
version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
filesystem_policy: Option<FilesystemDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
landlock: Option<LandlockDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
process: Option<ProcessDef>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
network_policies: BTreeMap<String, NetworkPolicyRuleDef>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
network_middlewares: BTreeMap<String, middleware::NetworkMiddlewareConfigDef>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct FilesystemDef {
#[serde(default)]
include_workdir: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
read_only: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
read_write: Vec<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum LandlockCompatibilityDef {
#[default]
BestEffort,
HardRequirement,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct LandlockDef {
#[serde(default)]
compatibility: LandlockCompatibilityDef,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ProcessDef {
#[serde(default, skip_serializing_if = "String::is_empty")]
run_as_user: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
run_as_group: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct NetworkPolicyRuleDef {
#[serde(default, skip_serializing_if = "String::is_empty")]
name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
endpoints: Vec<NetworkEndpointDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
binaries: Vec<NetworkBinaryDef>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[allow(
clippy::struct_excessive_bools,
reason = "Endpoint DTO mirrors independent policy schema toggles."
)]
struct NetworkEndpointDef {
#[serde(default, skip_serializing_if = "String::is_empty")]
host: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
path: String,
/// Single port (backwards compat). Mutually exclusive with `ports`.
/// Uses `u16` to reject invalid values >65535 at parse time.
#[serde(default, skip_serializing_if = "is_zero")]
port: u16,
/// Multiple ports. When non-empty, this endpoint covers all listed ports.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
ports: Vec<u16>,
#[serde(default, skip_serializing_if = "String::is_empty")]
protocol: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
tls: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
enforcement: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
access: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
rules: Vec<L7RuleDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
allowed_ips: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
deny_rules: Vec<L7DenyRuleDef>,
/// When true, percent-encoded `/` (`%2F`) is preserved in path segments
/// rather than rejected by the L7 path canonicalizer. Required for
/// upstreams like GitLab that embed `%2F` in namespaced resource paths.
/// Defaults to false (strict).
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
allow_encoded_slash: bool,
/// When true, client-to-server WebSocket text messages on this REST
/// endpoint rewrite credential placeholders after an allowed 101 upgrade.
/// Defaults to false.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
websocket_credential_rewrite: bool,
/// When true, supported textual REST request bodies rewrite credential
/// placeholders before forwarding upstream. Defaults to false.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
request_body_credential_rewrite: bool,
/// Explicitly permits credentials on traffic paths that `OpenShell` cannot
/// inspect or rewrite. Defaults to false.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
allow_uninspected_credentials: bool,
#[serde(default, skip_serializing_if = "String::is_empty")]
persisted_queries: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
graphql_persisted_queries: BTreeMap<String, GraphqlOperationDef>,
#[serde(default, skip_serializing_if = "is_zero_u32")]
graphql_max_body_bytes: u32,
#[serde(default, skip_serializing_if = "String::is_empty")]
credential_signing: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
signing_service: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
signing_region: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
credential_binding: Option<NetworkCredentialBindingDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
json_rpc: Option<JsonRpcConfigDef>,
#[serde(
default,
deserialize_with = "deserialize_non_null_optional_field",
skip_serializing_if = "Option::is_none"
)]
mcp: Option<McpConfigDef>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct NetworkCredentialBindingDef {
provider: String,
}
// Signature dictated by serde's `skip_serializing_if`, which requires `&T`.
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_zero(v: &u16) -> bool {
*v == 0
}
// Signature dictated by serde's `skip_serializing_if`, which requires `&T`.
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_zero_u32(v: &u32) -> bool {
*v == 0
}
fn deserialize_non_null_optional_field<'de, D, T>(
deserializer: D,
) -> std::result::Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
// Serde skips this function when the field is absent because the field has
// `default`. When it is present, deserialize `T` directly so an explicit
// YAML or JSON null is rejected instead of collapsing into omission.
T::deserialize(deserializer).map(Some)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct JsonRpcConfigDef {
#[serde(default, skip_serializing_if = "is_zero_u32")]
max_body_bytes: u32,
}
// The authored serde tree lives in `openshell-policy-schema`. These local
// aliases keep the protobuf adapter readable while exposing consumer-facing
// names from the schema crate.
use openshell_policy_schema::{
AnyMatcher as QueryAnyDef, FilesystemPolicy as FilesystemDef,
GraphqlOperation as GraphqlOperationDef, JsonRpcConfig as JsonRpcConfigDef,
L7Allow as L7AllowDef, L7DenyRule as L7DenyRuleDef, L7Rule as L7RuleDef,
LandlockCompatibility as LandlockCompatibilityDef, LandlockPolicy as LandlockDef,
MCP_VERSION_REMEDIATION, McpConfig as McpConfigDef, NetworkBinary as NetworkBinaryDef,
NetworkCredentialBinding as NetworkCredentialBindingDef, NetworkEndpoint as NetworkEndpointDef,
NetworkPolicyRule as NetworkPolicyRuleDef, ParameterMatcher as ParamMatcherDef,
PolicyDocument as PolicyFile, ProcessPolicy as ProcessDef, QueryMatcher as QueryMatcherDef,
};
fn json_rpc_config_from_proto(max_body_bytes: u32) -> Option<JsonRpcConfigDef> {
(max_body_bytes > 0).then_some(JsonRpcConfigDef { max_body_bytes })
}
// MCP rides the same HTTP/JSON-RPC inspection machinery at runtime, but it
// gets its own policy stanza so user-authored YAML can name the primary
// protocol instead of treating MCP as generic JSON-RPC.
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct McpConfigDef {
// Presence is retained until authored-policy validation so an omitted
// allowlist can select the pinned default while an explicit empty list is
// rejected as an authoring mistake.
#[serde(
default,
deserialize_with = "deserialize_non_null_optional_field",
skip_serializing_if = "Option::is_none"
)]
versions: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "is_zero_u32")]
max_body_bytes: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
strict_tool_names: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
allow_all_known_mcp_methods: Option<bool>,
}
const MCP_VERSION_REMEDIATION: &str = "omit mcp.versions to use the pinned default revision, use an exact supported revision, or omit protocol and mcp for deliberate uninspected L4 passthrough only when that weaker boundary is acceptable";
fn validate_authored_mcp_versions(versions: Option<&[String]>, context: &str) -> Result<()> {
let Some(versions) = versions else {
return Ok(());
};
if versions.is_empty() {
return Err(miette::miette!(
"{context} has an empty mcp.versions list; omit mcp.versions to use the pinned default revision"
));
}
let mut seen = BTreeSet::new();
for value in versions {
let version = value
.parse::<McpProtocolVersion>()
.map_err(|error| miette::miette!("{context}: {error}; {MCP_VERSION_REMEDIATION}"))?;
if !seen.insert(version) {
return Err(miette::miette!(
"{context} has duplicate protocol version '{value}'"
));
}
}
Ok(())
}
fn mcp_config_from_proto(max_body_bytes: u32, mcp: Option<&McpOptions>) -> Option<McpConfigDef> {
let mut versions = mcp
.map(|config| config.versions.clone())
@@ -319,22 +104,16 @@ impl L7ConfigStanza {
}
}
/// Parse an L7 nested config stanza and return the flattened runtime fields
/// consumed by the supervisor policy engine.
///
/// The stanza schema stays tied to this crate's canonical serde definitions, so
/// adding a new supported field requires updating this conversion next to the
/// type that parses it. MCP revision fields are validated and materialized
/// before the alias is flattened so both runtime ingress paths receive the
/// same canonical allowlist.
/// Parse an L7 config fragment using the canonical schema and return runtime
/// fields consumed by the supervisor policy engine.
pub fn l7_config_alias_runtime_fields(
stanza: L7ConfigStanza,
value: serde_json::Value,
) -> Result<Vec<(&'static str, serde_json::Value)>> {
match stanza {
L7ConfigStanza::JsonRpc => {
let JsonRpcConfigDef { max_body_bytes } = serde_json::from_value(value)
.map_err(|error| miette::miette!("invalid json_rpc config: {error}"))?;
let JsonRpcConfigDef { max_body_bytes } =
openshell_policy_schema::parse_json_rpc_config(value)?;
let mut fields = Vec::new();
if max_body_bytes > 0 {
fields.push(("json_rpc_max_body_bytes", serde_json::json!(max_body_bytes)));
@@ -342,9 +121,7 @@ pub fn l7_config_alias_runtime_fields(
Ok(fields)
}
L7ConfigStanza::Mcp => {
let config: McpConfigDef = serde_json::from_value(value)
.map_err(|error| miette::miette!("invalid mcp config: {error}"))?;
validate_authored_mcp_versions(config.versions.as_deref(), "invalid mcp config")?;
let config = openshell_policy_schema::parse_mcp_config(value)?;
let McpConfigDef {
versions,
max_body_bytes,
@@ -353,126 +130,20 @@ pub fn l7_config_alias_runtime_fields(
} = config;
let mut versions = versions.unwrap_or_else(default_mcp_versions);
canonicalize_mcp_versions(&mut versions);
let mut fields = Vec::new();
fields.push(("mcp_versions", serde_json::json!(versions)));
let mut fields = vec![("mcp_versions", serde_json::json!(versions))];
if max_body_bytes > 0 {
fields.push(("json_rpc_max_body_bytes", serde_json::json!(max_body_bytes)));
}
if let Some(strict_tool_names) = strict_tool_names {
fields.push((
"mcp_strict_tool_names",
serde_json::json!(strict_tool_names),
));
if let Some(value) = strict_tool_names {
fields.push(("mcp_strict_tool_names", serde_json::json!(value)));
}
if let Some(allow_all_known_mcp_methods) = allow_all_known_mcp_methods {
fields.push((
"mcp_allow_all_known_mcp_methods",
serde_json::json!(allow_all_known_mcp_methods),
));
if let Some(value) = allow_all_known_mcp_methods {
fields.push(("mcp_allow_all_known_mcp_methods", serde_json::json!(value)));
}
Ok(fields)
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct GraphqlOperationDef {
#[serde(default, skip_serializing_if = "String::is_empty")]
operation_type: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
operation_name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
fields: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct L7RuleDef {
allow: L7AllowDef,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct L7AllowDef {
#[serde(default, skip_serializing_if = "String::is_empty")]
method: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
path: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
command: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
query: BTreeMap<String, QueryMatcherDef>,
#[serde(default, skip_serializing_if = "String::is_empty")]
operation_type: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
operation_name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
fields: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tool: Option<QueryMatcherDef>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
params: BTreeMap<String, ParamMatcherDef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum QueryMatcherDef {
// Short form: `query: { repo: "NVIDIA/*" }`.
Glob(String),
// Expanded form: `query: { repo: { any: ["NVIDIA/*", "openai/*"] } }`.
Any(QueryAnyDef),
}
// MCP params can be authored as nested maps in YAML, but the runtime matcher
// map remains flat so the Rego policy can share query-param matching.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum ParamMatcherDef {
Matcher(QueryMatcherDef),
Object(BTreeMap<String, Self>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct QueryAnyDef {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
any: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct L7DenyRuleDef {
#[serde(default, skip_serializing_if = "String::is_empty")]
method: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
path: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
command: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
query: BTreeMap<String, QueryMatcherDef>,
#[serde(default, skip_serializing_if = "String::is_empty")]
operation_type: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
operation_name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
fields: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tool: Option<QueryMatcherDef>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
params: BTreeMap<String, ParamMatcherDef>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct NetworkBinaryDef {
path: String,
}
// ---------------------------------------------------------------------------
// YAML → proto conversion
// ---------------------------------------------------------------------------
fn matcher_def_to_proto(matcher: QueryMatcherDef) -> L7QueryMatcher {
match matcher {
QueryMatcherDef::Glob(glob) => L7QueryMatcher { glob, any: vec![] },
@@ -974,21 +645,37 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
let network_policies = policy
.network_policies
.iter()
.map(|(key, rule)| {
.map(|(key, rule)| -> Result<_> {
let yaml_rule = NetworkPolicyRuleDef {
name: rule.name.clone(),
endpoints: rule
.endpoints
.iter()
.map(|e| {
.map(|e| -> Result<_> {
// Use compact form: if ports has exactly 1 element,
// emit port (scalar). If >1, emit ports (array).
// Proto uses u32; YAML uses u16. Clamp at boundary.
let clamp = |v: u32| -> u16 { v.min(65535) as u16 };
// Proto uses u32; authored ports are u16. Reject an
// invalid protobuf value instead of silently clamping.
let checked = |value: u32| {
u16::try_from(value).map_err(|_| {
miette::miette!(
"cannot serialize endpoint '{}': port {value} exceeds {}",
e.host,
u16::MAX
)
})
};
let (port, ports) = if e.ports.len() > 1 {
(0, e.ports.iter().map(|&p| clamp(p)).collect())
(
0,
e.ports
.iter()
.copied()
.map(checked)
.collect::<Result<Vec<_>>>()?,
)
} else {
(clamp(e.ports.first().copied().unwrap_or(e.port)), vec![])
(checked(e.ports.first().copied().unwrap_or(e.port))?, vec![])
};
let protocol = e.protocol.clone();
let mcp_allow_all_known_mcp_methods = !is_mcp_protocol(&protocol)
@@ -1022,7 +709,7 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
} else {
(json_rpc_config_from_proto(e.json_rpc_max_body_bytes), None)
};
NetworkEndpointDef {
Ok(NetworkEndpointDef {
host: e.host.clone(),
path: e.path.clone(),
port,
@@ -1064,9 +751,9 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
}),
json_rpc,
mcp,
}
})
})
.collect(),
.collect::<Result<Vec<_>>>()?,
binaries: rule
.binaries
.iter()
@@ -1075,14 +762,21 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
})
.collect(),
};
(key.clone(), yaml_rule)
Ok((key.clone(), yaml_rule))
})
.collect();
.collect::<Result<BTreeMap<_, _>>>()?;
let network_middlewares = middleware::from_proto(&policy.network_middlewares);
Ok(PolicyFile {
version: policy.version,
// Proto3 scalar fields do not preserve presence. Treat zero as an
// omitted authored version and materialize the only supported schema
// version in canonical output.
version: if policy.version == 0 {
1
} else {
policy.version
},
filesystem_policy,
landlock,
process,
@@ -1143,43 +837,18 @@ pub fn is_valid_sandbox_identity(value: &str) -> bool {
// Validate raw authored values and their relationship to the endpoint protocol
// before conversion. Keeping validation outside the Serde error wrapper makes
// actionable MCP diagnostics the top-level user-facing error.
fn validate_mcp_version_schema(policy: &PolicyFile) -> Result<()> {
for (policy_key, rule) in &policy.network_policies {
let policy_name = if rule.name.is_empty() {
policy_key
} else {
&rule.name
};
for endpoint in &rule.endpoints {
if is_mcp_protocol(&endpoint.protocol) {
let context = format!(
"network policy '{policy_name}': MCP endpoint '{}'",
endpoint.host
);
validate_authored_mcp_versions(
endpoint
.mcp
.as_ref()
.and_then(|config| config.versions.as_deref()),
&context,
)?;
} else if endpoint.mcp.is_some() {
return Err(miette::miette!(
"network policy '{policy_name}': non-MCP endpoint '{}' cannot configure mcp options",
endpoint.host
));
}
}
}
Ok(())
}
/// Parse a sandbox policy from a YAML string.
pub fn parse_sandbox_policy(yaml: &str) -> Result<SandboxPolicy> {
let raw: PolicyFile = serde_yml::from_str(yaml)
.into_diagnostic()
.wrap_err("failed to parse sandbox policy YAML")?;
validate_mcp_version_schema(&raw)?;
let raw = openshell_policy_schema::parse_policy(yaml)?;
to_proto(raw)
}
/// Parse a sandbox policy from a regular file using the shared bounded reader.
pub fn parse_sandbox_policy_file(path: &Path) -> Result<SandboxPolicy> {
let raw = openshell_policy_schema::parse_policy_file(
path,
openshell_policy_schema::ParseLimits::default(),
)?;
to_proto(raw)
}
@@ -1189,12 +858,11 @@ pub fn parse_sandbox_policy(yaml: &str) -> Result<SandboxPolicy> {
/// canonical YAML field names (e.g. `filesystem_policy`, not `filesystem`)
/// and is round-trippable through `parse_sandbox_policy`.
pub fn serialize_sandbox_policy(policy: &SandboxPolicy) -> Result<String> {
validate_proto_version_for_authored_serialization(policy)?;
let canonical = validate_and_canonicalize_mcp_policy_schema(policy.clone())
.map_err(|error| miette::miette!("cannot serialize invalid sandbox policy: {error}"))?;
let yaml_repr = from_proto(&canonical)?;
serde_yml::to_string(&yaml_repr)
.into_diagnostic()
.wrap_err("failed to serialize policy to YAML")
openshell_policy_schema::serialize_policy(&yaml_repr)
}
/// Convert a proto sandbox policy into the canonical policy JSON representation.
@@ -1202,12 +870,21 @@ pub fn serialize_sandbox_policy(policy: &SandboxPolicy) -> Result<String> {
/// The shape mirrors the YAML schema used by [`serialize_sandbox_policy`], so
/// automation can use the same documented field names in either format.
pub fn sandbox_policy_to_json_value(policy: &SandboxPolicy) -> Result<serde_json::Value> {
validate_proto_version_for_authored_serialization(policy)?;
let canonical = validate_and_canonicalize_mcp_policy_schema(policy.clone())
.map_err(|error| miette::miette!("cannot serialize invalid sandbox policy: {error}"))?;
let json_repr = from_proto(&canonical)?;
serde_json::to_value(&json_repr)
.into_diagnostic()
.wrap_err("failed to serialize policy to JSON")
openshell_policy_schema::policy_to_json_value(&json_repr)
}
fn validate_proto_version_for_authored_serialization(policy: &SandboxPolicy) -> Result<()> {
if !matches!(policy.version, 0 | 1) {
miette::bail!(
"cannot serialize unsupported protobuf policy version {}; expected 0 (omitted) or 1",
policy.version
);
}
Ok(())
}
/// Serialize a proto sandbox policy to a pretty-printed JSON string.
@@ -1228,20 +905,14 @@ pub fn serialize_sandbox_policy_json(policy: &SandboxPolicy) -> Result<String> {
/// caller to omit the policy and let the server / sandbox apply its own
/// default.
pub fn load_sandbox_policy(cli_path: Option<&str>) -> Result<Option<SandboxPolicy>> {
let contents = if let Some(p) = cli_path {
let path = Path::new(p);
std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read sandbox policy from {}", path.display()))?
let policy = if let Some(p) = cli_path {
parse_sandbox_policy_file(Path::new(p))?
} else if let Ok(policy_path) = std::env::var("OPENSHELL_SANDBOX_POLICY") {
let path = Path::new(&policy_path);
std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read sandbox policy from {}", path.display()))?
parse_sandbox_policy_file(Path::new(&policy_path))?
} else {
return Ok(None);
};
parse_sandbox_policy(&contents).map(Some)
Ok(Some(policy))
}
/// Well-known path where a sandbox container image can ship a policy YAML file.
@@ -2212,7 +1883,7 @@ fn truncate_for_display(s: &str) -> String {
///
/// Re-exported from `openshell-core` so existing call sites
/// (`openshell_policy::normalize_path`) keep resolving.
pub use openshell_core::paths::normalize_path;
pub use openshell_policy_schema::normalize_path;
// ---------------------------------------------------------------------------
// Tests
@@ -2446,19 +2117,88 @@ network_policies:
}
#[test]
fn process_identity_omission_survives_yaml_round_trip() {
let policy = parse_sandbox_policy("version: 1\nprocess:\n run_as_user: \"1234\"\n")
.expect("partial process identity should parse");
let process = policy.process.as_ref().expect("process section");
assert_eq!(process.run_as_user, "1234");
assert!(process.run_as_group.is_empty());
assert!(validate_sandbox_policy(&policy).is_ok());
fn canonical_serializers_materialize_omitted_proto_version() {
let policy = SandboxPolicy::default();
let yaml = serialize_sandbox_policy(&policy).expect("partial identity should serialize");
assert!(yaml.contains("run_as_user"));
assert!(!yaml.contains("run_as_group"));
let reparsed = parse_sandbox_policy(&yaml).expect("round trip should parse");
assert!(reparsed.process.unwrap().run_as_group.is_empty());
let yaml = serialize_sandbox_policy(&policy).expect("serialize default protobuf policy");
assert_eq!(
parse_sandbox_policy(&yaml)
.expect("canonical YAML must round trip")
.version,
1
);
let json =
sandbox_policy_to_json_value(&policy).expect("serialize default protobuf policy");
assert_eq!(json["version"], 1);
}
#[test]
fn canonical_serializers_reject_unsupported_proto_version() {
let policy = SandboxPolicy {
version: 2,
..Default::default()
};
let error = serialize_sandbox_policy(&policy)
.expect_err("unsupported protobuf versions must not serialize");
assert!(error.to_string().contains("protobuf policy version 2"));
}
#[test]
fn canonical_serializers_preserve_independent_process_identity_omission() {
let cases = [
("", "", None, None),
("1500", "", Some("1500"), None),
("", "1600", None, Some("1600")),
("1500", "1600", Some("1500"), Some("1600")),
];
for (user, group, expected_user, expected_group) in cases {
let policy = SandboxPolicy {
version: 1,
process: Some(ProcessPolicy {
run_as_user: user.to_owned(),
run_as_group: group.to_owned(),
}),
..Default::default()
};
let yaml = serialize_sandbox_policy(&policy)
.expect("omitted process identity components must serialize to YAML");
assert_eq!(yaml.contains("run_as_user:"), expected_user.is_some());
assert_eq!(yaml.contains("run_as_group:"), expected_group.is_some());
let reparsed = parse_sandbox_policy(&yaml).expect("canonical YAML must round trip");
let reparsed_user = reparsed
.process
.as_ref()
.map(|process| process.run_as_user.as_str())
.filter(|value| !value.is_empty());
let reparsed_group = reparsed
.process
.as_ref()
.map(|process| process.run_as_group.as_str())
.filter(|value| !value.is_empty());
assert_eq!(reparsed_user, expected_user);
assert_eq!(reparsed_group, expected_group);
let json = sandbox_policy_to_json_value(&policy)
.expect("omitted process identity components must serialize to JSON");
let json_process = json.get("process");
assert_eq!(
json_process.and_then(|process| process.get("run_as_user")),
expected_user.map(serde_json::Value::from).as_ref()
);
assert_eq!(
json_process.and_then(|process| process.get("run_as_group")),
expected_group.map(serde_json::Value::from).as_ref()
);
assert_eq!(
json_process.is_some(),
expected_user.is_some() || expected_group.is_some()
);
}
}
#[test]
@@ -4853,6 +4593,45 @@ network_policies:
assert_eq!(proto1, proto2);
}
#[test]
fn round_trip_preserves_any_as_an_mcp_parameter_name() {
let yaml = r#"
version: 1
network_policies:
mcp:
endpoints:
- host: mcp.example.com
port: 443
protocol: mcp
mcp: {}
rules:
- allow:
method: tools/call
params:
arguments:
any: "first"
other: "second"
"#;
let proto = parse_sandbox_policy(yaml).expect("authored policy must parse");
let params = &proto.network_policies["mcp"].endpoints[0].rules[0]
.allow
.as_ref()
.expect("allow rule")
.params;
assert_eq!(params["arguments.any"].glob, "first");
assert_eq!(params["arguments.other"].glob, "second");
let serialized = serialize_sandbox_policy(&proto).expect("protobuf policy must serialize");
assert!(serialized.contains("arguments:"));
assert!(serialized.contains("any: first"));
assert!(serialized.contains("other: second"));
let reparsed =
parse_sandbox_policy(&serialized).expect("serialized protobuf policy must parse again");
assert_eq!(reparsed, proto);
}
#[test]
fn parse_rejects_unsupported_json_rpc_config_fields() {
let yaml = r"
@@ -5034,4 +4813,26 @@ network_policies:
"port >65535 should fail to parse"
);
}
#[test]
fn serialization_rejects_proto_port_above_u16() {
let mut policy = SandboxPolicy {
version: 1,
..Default::default()
};
policy.network_policies.insert(
"too-wide".to_owned(),
NetworkPolicyRule {
endpoints: vec![NetworkEndpoint {
host: "example.com".to_owned(),
port: u32::from(u16::MAX) + 1,
..Default::default()
}],
..Default::default()
},
);
let error =
serialize_sandbox_policy(&policy).expect_err("wide protobuf port must not be clamped");
assert!(error.to_string().contains("exceeds"));
}
}
+3 -10
View File
@@ -2064,21 +2064,14 @@ fn expand_existing_access(
}
fn expand_access_preset(protocol: &str, access: &str) -> Option<Vec<L7Rule>> {
let methods = match (protocol, access) {
(_, "full") => vec!["*"],
("websocket", "read-only") => vec!["GET"],
("websocket", "read-write") => vec!["GET", "WEBSOCKET_TEXT"],
(_, "read-only") => vec!["GET", "HEAD", "OPTIONS"],
(_, "read-write") => vec!["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"],
_ => return None,
};
let methods = openshell_policy_schema::expand_access_preset(protocol, access)?;
Some(
methods
.into_iter()
.iter()
.map(|method| L7Rule {
allow: Some(L7Allow {
method: method.to_string(),
method: (*method).to_string(),
path: "**".to_string(),
command: String::new(),
query: HashMap::default(),
+11 -35
View File
@@ -13,63 +13,39 @@ use openshell_core::proto::{
use openshell_core::proto_struct::{
ProtoStructError, json_object_to_struct, struct_to_json_object,
};
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use super::PolicyViolation;
pub use openshell_core::host_pattern::host_matches as middleware_host_matches;
use openshell_core::host_pattern::{HostPattern, HostSelector};
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkMiddlewareConfigDef {
#[serde(default, skip_serializing_if = "String::is_empty")]
name: String,
middleware: String,
#[serde(default, skip_serializing_if = "is_default")]
order: i32,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
config: BTreeMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "String::is_empty")]
on_error: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
endpoints: Option<MiddlewareEndpointSelectorDef>,
}
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
value == &T::default()
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct MiddlewareEndpointSelectorDef {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
include: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
exclude: Vec<String>,
}
use openshell_policy_schema::{
MiddlewareEndpointSelector as MiddlewareEndpointSelectorDef,
NetworkMiddleware as NetworkMiddlewareConfigDef,
};
/// Middleware-relevant projection of the runtime policy JSON accepted by the
/// supervisor's local-file mode. Unrelated network and L7 fields are ignored;
/// middleware entries retain their strict canonical schema.
#[derive(Debug, Default, Deserialize)]
struct MiddlewareValidationPolicyDef {
struct SupervisorRuntimePolicyJson {
#[serde(default)]
network_middlewares: BTreeMap<String, NetworkMiddlewareConfigDef>,
#[serde(default)]
network_policies: BTreeMap<String, MiddlewareValidationNetworkPolicyDef>,
network_policies: BTreeMap<String, SupervisorRuntimeNetworkPolicyJson>,
}
#[derive(Debug, Default, Deserialize)]
struct MiddlewareValidationNetworkPolicyDef {
struct SupervisorRuntimeNetworkPolicyJson {
#[serde(default)]
name: String,
#[serde(default)]
endpoints: Vec<MiddlewareValidationEndpointDef>,
endpoints: Vec<SupervisorRuntimeEndpointJson>,
}
#[derive(Debug, Default, Deserialize)]
struct MiddlewareValidationEndpointDef {
struct SupervisorRuntimeEndpointJson {
#[serde(default)]
host: String,
#[serde(default)]
@@ -155,7 +131,7 @@ pub fn validate_json_with_config<F>(
where
F: Fn(&str, &prost_types::Struct) -> Result<(), String>,
{
let definition: MiddlewareValidationPolicyDef = serde_json::from_value(data.clone())
let definition: SupervisorRuntimePolicyJson = serde_json::from_value(data.clone())
.map_err(|error| format!("failed to parse network middleware policy: {error}"))?;
let network_middlewares = into_proto(definition.network_middlewares)
.map_err(|error| format!("failed to convert network middleware config: {error}"))?;
+1
View File
@@ -16,6 +16,7 @@ prebuilt-z3 = ["z3/gh-release"]
[dependencies]
z3 = { workspace = true }
openshell-policy-schema = { path = "../openshell-policy-schema" }
serde = { workspace = true }
serde_yml = { workspace = true }
miette = { workspace = true }
+10 -2
View File
@@ -93,8 +93,12 @@ be additive — they don't displace existing categories.
## Inputs
- **Policy** — a `SandboxPolicy` proto, parsed via
`openshell-policy::parse_sandbox_policy`.
- **Policy** — authored YAML decoded by `openshell-policy-schema` with the shared
fail-closed parser, then projected into the prover-only `PolicyModel`.
Missing `version` and unknown authored fields are rejected consistently with
runtime parsing. An absent `filesystem_policy` uses the runtime-effective
`include_workdir: true`; an explicitly present empty object remains `false`.
Explicit `protocol: tcp` is treated as L4, matching runtime classification.
- **Credential set** — built from the sandbox's attached providers in
`crates/openshell-server/src/grpc/policy.rs::build_credential_set_for_sandbox`.
v1 captures presence only (host-coarse); no scope modeling.
@@ -103,6 +107,10 @@ be additive — they don't displace existing categories.
the binary's protocols, `bypasses_l7` flag, and `can_exfiltrate`
capability.
Accepted-risk files, credential descriptors, and binary registries are
separate file formats. They continue to use their own serde definitions and do
not form part of the authored policy schema.
## Outputs
- A list of `Finding` values, one per fired category. Each finding's
+29
View File
@@ -127,6 +127,35 @@ filesystem_policy:
assert!(!readable.contains(&policy::WORKDIR_PATH_SYMBOL.to_owned()));
}
#[test]
fn absent_filesystem_uses_runtime_effective_workdir_default() {
let model = policy::parse_policy_str("version: 1\n").expect("parse");
assert!(model.filesystem_policy.include_workdir);
assert!(
model
.filesystem_policy
.readable_paths(None)
.contains(&policy::WORKDIR_PATH_SYMBOL.to_owned())
);
}
#[test]
fn policy_parser_requires_version_and_rejects_unknown_authority() {
assert!(policy::parse_policy_str("network_policies: {}\n").is_err());
assert!(policy::parse_policy_str("version: 1\nfuture_authority: true\n").is_err());
}
#[test]
fn explicit_tcp_is_l4_in_the_risk_projection() {
let model = policy::parse_policy_str(
"version: 1\nnetwork_policies:\n tcp:\n endpoints:\n - host: example.com\n port: 443\n protocol: tcp\n",
)
.expect("parse");
let endpoint = &model.network_policies["tcp"].endpoints[0];
assert!(!endpoint.is_l7_enforced());
assert_eq!(endpoint.intent(), policy::PolicyIntent::L4Only);
}
// 4. Workdir excluded when include_workdir: false.
#[test]
fn test_include_workdir_false() {
+100 -191
View File
@@ -7,17 +7,13 @@
//! types) because the prover needs fields like `access`, `protocol`, and
//! individual L7 rules that the proto representation strips.
use openshell_policy_schema::{
AccessPreset, L7Allow as AuthoredAllow, NetworkEndpoint as AuthoredEndpoint, ParseLimits,
PolicyDocument,
};
use std::collections::{BTreeMap, HashSet};
use std::path::Path;
use miette::{IntoDiagnostic, Result, WrapErr};
use serde::Deserialize;
use serde::de::IgnoredAny;
// ---------------------------------------------------------------------------
// Policy intent
// ---------------------------------------------------------------------------
/// The inferred access intent for an endpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyIntent {
@@ -29,106 +25,22 @@ pub enum PolicyIntent {
}
impl std::fmt::Display for PolicyIntent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::L4Only => write!(f, "l4_only"),
Self::ReadOnly => write!(f, "read_only"),
Self::ReadWrite => write!(f, "read_write"),
Self::Full => write!(f, "full"),
Self::Custom => write!(f, "custom"),
}
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::L4Only => "l4_only",
Self::ReadOnly => "read_only",
Self::ReadWrite => "read_write",
Self::Full => "full",
Self::Custom => "custom",
})
}
}
/// HTTP methods considered to be write operations.
pub const WRITE_METHODS: &[&str] = &["POST", "PUT", "PATCH", "DELETE"];
/// All standard HTTP methods.
const ALL_METHODS: &[&str] = &["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"];
// ---------------------------------------------------------------------------
// Serde types — mirrors the YAML schema
// ---------------------------------------------------------------------------
#[derive(Debug, Deserialize)]
struct PolicyFile {
#[allow(dead_code)]
version: Option<u32>,
#[serde(default)]
filesystem_policy: Option<FilesystemDef>,
#[serde(default)]
network_policies: Option<BTreeMap<String, NetworkPolicyRuleDef>>,
// Ignored fields the prover does not need.
#[serde(default)]
#[allow(dead_code)]
landlock: Option<IgnoredAny>,
#[serde(default)]
#[allow(dead_code)]
process: Option<IgnoredAny>,
}
#[derive(Debug, Deserialize)]
struct FilesystemDef {
#[serde(default)]
include_workdir: bool,
#[serde(default)]
read_only: Vec<String>,
#[serde(default)]
read_write: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct NetworkPolicyRuleDef {
#[serde(default)]
name: Option<String>,
#[serde(default)]
endpoints: Vec<EndpointDef>,
#[serde(default)]
binaries: Vec<BinaryDef>,
}
#[derive(Debug, Deserialize)]
struct EndpointDef {
#[serde(default)]
host: String,
#[serde(default)]
port: u16,
#[serde(default)]
ports: Vec<u16>,
#[serde(default)]
protocol: String,
#[serde(default)]
tls: String,
#[serde(default)]
enforcement: String,
#[serde(default)]
access: String,
#[serde(default)]
rules: Vec<L7RuleDef>,
#[serde(default)]
allowed_ips: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct L7RuleDef {
allow: L7AllowDef,
}
#[derive(Debug, Deserialize)]
struct L7AllowDef {
#[serde(default)]
method: String,
#[serde(default)]
path: String,
#[serde(default)]
command: String,
}
#[derive(Debug, Deserialize)]
struct BinaryDef {
path: String,
}
// ---------------------------------------------------------------------------
// Public model types
// ---------------------------------------------------------------------------
@@ -158,19 +70,19 @@ pub struct Endpoint {
impl Endpoint {
/// Whether this endpoint has L7 (protocol-level) enforcement.
pub fn is_l7_enforced(&self) -> bool {
!self.protocol.is_empty()
!self.protocol.is_empty() && !self.protocol.eq_ignore_ascii_case("tcp")
}
/// The inferred access intent.
pub fn intent(&self) -> PolicyIntent {
if self.protocol.is_empty() {
if !self.is_l7_enforced() {
return PolicyIntent::L4Only;
}
match self.access.as_str() {
"read-only" => PolicyIntent::ReadOnly,
"read-write" => PolicyIntent::ReadWrite,
"full" => PolicyIntent::Full,
_ => {
match AccessPreset::parse(&self.access) {
Some(AccessPreset::ReadOnly) => PolicyIntent::ReadOnly,
Some(AccessPreset::ReadWrite) => PolicyIntent::ReadWrite,
Some(AccessPreset::Full) => PolicyIntent::Full,
None => {
if self.rules.is_empty() {
return PolicyIntent::Custom;
}
@@ -204,33 +116,32 @@ impl Endpoint {
/// The set of HTTP methods this endpoint allows. Empty means all (L4-only).
pub fn allowed_methods(&self) -> HashSet<String> {
if self.protocol.is_empty() {
if !self.is_l7_enforced() {
return HashSet::new(); // L4-only: all traffic passes
}
match self.access.as_str() {
"read-only" => ["GET", "HEAD", "OPTIONS"]
.iter()
.map(|s| (*s).to_owned())
.collect(),
"read-write" => ["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"]
.iter()
.map(|s| (*s).to_owned())
.collect(),
"full" => ALL_METHODS.iter().map(|s| (*s).to_owned()).collect(),
_ => {
if !self.rules.is_empty() {
let mut methods = HashSet::new();
for r in &self.rules {
let m = r.method.to_uppercase();
if m == "*" {
return ALL_METHODS.iter().map(|s| (*s).to_owned()).collect();
}
methods.insert(m);
}
return methods;
}
HashSet::new()
if let Some(preset) = AccessPreset::parse(&self.access) {
let methods = preset.methods(&self.protocol);
if methods.contains(&"*") {
ALL_METHODS
.iter()
.map(|method| (*method).to_owned())
.collect()
} else {
methods.iter().map(|method| (*method).to_owned()).collect()
}
} else {
if !self.rules.is_empty() {
let mut methods = HashSet::new();
for r in &self.rules {
let m = r.method.to_uppercase();
if m == "*" {
return ALL_METHODS.iter().map(|s| (*s).to_owned()).collect();
}
methods.insert(m);
}
return methods;
}
HashSet::new()
}
}
}
@@ -344,80 +255,78 @@ impl PolicyModel {
// Parsing
// ---------------------------------------------------------------------------
/// Parse an `OpenShell` policy YAML file into a [`PolicyModel`].
pub fn parse_policy(path: &Path) -> Result<PolicyModel> {
let contents = std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("reading policy file {}", path.display()))?;
parse_policy_str(&contents)
/// Parse an `OpenShell` policy YAML file into a `PolicyModel`.
pub fn parse_policy(path: &Path) -> miette::Result<PolicyModel> {
let document = openshell_policy_schema::parse_policy_file(path, ParseLimits::default())?;
Ok(project_policy(document))
}
/// Parse a policy YAML string into a [`PolicyModel`].
pub fn parse_policy_str(yaml: &str) -> Result<PolicyModel> {
let raw: PolicyFile = serde_yml::from_str(yaml)
.into_diagnostic()
.wrap_err("parsing policy YAML")?;
/// Parse a policy YAML string into a `PolicyModel`.
pub fn parse_policy_str(yaml: &str) -> miette::Result<PolicyModel> {
let document = openshell_policy_schema::parse_policy(yaml)?;
Ok(project_policy(document))
}
let fs = match raw.filesystem_policy {
Some(fs_def) => FilesystemPolicy {
include_workdir: fs_def.include_workdir,
read_only: fs_def.read_only,
read_write: fs_def.read_write,
},
None => FilesystemPolicy::default(),
fn project_policy(document: PolicyDocument) -> PolicyModel {
let filesystem = document.effective_filesystem_policy();
let filesystem_policy = FilesystemPolicy {
include_workdir: filesystem.include_workdir,
read_only: filesystem.read_only,
read_write: filesystem.read_write,
};
let mut network_policies = BTreeMap::new();
if let Some(np) = raw.network_policies {
for (key, rule_raw) in np {
let endpoints = rule_raw
.endpoints
.into_iter()
.map(|ep_raw| {
let rules = ep_raw
.rules
.into_iter()
.map(|r| L7Rule {
method: r.allow.method,
path: r.allow.path,
command: r.allow.command,
})
.collect();
Endpoint {
host: ep_raw.host,
port: ep_raw.port,
ports: ep_raw.ports,
protocol: ep_raw.protocol,
tls: ep_raw.tls,
enforcement: ep_raw.enforcement,
access: ep_raw.access,
rules,
allowed_ips: ep_raw.allowed_ips,
}
})
.collect();
let binaries = rule_raw
let network_policies = document
.network_policies
.into_iter()
.map(|(key, rule)| {
let name = rule.effective_name(&key).to_owned();
let endpoints = rule.endpoints.into_iter().map(project_endpoint).collect();
let binaries = rule
.binaries
.into_iter()
.map(|b| Binary { path: b.path })
.map(|binary| Binary { path: binary.path })
.collect();
let name = rule_raw.name.unwrap_or_else(|| key.clone());
network_policies.insert(
(
key,
NetworkPolicyRule {
name,
endpoints,
binaries,
},
);
}
}
)
})
.collect();
Ok(PolicyModel {
version: raw.version.unwrap_or(1),
filesystem_policy: fs,
PolicyModel {
version: document.version,
filesystem_policy,
network_policies,
})
}
}
fn project_endpoint(endpoint: AuthoredEndpoint) -> Endpoint {
let rules = endpoint
.rules
.into_iter()
.map(|rule| project_allow(rule.allow))
.collect();
Endpoint {
host: endpoint.host,
port: endpoint.port,
ports: endpoint.ports,
protocol: endpoint.protocol,
tls: endpoint.tls,
enforcement: endpoint.enforcement,
access: endpoint.access,
rules,
allowed_ips: endpoint.allowed_ips,
}
}
fn project_allow(allow: AuthoredAllow) -> L7Rule {
L7Rule {
method: allow.method,
path: allow.path,
command: allow.command,
}
}
@@ -79,8 +79,7 @@ mod tests {
async fn authenticator(driver: NoopTestDriver) -> ComputeDriverAuthenticator {
let store = Arc::new(Store::connect("sqlite::memory:").await.unwrap());
let compute =
new_test_runtime_with_driver(store, "external-kubernetes", Arc::new(driver)).await;
let compute = new_test_runtime_with_driver(store, "external-kubernetes", Arc::new(driver));
ComputeDriverAuthenticator::new(compute)
}
+12 -9
View File
@@ -4930,15 +4930,16 @@ fn is_terminal_failure_reason(reason: &str) -> bool {
!transient_reasons.contains(&reason.as_str())
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
#[derive(Debug)]
pub struct NoopTestDriver {
workspace_delete_failures: std::sync::atomic::AtomicUsize,
sandbox_authentication: Option<Result<String, (Code, String)>>,
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
impl NoopTestDriver {
#[cfg(test)]
pub fn failing_workspace_deletes(count: usize) -> Self {
Self {
workspace_delete_failures: std::sync::atomic::AtomicUsize::new(count),
@@ -4946,6 +4947,7 @@ impl NoopTestDriver {
}
}
#[cfg(test)]
pub fn authenticating_sandbox(sandbox_id: impl Into<String>) -> Self {
Self {
workspace_delete_failures: std::sync::atomic::AtomicUsize::new(0),
@@ -4953,6 +4955,7 @@ impl NoopTestDriver {
}
}
#[cfg(test)]
pub fn failing_sandbox_authentication(code: Code, message: impl Into<String>) -> Self {
Self {
workspace_delete_failures: std::sync::atomic::AtomicUsize::new(0),
@@ -4961,7 +4964,7 @@ impl NoopTestDriver {
}
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
impl Default for NoopTestDriver {
fn default() -> Self {
Self {
@@ -4971,7 +4974,7 @@ impl Default for NoopTestDriver {
}
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
#[tonic::async_trait]
impl ComputeDriver for NoopTestDriver {
async fn authenticate_sandbox(
@@ -5130,18 +5133,18 @@ impl ComputeDriver for NoopTestDriver {
}
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub async fn new_test_runtime(store: Arc<Store>) -> ComputeRuntime {
new_test_runtime_for_driver(store, "test").await
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub async fn new_test_runtime_for_driver(store: Arc<Store>, driver_name: &str) -> ComputeRuntime {
new_test_runtime_with_driver(store, driver_name, Arc::new(NoopTestDriver::default())).await
new_test_runtime_with_driver(store, driver_name, Arc::new(NoopTestDriver::default()))
}
#[cfg(test)]
pub async fn new_test_runtime_with_driver(
#[cfg(any(test, feature = "test-support"))]
pub fn new_test_runtime_with_driver(
store: Arc<Store>,
driver_name: &str,
driver: Arc<NoopTestDriver>,
@@ -141,7 +141,7 @@ impl RootfsTarStagingRegistry {
}
/// Registry for a driver that does not accept rootfs tar sources.
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub fn disabled() -> Self {
Self::new(None, 0)
}
+1 -1
View File
@@ -961,7 +961,7 @@ pub mod test_support {
);
crate::ensure_default_workspace(&store).await.unwrap();
let driver = Arc::new(NoopTestDriver::failing_workspace_deletes(failures));
let compute = new_test_runtime_with_driver(store.clone(), "test", driver).await;
let compute = new_test_runtime_with_driver(store.clone(), "test", driver);
Arc::new(ServerState::new(
Config::new(None)
.with_database_url("sqlite::memory:?cache=shared")
@@ -3,7 +3,17 @@
//! Test fixtures for exercising gateway integration points.
use crate::ServerState;
use crate::auth::identity::{Identity, IdentityProvider};
use crate::auth::principal::{Principal, UserPrincipal};
use crate::compute::{new_test_runtime, new_test_runtime_for_driver};
use crate::persistence::Store;
use crate::sandbox_index::SandboxIndex;
use crate::sandbox_watch::SandboxWatchBus;
use crate::supervisor_session::SupervisorSessionRegistry;
use crate::tracing_bus::TracingLogBus;
use futures::{Stream, stream};
use openshell_core::Config;
#[cfg(unix)]
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
use openshell_core::proto::compute::v1::{
@@ -34,6 +44,55 @@ use tonic::{Request, Response, Status};
type WatchStream = Pin<Box<dyn Stream<Item = Result<WatchSandboxesEvent, Status>> + Send>>;
/// Build a real in-memory gateway service backed by the requested test compute driver.
///
/// This fixture is intentionally narrow: integration tests can exercise the public
/// gRPC service without exposing the gateway's internal state construction details.
pub async fn gateway_service_with_driver(driver_name: &str) -> crate::OpenShellService {
let store = Arc::new(
Store::connect("sqlite::memory:?cache=shared")
.await
.expect("in-memory gateway store should open"),
);
crate::ensure_default_workspace(&store)
.await
.expect("default workspace should be created");
let compute = if driver_name == "test" {
new_test_runtime(store.clone()).await
} else {
new_test_runtime_for_driver(store.clone(), driver_name).await
};
let state = Arc::new(ServerState::new(
Config::new(None)
.with_database_url("sqlite::memory:?cache=shared")
.with_credential_drivers(["test-static"]),
store,
compute,
SandboxIndex::new(),
SandboxWatchBus::new(),
TracingLogBus::new(),
Arc::new(SupervisorSessionRegistry::new()),
None,
));
crate::OpenShellService::new(state)
}
/// Tonic interceptor that authenticates integration-test requests as the dev user.
pub fn authenticate_as_dev_user(mut request: Request<()>) -> Result<Request<()>, Status> {
request
.extensions_mut()
.insert(Principal::User(UserPrincipal {
identity: Identity {
subject: "dev-user".to_string(),
display_name: None,
roles: vec!["openshell-admin".to_string(), "openshell-user".to_string()],
scopes: vec![],
provider: IdentityProvider::Oidc,
},
}));
Ok(request)
}
#[derive(Debug, Clone, PartialEq)]
pub enum FakeComputeDriverCall {
GetCapabilities,
+30 -15
View File
@@ -2745,10 +2745,10 @@ fn discover_policy_from_disk_or_default() -> openshell_core::proto::SandboxPolic
/// hardcoded restrictive default if the file is missing or invalid.
fn discover_policy_from_path(path: &std::path::Path) -> openshell_core::proto::SandboxPolicy {
use openshell_policy::{
parse_sandbox_policy, restrictive_default_policy, validate_sandbox_policy,
parse_sandbox_policy_file, restrictive_default_policy, validate_sandbox_policy,
};
let Ok(yaml) = std::fs::read_to_string(path) else {
if !path.exists() {
ocsf_emit!(
ConfigStateChangeBuilder::new(ocsf_ctx())
.severity(SeverityId::Informational)
@@ -2761,20 +2761,20 @@ fn discover_policy_from_path(path: &std::path::Path) -> openshell_core::proto::S
.build()
);
return restrictive_default_policy();
};
ocsf_emit!(
ConfigStateChangeBuilder::new(ocsf_ctx())
.severity(SeverityId::Informational)
.status(StatusId::Success)
.state(StateId::Enabled, "loaded")
.message(format!(
"Loaded sandbox policy from container disk [path:{}]",
path.display()
))
.build()
);
match parse_sandbox_policy(&yaml) {
}
match parse_sandbox_policy_file(path) {
Ok(policy) => {
ocsf_emit!(
ConfigStateChangeBuilder::new(ocsf_ctx())
.severity(SeverityId::Informational)
.status(StatusId::Success)
.state(StateId::Enabled, "loaded")
.message(format!(
"Loaded sandbox policy from container disk [path:{}]",
path.display()
))
.build()
);
// Validate the disk-loaded policy for safety.
if let Err(violations) = validate_sandbox_policy(&policy) {
let messages: Vec<String> = violations.iter().map(ToString::to_string).collect();
@@ -5468,6 +5468,21 @@ network_policies:
assert!(policy.filesystem.is_some());
}
#[test]
fn discover_policy_from_oversized_file_returns_restrictive_default() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("policy.yaml");
let oversized = format!("version: 1\n{}", " ".repeat(4 * 1024 * 1024));
std::fs::write(&path, oversized).unwrap();
let policy = discover_policy_from_path(&path);
assert!(policy.network_policies.is_empty());
assert!(
policy.filesystem.is_some(),
"an oversized otherwise-valid policy must fall back instead of parsing"
);
}
#[test]
fn discover_policy_from_unsafe_yaml_falls_back_to_default() {
let dir = tempfile::tempdir().unwrap();
+1 -1
View File
@@ -67,7 +67,7 @@ RUN case "$TARGETARCH" in \
| tar xz --strip-components=2 -C /usr/local/bin "gh_${GH_VERSION}_linux_${gh_arch}/bin/gh"
# Install mise (NOTE: keep this version in sync with mise.toml)
ARG MISE_VERSION=v2026.4.25
ARG MISE_VERSION=v2026.9.9
RUN curl https://mise.run | MISE_VERSION=$MISE_VERSION sh
# Copy mise.toml and task includes, then install all tools via mise
+26
View File
@@ -36,6 +36,32 @@ The YAML root and each present top-level section listed as an object or map must
Static fields are set at sandbox creation time. Changing them requires destroying and recreating the sandbox. Dynamic fields can be updated on a running sandbox with `openshell policy update` for incremental merges or `openshell policy set` for full replacement, and take effect without restarting.
## Parsing and representation
OpenShell uses one canonical authored-policy representation for YAML and JSON.
Runtime policy loading and the policy prover both decode through the same
bounded parser before projecting the document into their runtime-specific
models. The parser requires `version: 1`, rejects duplicate mapping keys and
YAML merge keys, and accepts one document of at most 4 MiB. It limits nesting
to 64 levels, total AST nodes to 100,000, parser events to 300,000, cumulative
scalar data to 4 MiB, alias expansions to 100 with a 5:1 alias-to-anchor ratio,
and each mapping or sequence to 10,000 entries.
Unknown keys in closed schema objects are rejected with their field path before
the policy is converted or analyzed. The following maps are intentionally
open user namespaces, so their keys are preserved as data: middleware `config`,
query matcher names, GraphQL persisted-query names, and recursively nested MCP
`params` names.
An absent `filesystem_policy` resolves to the runtime-effective
`include_workdir: true`. An explicitly present `filesystem_policy: {}` keeps
`include_workdir: false`. During protobuf conversion, `ports` takes precedence
over scalar `port`, one effective port serializes in compact scalar form, empty
rule names fall back to their map key, and runtime-only provenance fields are
omitted. Because proto3 scalar fields do not preserve presence, protobuf `version: 0` means omission. Canonical serialization materializes
`version: 1`; any other unsupported protobuf version is rejected. A protobuf port above
65535 is rejected rather than clamped.
## Version
The version field identifies which schema the policy uses:
+2 -1
View File
@@ -542,7 +542,8 @@ run_variant() {
-u CONTAINER_HOST -u CONTAINER_CONNECTION -u CONTAINERS_STORAGE_CONF \
-u CONTAINERS_CONF -u CONTAINERS_REGISTRIES_CONF -u CONTAINERS_REGISTRIES_CONF_DIR \
-u CONTAINERS_POLICY -u PODMAN_CONNECTIONS_CONF -u DOCKER_HOST \
-u OPENSHELL_SANDBOX_IMAGE -u OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE \
-u OPENSHELL_SANDBOX_IMAGE -u OPENSHELL_SANDBOX_RUNTIME_IMAGE \
-u OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE \
-u OPENSHELL_GRPC_ENDPOINT -u OPENSHELL_PODMAN_HOST_GATEWAY_IP \
-u OPENSHELL_PODMAN_USERNS -u OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET \
-u OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET -u OPENSHELL_APP_ARMOR_PROFILE \
+11 -6
View File
@@ -97,6 +97,7 @@ for variable in \
CONTAINER_HOST CONTAINER_CONNECTION CONTAINERS_STORAGE_CONF CONTAINERS_CONF \
CONTAINERS_REGISTRIES_CONF CONTAINERS_REGISTRIES_CONF_DIR CONTAINERS_POLICY \
PODMAN_CONNECTIONS_CONF DOCKER_HOST OPENSHELL_SANDBOX_IMAGE \
OPENSHELL_SANDBOX_RUNTIME_IMAGE \
OPENSHELL_GRPC_ENDPOINT OPENSHELL_PODMAN_HOST_GATEWAY_IP OPENSHELL_PODMAN_USERNS \
OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET \
OPENSHELL_APP_ARMOR_PROFILE OPENSHELL_SANDBOX_HTTPS_PROXY OPENSHELL_SANDBOX_NO_PROXY \
@@ -109,8 +110,8 @@ expected_sandbox="ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:$(pri
[ "${OPENSHELL_E2E_REQUIRE_DIGEST_PINNED_SANDBOX_IMAGE:-0}" = 1 ] || exit 24
[ "${OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE:-}" = "${expected_sandbox}" ] || exit 25
[ "${OPENSHELL_COMMUNITY_REGISTRY:-}" = "ghcr.io/nvidia/openshell-community/sandboxes" ] || exit 28
expected_base="docker.io/library/alpine@sha256:$(printf '%064d' 0)"
[ "${OPENSHELL_E2E_SUPERVISOR_BASE_IMAGE:-}" = alpine:3.22 ] || exit 26
expected_base="docker.io/library/debian@sha256:$(printf '%064d' 0)"
[ "${OPENSHELL_E2E_SUPERVISOR_BASE_IMAGE:-}" = debian:bookworm-slim ] || exit 26
[ "${OPENSHELL_E2E_SUPERVISOR_BASE_RUNTIME_IMAGE:-}" = "${expected_base}" ] || exit 27
printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$OPENSHELL_PARITY_VARIANT" "$OPENSHELL_E2E_CONFIG_SCHEMA_VERSION" "$OPENSHELL_GATEWAY_BIN" "$OPENSHELL_BIN" "$OPENSHELL_CONFORMANCE_BIN" "$MISE_TRUSTED_CONFIG_PATHS" "${OPENSHELL_E2E_PODMAN_OPTION_PROFILE:-}" "${OPENSHELL_PARITY_ORACLE_RESULT:-}" "${OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER:-}" "${OPENSHELL_EXTERNAL_DRIVER_BIN:-}" "${OPENSHELL_E2E_SUPERVISOR_BIN:-}" >>"$OPENSHELL_PARITY_TEST_CALLS"
mkdir -p "$XDG_DATA_HOME/containers/storage"
@@ -127,8 +128,9 @@ external = os.environ.get("OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER") == "1"
zero = "0" * 64
image_digest = f"sha256:{zero}"
sandbox_runtime = f"ghcr.io/nvidia/openshell-community/sandboxes/base@{image_digest}"
sandbox_boundary = "localhost/openshell/sandbox:dev"
supervisor_runtime = f"localhost/openshell/supervisor@{image_digest}"
base_runtime = f"docker.io/library/alpine@{image_digest}"
base_runtime = f"docker.io/library/debian@{image_digest}"
pull_policy = "missing" if schema == 1 else "if_not_present"
gateway_port = 18181
callback = f"https://host.containers.internal:{gateway_port}"
@@ -172,7 +174,7 @@ launch = {
"supervisor_image_id": zero,
"supervisor_image_digest": image_digest,
"supervisor_runtime_image": supervisor_runtime,
"supervisor_base_image": "alpine:3.22",
"supervisor_base_image": "debian:bookworm-slim",
"supervisor_base_image_id": zero,
"supervisor_base_image_digest": image_digest,
"supervisor_base_runtime_image": base_runtime,
@@ -183,6 +185,7 @@ launch = {
"sandbox_image_id": zero,
"sandbox_image_digest": image_digest,
"sandbox_runtime_image": sandbox_runtime,
"sandbox_boundary_image": sandbox_boundary,
"sandbox_client_image_alias": "ghcr.io/nvidia/openshell-community/sandboxes/base:latest",
"sandbox_client_image_alias_id": zero,
"gateway_sha256_before_execution": os.environ[
@@ -220,6 +223,7 @@ if external:
"OPENSHELL_PODMAN_SOCKET": podman_socket,
"OPENSHELL_SANDBOX_IMAGE": sandbox_runtime,
"OPENSHELL_SANDBOX_IMAGE_PULL_POLICY": pull_policy,
"OPENSHELL_SANDBOX_RUNTIME_IMAGE": sandbox_boundary,
"OPENSHELL_HEALTH_CHECK_INTERVAL_SECS": 10,
"OPENSHELL_GRPC_ENDPOINT": callback,
"OPENSHELL_GATEWAY_PORT": gateway_port,
@@ -288,7 +292,7 @@ case "$1" in
case "$4" in
'{{.Id}}') printf 'sha256:%064d\n' 0 ;;
'{{.Digest}}') printf 'sha256:%064d\n' 0 ;;
'{{index .RepoDigests 0}}') printf 'docker.io/library/alpine@sha256:%064d\n' 0 ;;
'{{index .RepoDigests 0}}') printf 'docker.io/library/debian@sha256:%064d\n' 0 ;;
*) exit 19 ;;
esac
;;
@@ -369,6 +373,7 @@ CONTAINERS_POLICY=/tmp/untrusted-policy.json \
PODMAN_CONNECTIONS_CONF=/tmp/untrusted-connections.json \
DOCKER_HOST=tcp://untrusted.invalid:2375 \
OPENSHELL_SANDBOX_IMAGE=untrusted.invalid/sandbox:latest \
OPENSHELL_SANDBOX_RUNTIME_IMAGE=untrusted.invalid/runtime:latest \
OPENSHELL_GRPC_ENDPOINT=http://untrusted.invalid:1 \
OPENSHELL_PODMAN_HOST_GATEWAY_IP=192.0.2.1 \
OPENSHELL_PODMAN_USERNS=keep-id \
@@ -402,7 +407,7 @@ assert_not_contains "${WORKDIR}/results/baseline.json" '"scenarios"'
assert_contains "${WORKDIR}/results/baseline.log" '"scenarios"'
assert_contains "${WORKDIR}/results/baseline.conformance.json" '"passed":true'
assert_contains "${WORKDIR}/podman-calls" 'pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest'
assert_contains "${WORKDIR}/podman-calls" 'pull alpine:3.22'
assert_contains "${WORKDIR}/podman-calls" 'pull debian:bookworm-slim'
assert_contains "${WORKDIR}/podman-calls" 'unshare rm -rf -- '
assert_contains "${WORKDIR}/podman-calls" 'openshell-parity-run.'
+44
View File
@@ -22,6 +22,7 @@ use std::time::{Duration, Instant};
use base64::Engine as _;
use openshell_e2e::harness::binary::openshell_cmd;
use serde_json::Value;
use serial_test::serial;
use tokio::process::Command;
use tokio::sync::Mutex;
use url::Url;
@@ -64,6 +65,7 @@ struct LoginSession {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn admin_can_list_sandboxes() {
let session = login_identity(ADMIN).await;
assert_allowed(
@@ -75,6 +77,7 @@ async fn admin_can_list_sandboxes() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_can_report_gateway_validated_identity() {
let session = login_identity(USER).await;
let output = assert_allowed(
@@ -102,6 +105,7 @@ async fn user_can_report_gateway_validated_identity() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_can_list_sandboxes() {
const WORKSPACE: &str = "oidc-user-list-sb";
let user = login_identity(USER).await;
@@ -118,6 +122,7 @@ async fn user_can_list_sandboxes() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn admin_can_create_sandbox() {
let session = login_identity(ADMIN).await;
let _lifecycle = SANDBOX_LIFECYCLE_LOCK.lock().await;
@@ -125,6 +130,7 @@ async fn admin_can_create_sandbox() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_can_create_sandbox() {
const WORKSPACE: &str = "oidc-user-create";
let user = login_identity(USER).await;
@@ -138,6 +144,7 @@ async fn user_can_create_sandbox() {
/// Workspace users must be able to create sandboxes with inferred-provider
/// commands (e.g. `claude`) without requiring Platform Admin access.
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_can_create_sandbox_with_inferred_provider_command() {
const WORKSPACE: &str = "oidc-inferred-cmd";
let user = login_identity(USER).await;
@@ -187,6 +194,7 @@ async fn user_can_create_sandbox_with_inferred_provider_command() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn admin_can_delete_sandbox() {
let session = login_identity(ADMIN).await;
let _lifecycle = SANDBOX_LIFECYCLE_LOCK.lock().await;
@@ -194,6 +202,7 @@ async fn admin_can_delete_sandbox() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_can_delete_sandbox() {
const WORKSPACE: &str = "oidc-user-delete";
let user = login_identity(USER).await;
@@ -205,6 +214,7 @@ async fn user_can_delete_sandbox() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn admin_can_inspect_gateway() {
let session = login_identity(ADMIN).await;
let output = assert_allowed(&session, &["gateway", "info"], "inspect gateway info").await;
@@ -219,6 +229,7 @@ async fn admin_can_inspect_gateway() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_cannot_inspect_gateway() {
let session = login_identity(USER).await;
let output = run_session_cli(&session, &["gateway", "info"]).await;
@@ -235,6 +246,7 @@ async fn user_cannot_inspect_gateway() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn admin_can_list_providers() {
let session = login_identity(ADMIN).await;
assert_allowed(
@@ -246,6 +258,7 @@ async fn admin_can_list_providers() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_can_list_providers() {
const WORKSPACE: &str = "oidc-user-list-pr";
let user = login_identity(USER).await;
@@ -262,6 +275,7 @@ async fn user_can_list_providers() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn admin_can_manage_provider() {
const PROVIDER: &str = "oidc-pkce-admin-provider";
let session = login_identity(ADMIN).await;
@@ -298,6 +312,7 @@ async fn admin_can_manage_provider() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_cannot_create_provider() {
const WORKSPACE: &str = "oidc-user-no-create";
let user = login_identity(USER).await;
@@ -323,6 +338,7 @@ async fn user_cannot_create_provider() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_cannot_delete_provider() {
const PROVIDER: &str = "oidc-pkce-user-delete-target";
const WORKSPACE: &str = "oidc-user-no-delete";
@@ -360,6 +376,7 @@ async fn user_cannot_delete_provider() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn admin_can_create_workspace() {
const WORKSPACE: &str = "oidc-admin-create";
let admin = login_identity(ADMIN).await;
@@ -381,6 +398,7 @@ async fn admin_can_create_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_cannot_create_workspace() {
let user = login_identity(USER).await;
let denied = run_session_cli(
@@ -392,6 +410,7 @@ async fn user_cannot_create_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn admin_can_delete_workspace() {
const WORKSPACE: &str = "oidc-admin-delete";
let admin = login_identity(ADMIN).await;
@@ -406,6 +425,7 @@ async fn admin_can_delete_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn user_cannot_delete_workspace() {
const WORKSPACE: &str = "oidc-user-del-deny";
let admin = login_identity(ADMIN).await;
@@ -423,6 +443,7 @@ async fn user_cannot_delete_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_user_can_read_workspace() {
const WORKSPACE: &str = "oidc-ws-user-read";
let user = login_identity(USER).await;
@@ -454,6 +475,7 @@ async fn workspace_user_can_read_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_user_cannot_manage_members() {
const WORKSPACE: &str = "oidc-ws-user-deny";
let user = login_identity(USER).await;
@@ -479,6 +501,7 @@ async fn workspace_user_cannot_manage_members() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_can_read_workspace() {
const WORKSPACE: &str = "oidc-wsa-read";
let user = login_identity(USER).await;
@@ -496,6 +519,7 @@ async fn workspace_admin_can_read_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_can_create_sandbox() {
const WORKSPACE: &str = "oidc-wsa-create-sb";
let user = login_identity(USER).await;
@@ -507,6 +531,7 @@ async fn workspace_admin_can_create_sandbox() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_can_delete_sandbox() {
const WORKSPACE: &str = "oidc-wsa-delete-sb";
let user = login_identity(USER).await;
@@ -518,6 +543,7 @@ async fn workspace_admin_can_delete_sandbox() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_can_create_provider() {
const WORKSPACE: &str = "oidc-wsa-create-pr";
const PROVIDER: &str = "oidc-wsa-create-provider";
@@ -552,6 +578,7 @@ async fn workspace_admin_can_create_provider() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_can_delete_provider() {
const WORKSPACE: &str = "oidc-wsa-delete-pr";
const PROVIDER: &str = "oidc-wsa-delete-provider";
@@ -585,6 +612,7 @@ async fn workspace_admin_can_delete_provider() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_can_add_user_member() {
const WORKSPACE: &str = "oidc-wsa-add-user";
let workspace_admin = login_identity(USER).await;
@@ -619,6 +647,7 @@ async fn workspace_admin_can_add_user_member() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_can_remove_user_member() {
const WORKSPACE: &str = "oidc-wsa-rm-user";
let workspace_admin = login_identity(USER).await;
@@ -662,6 +691,7 @@ async fn workspace_admin_can_remove_user_member() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_cannot_grant_admin() {
const WORKSPACE: &str = "oidc-ws-admin-deny";
let user = login_identity(USER).await;
@@ -687,6 +717,7 @@ async fn workspace_admin_cannot_grant_admin() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_cannot_create_workspace() {
const WORKSPACE: &str = "oidc-wsa-no-create";
let user = login_identity(USER).await;
@@ -700,6 +731,7 @@ async fn workspace_admin_cannot_create_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_cannot_delete_workspace() {
const WORKSPACE: &str = "oidc-wsa-no-delete";
let user = login_identity(USER).await;
@@ -712,6 +744,7 @@ async fn workspace_admin_cannot_delete_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_cannot_inspect_gateway() {
const WORKSPACE: &str = "oidc-wsa-no-gw-info";
let user = login_identity(USER).await;
@@ -724,6 +757,7 @@ async fn workspace_admin_cannot_inspect_gateway() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_cannot_read_another_workspace() {
const WORKSPACE_A: &str = "oidc-wsa-xread-a";
const WORKSPACE_B: &str = "oidc-wsa-xread-b";
@@ -738,6 +772,7 @@ async fn workspace_admin_cannot_read_another_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_cannot_manage_another_workspace_members() {
const WORKSPACE_A: &str = "oidc-wsa-xmem-a";
const WORKSPACE_B: &str = "oidc-wsa-xmem-b";
@@ -769,6 +804,7 @@ async fn workspace_admin_cannot_manage_another_workspace_members() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_admin_cannot_manage_another_workspace_providers() {
const WORKSPACE_A: &str = "oidc-wsa-xprov-a";
const WORKSPACE_B: &str = "oidc-wsa-xprov-b";
@@ -800,6 +836,7 @@ async fn workspace_admin_cannot_manage_another_workspace_providers() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn membership_removal_revokes_workspace_access() {
const WORKSPACE: &str = "oidc-ws-revoke";
let user = login_identity(USER).await;
@@ -825,6 +862,7 @@ async fn membership_removal_revokes_workspace_access() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_user_cannot_read_another_users_workspace() {
const WORKSPACE_A: &str = "oidc-xread-a";
const WORKSPACE_B: &str = "oidc-xread-b";
@@ -838,6 +876,7 @@ async fn workspace_user_cannot_read_another_users_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn second_workspace_user_cannot_read_first_users_workspace() {
const WORKSPACE_A: &str = "oidc-xread2-a";
const WORKSPACE_B: &str = "oidc-xread2-b";
@@ -851,6 +890,7 @@ async fn second_workspace_user_cannot_read_first_users_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_list_hides_another_users_workspace() {
const WORKSPACE_A: &str = "oidc-xlist-a";
const WORKSPACE_B: &str = "oidc-xlist-b";
@@ -877,6 +917,7 @@ async fn workspace_list_hides_another_users_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_user_cannot_list_another_workspace_sandboxes() {
const WORKSPACE_A: &str = "oidc-xsbox-a";
const WORKSPACE_B: &str = "oidc-xsbox-b";
@@ -895,6 +936,7 @@ async fn workspace_user_cannot_list_another_workspace_sandboxes() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_user_cannot_create_sandbox_in_another_workspace() {
const WORKSPACE_A: &str = "oidc-xcreate-a";
const WORKSPACE_B: &str = "oidc-xcreate-b";
@@ -922,6 +964,7 @@ async fn workspace_user_cannot_create_sandbox_in_another_workspace() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_user_cannot_list_another_workspace_providers() {
const WORKSPACE_A: &str = "oidc-xprov-a";
const WORKSPACE_B: &str = "oidc-xprov-b";
@@ -940,6 +983,7 @@ async fn workspace_user_cannot_list_another_workspace_providers() {
}
#[tokio::test]
#[serial(oidc_pkce)]
async fn workspace_user_cannot_list_another_workspace_members() {
const WORKSPACE_A: &str = "oidc-xmember-a";
const WORKSPACE_B: &str = "oidc-xmember-b";
+151 -128
View File
@@ -19,28 +19,28 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.4"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "async-trait"
version = "0.1.89"
version = "0.1.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.5",
]
[[package]]
@@ -154,9 +154,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.13.0"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "block-buffer"
@@ -175,15 +175,15 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytes"
version = "1.12.0"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cc"
version = "1.2.65"
version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -307,13 +307,13 @@ dependencies = [
[[package]]
name = "displaydoc"
version = "0.2.6"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.5",
]
[[package]]
@@ -324,9 +324,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.16.0"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]]
name = "equivalent"
@@ -346,15 +346,15 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
[[package]]
name = "fixedbitset"
@@ -391,36 +391,36 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures-channel"
version = "0.3.32"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.32"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-sink"
version = "0.3.32"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
[[package]]
name = "futures-task"
version = "0.3.32"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.32"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
@@ -475,9 +475,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "h2"
version = "0.4.15"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
dependencies = [
"atomic-waker",
"bytes",
@@ -534,9 +534,9 @@ dependencies = [
[[package]]
name = "http"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"bytes",
"itoa",
@@ -544,9 +544,9 @@ dependencies = [
[[package]]
name = "http-body"
version = "1.0.1"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
dependencies = [
"bytes",
"http",
@@ -554,9 +554,9 @@ dependencies = [
[[package]]
name = "http-body-util"
version = "0.1.3"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [
"bytes",
"futures-core",
@@ -579,9 +579,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.10.1"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
dependencies = [
"atomic-waker",
"bytes",
@@ -737,9 +737,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.14.0"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
@@ -801,7 +801,7 @@ dependencies = [
"quote",
"rustc_version",
"simd_cesu8",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -820,7 +820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -864,9 +864,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.186"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libm"
@@ -897,9 +897,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.33"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "matchit"
@@ -909,9 +909,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.8.2"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "miette"
@@ -940,7 +940,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -960,9 +960,9 @@ dependencies = [
[[package]]
name = "mio"
version = "1.2.1"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
dependencies = [
"libc",
"wasi",
@@ -1005,9 +1005,9 @@ dependencies = [
[[package]]
name = "num-bigint"
version = "0.4.6"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
@@ -1021,9 +1021,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-integer"
version = "0.1.46"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
@@ -1067,6 +1067,7 @@ dependencies = [
"miette",
"nix",
"openshell-extension-core",
"openshell-policy-schema",
"prost",
"prost-types",
"protoc-bin-vendored",
@@ -1126,13 +1127,23 @@ version = "0.0.0"
dependencies = [
"hickory-proto",
"miette",
"noyalib",
"openshell-core",
"openshell-policy-schema",
"prost-types",
"serde",
"serde_json",
]
[[package]]
name = "openshell-policy-schema"
version = "0.0.0"
dependencies = [
"miette",
"noyalib",
"serde",
"serde_json",
]
[[package]]
name = "openshell-providers"
version = "0.0.0"
@@ -1235,7 +1246,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -1278,14 +1289,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
"syn 2.0.119",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
@@ -1317,7 +1328,7 @@ dependencies = [
"pulldown-cmark",
"pulldown-cmark-to-cmark",
"regex",
"syn",
"syn 2.0.119",
"tempfile",
]
@@ -1331,7 +1342,7 @@ dependencies = [
"itertools",
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -1433,18 +1444,18 @@ dependencies = [
[[package]]
name = "pulldown-cmark-to-cmark"
version = "22.0.0"
version = "22.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90"
checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60"
dependencies = [
"pulldown-cmark",
]
[[package]]
name = "quote"
version = "1.0.46"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@@ -1505,9 +1516,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.12.4"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
@@ -1517,9 +1528,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.14"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
@@ -1582,9 +1593,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.41"
version = "0.23.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
"aws-lc-rs",
"log",
@@ -1618,18 +1629,18 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.13"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"aws-lc-rs",
"ring",
@@ -1639,9 +1650,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "same-file"
@@ -1698,9 +1709,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
@@ -1718,29 +1729,29 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.5",
]
[[package]]
name = "serde_json"
version = "1.0.150"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
@@ -1821,15 +1832,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.2"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
[[package]]
name = "socket2"
version = "0.6.4"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.61.2",
@@ -1870,9 +1881,20 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "2.0.118"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
@@ -1893,7 +1915,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -1931,29 +1953,29 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.18"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.5",
]
[[package]]
name = "time"
version = "0.3.52"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e48db7b415311b615f910b3dcaa4557bcd4bf1982379c95c223fd8c2a20e210"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"num-conv",
@@ -1971,9 +1993,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.31"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
@@ -2006,9 +2028,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.52.3"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
"libc",
@@ -2023,20 +2045,20 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.7.0"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.5",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
dependencies = [
"rustls",
"tokio",
@@ -2044,9 +2066,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -2055,13 +2077,14 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.18"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"libc",
"pin-project-lite",
"tokio",
]
@@ -2106,7 +2129,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -2131,7 +2154,7 @@ dependencies = [
"prost-build",
"prost-types",
"quote",
"syn",
"syn 2.0.119",
"tempfile",
"tonic-build",
]
@@ -2197,7 +2220,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -2355,7 +2378,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
"wasm-bindgen-shared",
]
@@ -2499,7 +2522,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
"synstructure",
]
@@ -2520,7 +2543,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
"synstructure",
]
@@ -2541,7 +2564,7 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -2574,11 +2597,11 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
name = "zmij"
version = "1.0.21"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+50 -9
View File
@@ -810,6 +810,8 @@ checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
@@ -872,6 +874,12 @@ version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -985,6 +993,22 @@ dependencies = [
"libc",
]
[[package]]
name = "noyalib"
version = "0.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f075ef19fa3bcf8697c0ef96c37d5c435d339a40ab8081cae3aac3a4e7fee9a"
dependencies = [
"hashbrown 0.17.1",
"indexmap",
"libm",
"memchr",
"rustc-hash",
"serde",
"serde_core",
"smallvec",
]
[[package]]
name = "object"
version = "0.37.3"
@@ -1017,6 +1041,7 @@ dependencies = [
"miette",
"nix",
"openshell-extension-core",
"openshell-policy-schema",
"prost",
"prost-types",
"protoc-bin-vendored",
@@ -1050,6 +1075,16 @@ dependencies = [
"tower",
]
[[package]]
name = "openshell-policy-schema"
version = "0.0.0"
dependencies = [
"miette",
"noyalib",
"serde",
"serde_json",
]
[[package]]
name = "openshell-supervisor-middleware-content-guard"
version = "0.0.0"
@@ -1386,6 +1421,12 @@ version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
[[package]]
name = "rustc-hash"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "rustix"
version = "1.1.4"
@@ -1661,13 +1702,13 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "synstructure"
version = "0.13.2"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.5",
]
[[package]]
@@ -2190,13 +2231,13 @@ dependencies = [
[[package]]
name = "yoke-derive"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.5",
"synstructure",
]
@@ -2211,13 +2252,13 @@ dependencies = [
[[package]]
name = "zerofrom-derive"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.5",
"synstructure",
]
+10 -4
View File
@@ -445,15 +445,19 @@ backend = "aqua:GoogleContainerTools/skaffold"
"vars.version_prefix" = "v"
[tools.skaffold."platforms.linux-arm64"]
checksum = "sha256:0062e1ff61f35200168454fa5a836306dd30b51acf30999d5d8836770d70ad2f"
url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-linux-arm64"
[tools.skaffold."platforms.linux-x64"]
checksum = "sha256:de75d5697d4a14d6a5e87105671ef9a30990270ab14ad301913f8f8d41a1ecca"
url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-linux-amd64"
[tools.skaffold."platforms.macos-arm64"]
checksum = "sha256:73478a431c17674d2f75d2c6e2fcbf35aa019de4ad6027e5573cc0f581767fbc"
url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-darwin-arm64"
[tools.skaffold."platforms.windows-x64"]
checksum = "sha256:a257da2988782b4c1971bb58b9c7bb52176f8a0b587b6520dec0064f271fa416"
url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-windows-amd64.exe"
[[tools.uv]]
@@ -461,13 +465,15 @@ version = "0.10.12"
backend = "aqua:astral-sh/uv"
[tools.uv."platforms.linux-arm64"]
checksum = "sha256:55bd1c1c10ec8b95a8c184f5e18b566703c6ab105f0fc118aaa4d748aabf28e4"
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-aarch64-unknown-linux-musl.tar.gz"
checksum = "sha256:0ed7d20f49f6b9b60d45fdfcac28f3ac01a671a6ef08672401ed2833423fea2a"
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-aarch64-unknown-linux-gnu.tar.gz"
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/377491938"
provenance = "github-attestations"
[tools.uv."platforms.linux-x64"]
checksum = "sha256:adccf40b5d1939a5e0093081ec2307ea24235adf7c2d96b122c561fa37711c46"
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-x86_64-unknown-linux-musl.tar.gz"
checksum = "sha256:ec72570c9d1f33021aa80b176d7baba390de2cfeb1abcbefca346d563bf17484"
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-x86_64-unknown-linux-gnu.tar.gz"
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/377491996"
provenance = "github-attestations"
[tools.uv."platforms.macos-arm64"]
+1 -1
View File
@@ -7,7 +7,7 @@
# Tasks are defined in tasks/*.toml — run `mise tasks` to list them all.
# NOTE: keep this version in sync with what CI uses in Dockerfile.ci
min_version = { soft = "2026.4.25" }
min_version = { soft = "2026.9.9" }
redactions = ["*_TOKEN", "*_PASSWORD"]
+5 -1
View File
@@ -6,6 +6,7 @@
["python:dev"]
description = "Install the Python SDK in development mode"
depends = ["python:proto"]
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0" }
run = "uv sync --group dev"
hide = true
@@ -38,23 +39,26 @@ hide = true
["python:lint"]
description = "Lint Python code with ruff"
depends = ["python:proto"]
env = { UV_NO_SYNC = "1" }
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0", UV_NO_SYNC = "1" }
run = "uv run ruff check {{vars.python_paths}}"
hide = true
["python:format"]
description = "Format Python code with ruff"
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0" }
run = "uv run ruff format {{vars.python_paths}}"
hide = true
["python:format:check"]
description = "Check Python formatting with ruff"
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0" }
run = "uv run ruff format --check {{vars.python_paths}}"
hide = true
["python:typecheck"]
description = "Type check Python code with ty"
depends = ["python:proto"]
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0" }
run = "uv run ty check {{vars.python_paths}}"
hide = true