From de2f8345c53a9afd721519b89c5e854a9a781928 Mon Sep 17 00:00:00 2001 From: Shailendra Singh Date: Fri, 4 Sep 2026 12:55:30 -0700 Subject: [PATCH] feat(policy): add portable UI policy controls Signed-off-by: Shailendra Singh --- .../build-openshell-mxc-windows/SKILL.md | 6 + architecture/compute-runtimes.md | 13 +- architecture/security-policy.md | 24 +- crates/openshell-cli/src/commands/gateway.rs | 17 + crates/openshell-core/src/policy.rs | 32 ++ crates/openshell-driver-docker/src/lib.rs | 1 + .../openshell-driver-kubernetes/src/driver.rs | 1 + crates/openshell-driver-mxc/README.md | 8 +- crates/openshell-driver-mxc/src/driver.rs | 90 +++- crates/openshell-driver-mxc/src/mxc.rs | 83 +++- crates/openshell-driver-mxc/src/policy.rs | 102 +++- .../src/policy_map/loss.rs | 2 +- .../src/policy_map/map.rs | 93 +++- .../tests/policy_mapper_matrix.rs | 97 +++- .../tests/wxc_exec_real.rs | 134 +++++- crates/openshell-driver-podman/src/driver.rs | 1 + crates/openshell-driver-vm/src/driver.rs | 1 + crates/openshell-policy/src/lib.rs | 160 +++++- crates/openshell-server/src/compute/mod.rs | 114 +++++ crates/openshell-server/src/grpc/mod.rs | 1 + crates/openshell-server/src/grpc/policy.rs | 29 ++ .../openshell-server/src/grpc/validation.rs | 34 +- crates/openshell-server/src/test_support.rs | 1 + .../openshell-supervisor-network/src/opa.rs | 17 + docs/reference/policy-schema.mdx | 33 ++ docs/sandboxes/policies.mdx | 10 +- proto/compute_driver.proto | 4 + proto/openshell.proto | 4 + proto/sandbox.proto | 30 ++ .../v1/internal/converter/coverage_test.go | 1 + .../openshell/v1/internal/converter/health.go | 1 + .../v1/internal/converter/health_test.go | 8 +- .../openshell/v1/internal/converter/policy.go | 54 +++ .../v1/internal/converter/policy_test.go | 42 ++ sdk/go/openshell/v1/types/health.go | 7 +- sdk/go/openshell/v1/types/policy.go | 32 +- sdk/go/proto/openshellv1/openshell.pb.go | 23 +- sdk/go/proto/sandboxv1/sandbox.pb.go | 454 ++++++++++++------ skills/generate-sandbox-policy/SKILL.md | 30 +- skills/generate-sandbox-policy/examples.md | 2 +- skills/openshell-cli/SKILL.md | 4 +- tasks/scripts/windows-msvc.ps1 | 26 +- tasks/windows.toml | 9 +- 43 files changed, 1627 insertions(+), 208 deletions(-) diff --git a/.agents/skills/build-openshell-mxc-windows/SKILL.md b/.agents/skills/build-openshell-mxc-windows/SKILL.md index 5f0d617b08..ce66ac9672 100644 --- a/.agents/skills/build-openshell-mxc-windows/SKILL.md +++ b/.agents/skills/build-openshell-mxc-windows/SKILL.md @@ -154,6 +154,8 @@ mise run --skip-tools windows:build:x64 mise run --skip-tools windows:build:arm64 mise run --skip-tools windows:test:x64 mise run --skip-tools windows:test:unsupported:x64 +mise run --skip-tools windows:test:mxc-real:x64 +mise run --skip-tools windows:test:mxc-real:arm64 ``` For full validation, detect the Windows host architecture first and choose the @@ -235,6 +237,8 @@ crypto dependency builds. | `windows:test:arm64` | Runs native ARM64 workspace tests with `--no-fail-fast` and the same package exclusions. Rejects non-ARM64 hosts. | | `windows:test:unsupported:x64` | Re-runs focused `openshell-gateway` tests for unsupported Windows driver behavior. | | `windows:test:unsupported:arm64` | Re-runs the same focused contracts natively on ARM64. Rejects non-ARM64 hosts. | +| `windows:test:mxc-real:x64` | Runs the serial, ignored real-`wxc-exec` integration suite natively on x64 through the MSVC wrapper. Rejects non-x64 hosts. | +| `windows:test:mxc-real:arm64` | Runs the same real-`wxc-exec` suite natively on ARM64. Rejects non-ARM64 hosts. | | `windows:artifacts` | Reports size and SHA256 for release artifacts that exist. | | `windows:ci` | Runs the full ordered x64-host Windows CI lane, plus ARM64 check/build when not skipped. | @@ -292,6 +296,8 @@ Useful log files: | `test-aarch64-pc-windows-msvc.log` | Full native ARM64 workspace test output. | | `test-x86_64-pc-windows-msvc-unsupported-*.log` | Focused unsupported-driver contract output. | | `test-aarch64-pc-windows-msvc-unsupported-*.log` | Focused native ARM64 contract output. | +| `test-x86_64-pc-windows-msvc-mxc-real.log` | Native x64 real-MXC integration output. | +| `test-aarch64-pc-windows-msvc-mxc-real.log` | Native ARM64 real-MXC integration output. | The first bundled-Z3 check or test can spend several minutes in CMake/MSBuild without much console output because Cargo output is redirected to the log. Look diff --git a/architecture/compute-runtimes.md b/architecture/compute-runtimes.md index e1e731a0ce..cb7732b633 100644 --- a/architecture/compute-runtimes.md +++ b/architecture/compute-runtimes.md @@ -32,8 +32,8 @@ launches and monitors the policy-constrained workload itself. `compute_driver.proto` is the supported gateway/driver extension boundary. At initialization the gateway snapshots the driver's identity, version, -default image, gateway-lifecycle preference, and -`driver_reports_runtime_readiness` from `GetCapabilities`. The gateway includes +default image, gateway-lifecycle preference, runtime-readiness ownership, and +complete UI-policy enforcement support from `GetCapabilities`. The gateway includes the canonical `SandboxPolicy` in `DriverSandboxSpec.policy` for validation and creation. Drivers that enforce policy outside the standard supervisor fetch later revisions through `GetSandboxConfig` and acknowledge them through @@ -106,6 +106,15 @@ The capability RPC reports driver identity, version, and the default sandbox image used by the gateway. GPU availability stays driver-local and is validated when a sandbox create request asks for GPU resources. +UI policy is capability-negotiated at the configured driver-instance boundary. +`supports_ui_policy = true` means the driver completely enforces every field in +the current portable `SandboxPolicy.ui` contract; partial implementations must +report false. When `ui` is explicitly present, including as `{}`, the gateway +rejects create before the driver validation RPC or provisioning unless this +capability is true. An absent section bypasses this gate and preserves the +runtime's existing behavior. The startup snapshot is also exposed through +gateway info so clients can discover the selected runtime's support. + The gateway records driver identity and version from the startup capability response. Elevated gateway info reports that initialized driver snapshot instead of re-querying drivers on each request. diff --git a/architecture/security-policy.md b/architecture/security-policy.md index 62f5837e70..4c93c83990 100644 --- a/architecture/security-policy.md +++ b/architecture/security-policy.md @@ -8,19 +8,41 @@ per-request egress decisions. For the field-by-field YAML reference, use [Policy Schema Reference](../docs/reference/policy-schema.mdx). +## Windows MXC Static Enforcement + +On native Windows, the MXC driver cannot rely on Linux Landlock or setuid. It +maps portable static filesystem and UI controls from `SandboxPolicy` into MXC +configuration. UI controls are available only with the MXC +`process_container` backend; all omitted UI fields retain deny-by-default +values, and other compute runtimes reject an explicit UI policy. + ## Policy Areas | Area | Enforcement | |---|---| | Filesystem | Landlock restricts read-only and read-write paths. | | Process | The supervisor launches the agent as an unprivileged user with reduced capabilities. | +| UI | Within an explicit UI section, omitted display, clipboard, and input-injection fields deny. The MXC driver's OpenShell `process_container` backend (MXC containment `processcontainer`) can selectively enable them. Other configured backends reject the entire explicit section before provisioning. | | Network | The proxy evaluates destination, port, calling binary, and optional L7 rules. | | Inference | `inference.local` is configured through gateway inference settings, not OPA network policy. | | Runtime settings | Typed settings are delivered with policy and can be global or sandbox scoped. | -Filesystem and process policy are startup-time controls. Network policy is +Filesystem, process, and UI policy are startup-time controls. Network policy is dynamic and can be hot-reloaded when the new policy validates successfully. +The UI schema names portable capabilities rather than Windows primitives: +graphical output, directional clipboard access, and synthetic input. The +configured compute driver advertises whether it completely enforces this +contract. Any explicit section, including `{}`, is rejected before driver +validation or provisioning when that capability is false. The MXC +`process_container` mapper translates the fields to MXC's top-level `ui` object +under its `processcontainer` containment value and treats omitted fields inside +the section as deny. That object is common to MXC's 0.8 stable and 0.9 +development schemas. Both schema lines reject it for `isolation_session`, so +that backend advertises no support and the mapper also rejects it in depth. +Linux, macOS, and other non-MXC paths advertise no support: explicit UI policy +fails closed, while an absent section leaves their runtime behavior unchanged. + 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 diff --git a/crates/openshell-cli/src/commands/gateway.rs b/crates/openshell-cli/src/commands/gateway.rs index 0a7950050d..3c682931f6 100644 --- a/crates/openshell-cli/src/commands/gateway.rs +++ b/crates/openshell-cli/src/commands/gateway.rs @@ -45,6 +45,7 @@ struct ComputeDriverInfoView { struct ComputeDriverCapabilitiesView { driver_name: String, driver_version: String, + supports_ui_policy: bool, } /// Show gateway status. @@ -392,6 +393,7 @@ pub async fn gateway_info( capabilities: ComputeDriverCapabilitiesView { driver_name: capabilities.driver_name, driver_version: capabilities.driver_version, + supports_ui_policy: capabilities.supports_ui_policy, }, } }) @@ -446,6 +448,15 @@ fn print_compute_driver_info(drivers: &[ComputeDriverInfoView]) { "Driver version:".dimmed(), driver.capabilities.driver_version ); + println!( + " {} {}", + "UI policy:".dimmed(), + if driver.capabilities.supports_ui_policy { + "supported" + } else { + "unsupported" + } + ); } } @@ -464,6 +475,7 @@ fn gateway_info_to_json(view: &GatewayInfoView) -> serde_json::Value { "capabilities": { "driver_name": &driver.capabilities.driver_name, "driver_version": &driver.capabilities.driver_version, + "supports_ui_policy": driver.capabilities.supports_ui_policy, }, })) .collect::>(), @@ -1822,6 +1834,7 @@ mod tests { capabilities: ComputeDriverCapabilitiesView { driver_name: "podman".to_string(), driver_version: "0.0.75".to_string(), + supports_ui_policy: false, }, }], }; @@ -1840,6 +1853,10 @@ mod tests { json["compute_drivers"][0]["capabilities"]["driver_version"], "0.0.75" ); + assert_eq!( + json["compute_drivers"][0]["capabilities"]["supports_ui_policy"], + false + ); } #[test] diff --git a/crates/openshell-core/src/policy.rs b/crates/openshell-core/src/policy.rs index 1645b9da44..dd825fbf00 100644 --- a/crates/openshell-core/src/policy.rs +++ b/crates/openshell-core/src/policy.rs @@ -100,6 +100,10 @@ impl TryFrom for SandboxPolicy { type Error = miette::Report; fn try_from(proto: ProtoSandboxPolicy) -> Result { + // UI capabilities are intentionally absent from the portable supervisor + // runtime. Non-Windows compute paths do not expose them, so even a + // schema-level UI allowance cannot grant a UI surface there. The MXC + // driver consumes the typed proto directly on Windows. // In cluster mode we always run with proxy networking so all egress // can be evaluated by OPA and `inference.local` is always addressable. let network = NetworkPolicy { @@ -165,3 +169,31 @@ impl From for ProcessPolicy { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::proto::{UiClipboardAccess, UiPolicy}; + + #[test] + fn portable_runtime_does_not_activate_ui_allowances() { + let converted = SandboxPolicy::try_from(ProtoSandboxPolicy { + version: 1, + ui: Some(UiPolicy { + allow_graphical_ui: true, + clipboard: UiClipboardAccess::All as i32, + allow_input_injection: true, + }), + ..Default::default() + }) + .expect("portable policy conversion succeeds"); + + assert_eq!(converted.version, 1); + assert!(matches!(converted.network.mode, NetworkMode::Proxy)); + assert!(converted.network.proxy.is_some()); + assert!(converted.filesystem.read_only.is_empty()); + assert!(converted.filesystem.read_write.is_empty()); + assert!(converted.process.run_as_user.is_none()); + assert!(converted.process.run_as_group.is_none()); + } +} diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index d5df4c0b96..500e79bcc9 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -603,6 +603,7 @@ impl DockerComputeDriver { count_selection_supported: self.config.gpu.cdi_supported, }), }), + supports_ui_policy: false, } } diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index 39dab36317..c528d73c9f 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -589,6 +589,7 @@ impl KubernetesComputeDriver { count_selection_supported: true, }), }), + supports_ui_policy: false, }) } diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md index bd0b51f48b..1e9599263e 100644 --- a/crates/openshell-driver-mxc/README.md +++ b/crates/openshell-driver-mxc/README.md @@ -19,6 +19,7 @@ readiness; there is no in-sandbox supervisor or `ConnectSupervisor` relay. | Capability | MXC driver | |---|---| | Filesystem policy | Read-only/read-write grants come only from `SandboxPolicy`. `process_container` enforces default-deny; `isolation_session` is an explicit grant-only compatibility mode. | +| UI policy | `process_container` advertises complete support and maps portable graphical UI, clipboard-direction, and input-injection controls to MXC; omitted fields inside an explicit section deny. `isolation_session` advertises no support, so the gateway rejects any explicit section before provisioning. | | Network policy | Rejected synchronously during sandbox creation until an enforcing egress path is bound. | | Process policy | Unsupported; MXC supplies OS isolation only. | | Interactive exec/connect/forward | Unsupported; the configured workload runs in-driver. | @@ -75,11 +76,11 @@ invokes `wxc-exec`. Mapping failure therefore returns from `CreateSandbox` without leaving a partial sandbox. There is no in-process policy side channel or MXC-specific gateway composition variant. -`EmbeddedPolicyMapper` calls the embedded [`policy_map`](src/policy_map/) module directly and normalizes filesystem paths to Windows form. It does not add gateway-configured host paths. The policy supplied for the sandbox is the only source of filesystem grants. +`EmbeddedPolicyMapper` calls the embedded [`policy_map`](src/policy_map/) module directly and normalizes filesystem paths to Windows form. It does not add gateway-configured host paths. The policy supplied for the sandbox is the only source of filesystem grants. For `process_container`, the driver advertises `supports_ui_policy = true` and the mapper emits the top-level `ui` object shared by MXC's 0.8 stable and 0.9 development schemas, with restrictive defaults or the exact requested clipboard direction, graphical UI setting, and input-injection setting. `isolation_session` advertises false, so the gateway rejects explicit UI policy before the driver RPC; the mapper also rejects it before lifecycle side effects as defense in depth. The mapper retains an internal policy-splitting seam for future development, but the runtime exposes no governed-egress switch. Any network rule fails closed until an enforcing proxy is implemented and bound to the sandbox lifecycle. -Parity and matrix tests under [`tests/`](tests/) cover the mapper on the Windows MSVC lane. The driver performs this mapping automatically; there is no separate policy-export command or example. +Parity and matrix tests under [`tests/`](tests/) cover the mapper on the Windows MSVC lane. The real-MXC lane also dry-runs every clipboard direction against the installed schema. The driver performs this mapping automatically; there is no separate policy-export command or example. ## Packaging the demo for the demo box @@ -98,7 +99,8 @@ exits 0 rather than failing. | Task | What it runs | When to use | |---|---|---| -| `windows:test:mxc-real:x64` | `tests/wxc_exec_real.rs` — Tier-2 invoker tests with `--ignored --test-threads=1` | Pre-merge on any Windows host that has `wxc-exec`; dry-run tests always pass; enforcement tests probe-gate themselves | +| `windows:test:mxc-real:x64` | Native x64 `tests/wxc_exec_real.rs` — Tier-2 invoker tests with `--ignored --test-threads=1` | Pre-merge on an x64 Windows host with `wxc-exec`; dry-run tests always pass; enforcement tests probe-gate themselves | +| `windows:test:mxc-real:arm64` | Native ARM64 `tests/wxc_exec_real.rs` with the same contract | Pre-merge on an ARM64 Windows host with `wxc-exec` | | `windows:e2e:mxc` | `examples/run-mxc-e2e.ps1` — Tier-3 scenario runner, real binary, probe-gated | Demo box / nightly; needs the gateway + CLI binaries in the script directory | | `windows:e2e:mxc:mock` | Same runner with `-Mock` — wiring-only, no real `wxc-exec` needed | Any Windows host (CI, dev machine); validates wiring and the network-reject scenario | diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs index 28de9e33d7..e41bcfe626 100644 --- a/crates/openshell-driver-mxc/src/driver.rs +++ b/crates/openshell-driver-mxc/src/driver.rs @@ -47,6 +47,15 @@ pub enum MxcBackend { ProcessContainer, } +impl MxcBackend { + const fn containment(self) -> &'static str { + match self { + Self::IsolationSession => "isolation_session", + Self::ProcessContainer => "processcontainer", + } + } +} + /// Configuration for the MXC compute driver. /// /// Loaded from `[openshell.drivers.mxc]` in the gateway TOML file, or from @@ -292,10 +301,11 @@ impl MxcComputeBackend { supports_sandbox_authentication: false, driver_reports_runtime_readiness: true, resource_capabilities: None, + supports_ui_policy: self.config.backend == MxcBackend::ProcessContainer, } } - fn validate_sandbox_fields(&self, sandbox: &DriverSandbox) -> Result<(), tonic::Status> { + fn validate_sandbox_fields(sandbox: &DriverSandbox) -> Result<(), tonic::Status> { if let Some(spec) = &sandbox.spec { if effective_driver_gpu_count(driver_gpu_requirements( spec.resource_requirements.as_ref(), @@ -330,13 +340,14 @@ impl MxcComputeBackend { &MapCtx { sandbox_id: sandbox_id.to_string(), egress: None, + containment: self.config.backend.containment().into(), }, ) .map_err(|error| tonic::Status::invalid_argument(error.to_string())) } pub fn validate_sandbox_create(&self, sandbox: &DriverSandbox) -> Result<(), tonic::Status> { - self.validate_sandbox_fields(sandbox)?; + Self::validate_sandbox_fields(sandbox)?; let policy = sandbox.spec.as_ref().and_then(|spec| spec.policy.as_ref()); self.map_sandbox_policy(&sandbox.id, policy)?; Ok(()) @@ -357,7 +368,7 @@ impl MxcComputeBackend { pub async fn create_sandbox(&self, sandbox: &DriverSandbox) -> Result<(), tonic::Status> { let sandbox_id = sandbox.id.clone(); - self.validate_sandbox_fields(sandbox)?; + Self::validate_sandbox_fields(sandbox)?; let sandbox_config = sandbox_config(sandbox)?; // Policy translation is deterministic and side-effect free. Do it before @@ -626,6 +637,7 @@ async fn run_lifecycle( ) { let sandbox_id = sandbox.id.clone(); let sandbox_name = sandbox.name.clone(); + let ui = mapped.ui; let filesystem = MxcFilesystem { readwrite_paths: mapped.readwrite_paths, readonly_paths: mapped.readonly_paths, @@ -702,7 +714,14 @@ async fn run_lifecycle( capabilities: config.pc_capabilities.clone(), }; match invoker - .run_oneshot(&sandbox_id, filesystem, process_container, process, None) + .run_oneshot( + &sandbox_id, + filesystem, + process_container, + process, + None, + ui, + ) .await { Ok(child) => child, @@ -898,13 +917,25 @@ mod lifecycle_tests { use super::*; use futures::StreamExt; use openshell_core::proto::compute::v1::{DriverSandboxSpec, DriverSandboxTemplate}; - use openshell_core::proto::{FilesystemPolicy, SandboxPolicy}; + use openshell_core::proto::{FilesystemPolicy, SandboxPolicy, UiClipboardAccess, UiPolicy}; use std::time::Duration; fn driver_sandbox(id: &str) -> DriverSandbox { driver_sandbox_with_command(id, "", vec!["cmd".into(), "/c".into(), "exit 0".into()]) } + #[test] + fn ui_policy_capability_tracks_configured_backend() { + let process_container = MxcComputeBackend::new_mocked(MxcComputeConfig::default()); + assert!(process_container.capabilities().supports_ui_policy); + + let isolation_session = MxcComputeBackend::new_mocked(MxcComputeConfig { + backend: MxcBackend::IsolationSession, + ..Default::default() + }); + assert!(!isolation_session.capabilities().supports_ui_policy); + } + fn driver_sandbox_with_command(id: &str, cwd: &str, command: Vec) -> DriverSandbox { let serde_json::Value::Object(driver_config) = serde_json::json!({ "command": command, @@ -1102,6 +1133,9 @@ mod lifecycle_tests { recorded.get("network").is_none(), "coarse path must not emit an MXC network block" ); + assert_eq!(recorded["ui"]["disable"], true); + assert_eq!(recorded["ui"]["clipboard"], "none"); + assert_eq!(recorded["ui"]["injection"], false); let host_path = std::path::Path::new(tmp.path()).join("hello.txt"); let mut found = false; @@ -1118,6 +1152,52 @@ mod lifecycle_tests { ); } + #[tokio::test] + async fn processcontainer_live_config_carries_explicit_ui_policy() { + let backend = MxcComputeBackend::new_mocked(MxcComputeConfig::default()); + let mut policy = fs_policy(&[]); + policy.ui = Some(UiPolicy { + allow_graphical_ui: true, + clipboard: UiClipboardAccess::All as i32, + allow_input_injection: true, + }); + let sandbox = with_policy(driver_sandbox("sb-pc-ui"), policy); + backend + .create_sandbox(&sandbox) + .await + .expect("create accepted"); + let _ = wait_for(&backend, "sb-pc-ui", |_| { + crate::mxc::mock_recorded_config("sb-pc-ui").is_some() + }) + .await; + let recorded = crate::mxc::mock_recorded_config("sb-pc-ui") + .expect("mock recorded processContainer config"); + assert_eq!(recorded["ui"]["disable"], false); + assert_eq!(recorded["ui"]["clipboard"], "all"); + assert_eq!(recorded["ui"]["injection"], true); + } + + #[tokio::test] + async fn isolation_session_rejects_ui_before_lifecycle_side_effects() { + let backend = MxcComputeBackend::new_mocked(MxcComputeConfig { + backend: MxcBackend::IsolationSession, + ..Default::default() + }); + let policy = SandboxPolicy { + ui: Some(UiPolicy::default()), + ..Default::default() + }; + let sandbox = with_policy(driver_sandbox("sb-iso-ui"), policy); + let error = backend + .create_sandbox(&sandbox) + .await + .expect_err("isolation UI must be rejected synchronously"); + assert_eq!(error.code(), tonic::Code::InvalidArgument); + assert!(error.message().contains("ui")); + assert!(backend.list_sandboxes().await.is_empty()); + assert!(crate::mxc::mock_recorded_config("sb-iso-ui").is_none()); + } + #[tokio::test] async fn negative_out_of_policy_write_is_denied_with_event() { let share_tmp = tempfile::tempdir().unwrap(); diff --git a/crates/openshell-driver-mxc/src/mxc.rs b/crates/openshell-driver-mxc/src/mxc.rs index 32d7006d90..9f7b282dde 100644 --- a/crates/openshell-driver-mxc/src/mxc.rs +++ b/crates/openshell-driver-mxc/src/mxc.rs @@ -71,6 +71,42 @@ pub struct MxcNetwork { pub proxy: Option, } +/// Directional clipboard access in the MXC top-level `ui` policy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MxcClipboardAccess { + None, + Read, + Write, + All, +} + +impl MxcClipboardAccess { + const fn as_str(self) -> &'static str { + match self { + Self::None => "none", + Self::Read => "read", + Self::Write => "write", + Self::All => "all", + } + } +} + +/// Cross-platform MXC UI policy emitted for a process container. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MxcUi { + pub disable: bool, + pub clipboard: MxcClipboardAccess, + pub injection: bool, +} + +fn ui_json(ui: &MxcUi) -> serde_json::Value { + serde_json::json!({ + "disable": ui.disable, + "clipboard": ui.clipboard.as_str(), + "injection": ui.injection, + }) +} + /// `processContainer`-specific knobs (one-shot `AppContainer` backend). #[derive(Debug, Default, Clone)] pub struct MxcProcessContainer { @@ -142,6 +178,7 @@ fn oneshot_config_json( pc: &MxcProcessContainer, process: &MxcProcess, network: Option<&MxcNetwork>, + ui: Option<&MxcUi>, ) -> serde_json::Value { let mut filesystem_json = serde_json::Map::new(); if !filesystem.readwrite_paths.is_empty() { @@ -182,6 +219,9 @@ fn oneshot_config_json( if let Some(network) = network { config["network"] = network_json(network); } + if let Some(ui) = ui { + config["ui"] = ui_json(ui); + } config } @@ -571,9 +611,16 @@ impl WxcExecInvoker { pc: MxcProcessContainer, process: MxcProcess, network: Option, + ui: Option, ) -> Result { - let config = - oneshot_config_json(container_id, &filesystem, &pc, &process, network.as_ref()); + let config = oneshot_config_json( + container_id, + &filesystem, + &pc, + &process, + network.as_ref(), + ui.as_ref(), + ); if self.mock { let grants: Vec = filesystem .readwrite_paths @@ -807,9 +854,39 @@ mod tests { env: Vec::new(), timeout: 0, }; - let config = oneshot_config_json("sb-1", &filesystem, &pc, &process, None); + let config = oneshot_config_json("sb-1", &filesystem, &pc, &process, None, None); assert!(config.get("network").is_none()); + assert!(config.get("ui").is_none()); + } + + #[test] + fn oneshot_config_json_emits_typed_ui_policy() { + let filesystem = MxcFilesystem::default(); + let pc = MxcProcessContainer::default(); + let process = MxcProcess { + command_line: "cmd /c exit 0".into(), + cwd: "C:\\work\\demo".into(), + env: Vec::new(), + timeout: 0, + }; + let ui = MxcUi { + disable: false, + clipboard: MxcClipboardAccess::Write, + injection: true, + }; + let config = oneshot_config_json("sb-ui", &filesystem, &pc, &process, None, Some(&ui)); + + assert_eq!(config["ui"]["disable"], false); + assert_eq!(config["ui"]["clipboard"], "write"); + assert_eq!(config["ui"]["injection"], true); + } + + #[test] + fn isolation_provision_config_never_synthesizes_ui() { + let config = + provision_config_json(DEFAULT_CONFIGURATION_ID, &MxcFilesystem::default(), None); + assert!(config.get("ui").is_none()); } #[test] diff --git a/crates/openshell-driver-mxc/src/policy.rs b/crates/openshell-driver-mxc/src/policy.rs index a909238851..fa1e35434c 100644 --- a/crates/openshell-driver-mxc/src/policy.rs +++ b/crates/openshell-driver-mxc/src/policy.rs @@ -19,6 +19,7 @@ use std::net::SocketAddr; +use crate::mxc::{MxcClipboardAccess, MxcUi}; use openshell_core::proto::SandboxPolicy; use thiserror::Error; @@ -38,6 +39,9 @@ pub struct MappedConfig { /// Loopback address MXC redirects sandbox egress to. `None` when governed /// egress is disabled. pub proxy_addr: Option, + /// Top-level MXC UI policy for process containers. Isolation sessions keep + /// this absent because current MXC rejects the section on presence. + pub ui: Option, } /// Context passed to the mapper alongside the policy. @@ -49,6 +53,8 @@ pub struct MapCtx { /// Pattern-C governed-egress redirect address. When set, the embedded /// mapper uses `split_policy`; otherwise it uses the coarse MXC map. pub egress: Option, + /// MXC containment backend selected by the live driver. + pub containment: String, } /// A policy rule that the active mapper cannot enforce. @@ -109,6 +115,39 @@ fn extract_paths(config: &serde_json::Value, key: &str) -> Vec { .unwrap_or_default() } +fn extract_ui(config: &serde_json::Value) -> Result, MapError> { + let Some(ui) = config.get("ui") else { + return Ok(None); + }; + let disable = ui["disable"] + .as_bool() + .ok_or_else(|| MapError::Internal("mapped MXC ui.disable is not a boolean".into()))?; + let clipboard = match ui["clipboard"].as_str() { + Some("none") => MxcClipboardAccess::None, + Some("read") => MxcClipboardAccess::Read, + Some("write") => MxcClipboardAccess::Write, + Some("all") => MxcClipboardAccess::All, + Some(value) => { + return Err(MapError::Internal(format!( + "mapped MXC ui.clipboard has unknown value '{value}'" + ))); + } + None => { + return Err(MapError::Internal( + "mapped MXC ui.clipboard is not a string".into(), + )); + } + }; + let injection = ui["injection"] + .as_bool() + .ok_or_else(|| MapError::Internal("mapped MXC ui.injection is not a boolean".into()))?; + Ok(Some(MxcUi { + disable, + clipboard, + injection, + })) +} + impl PolicyMapper for EmbeddedPolicyMapper { fn map(&self, policy: Option<&SandboxPolicy>, ctx: &MapCtx) -> Result { let policy = policy.ok_or_else(|| { @@ -122,7 +161,7 @@ impl PolicyMapper for EmbeddedPolicyMapper { // Pattern C: MXC handles filesystem + a proxy redirect, while the // host CONNECT proxy receives the network-only trimmed policy. let opts = crate::policy_map::MxcMappingOptions { - containment: "processcontainer".to_owned(), + containment: ctx.containment.clone(), container_id: ctx.sandbox_id.clone(), proxy_redirect: Some(addr), ..Default::default() @@ -144,7 +183,7 @@ impl PolicyMapper for EmbeddedPolicyMapper { // yields an `error` loss for any host allowlist, which rejects // network policy below. let opts = crate::policy_map::MxcMappingOptions { - containment: "isolation_session".to_owned(), + containment: ctx.containment.clone(), container_id: ctx.sandbox_id.clone(), ..Default::default() }; @@ -176,12 +215,14 @@ impl PolicyMapper for EmbeddedPolicyMapper { .iter() .map(|p| normalize_path(p)) .collect(); + let ui = extract_ui(&config)?; Ok(MappedConfig { readwrite_paths: readwrite, readonly_paths: readonly, trimmed_policy, proxy_addr, + ui, }) } } @@ -195,6 +236,15 @@ mod tests { MapCtx { sandbox_id: "sb-test".into(), egress: None, + containment: "isolation_session".into(), + } + } + + fn processcontainer_ctx() -> MapCtx { + MapCtx { + sandbox_id: "sb-test".into(), + egress: None, + containment: "processcontainer".into(), } } @@ -253,6 +303,53 @@ mod tests { assert!(matches!(err, MapError::Unsupported(_))); } + #[test] + fn embedded_rejects_explicit_ui_on_isolation_session() { + use openshell_core::proto::UiPolicy; + + let mapper = EmbeddedPolicyMapper; + let policy = SandboxPolicy { + ui: Some(UiPolicy::default()), + ..Default::default() + }; + let err = mapper.map(Some(&policy), &demo_ctx()).unwrap_err(); + match err { + MapError::Unsupported(items) => { + assert_eq!(items.len(), 1); + assert_eq!(items[0].rule_kind, "ui"); + } + MapError::Internal(message) => { + panic!("expected unsupported UI, got internal error: {message}") + } + } + } + + #[test] + fn embedded_carries_typed_ui_for_processcontainer() { + use openshell_core::proto::{UiClipboardAccess, UiPolicy}; + + let mapper = EmbeddedPolicyMapper; + let policy = SandboxPolicy { + ui: Some(UiPolicy { + allow_graphical_ui: true, + clipboard: UiClipboardAccess::Read as i32, + allow_input_injection: true, + }), + ..Default::default() + }; + let result = mapper + .map(Some(&policy), &processcontainer_ctx()) + .expect("processContainer UI maps"); + assert_eq!( + result.ui, + Some(MxcUi { + disable: false, + clipboard: MxcClipboardAccess::Read, + injection: true, + }) + ); + } + #[test] fn embedded_split_normalizes_paths_and_returns_proxy_handoff() { use openshell_core::proto::{NetworkBinary, NetworkEndpoint, NetworkPolicyRule}; @@ -280,6 +377,7 @@ mod tests { sandbox_id: "sb-egress".into(), egress: Some(proxy_addr), + containment: "processcontainer".into(), }; let config = mapper.map(Some(&policy), &ctx).unwrap(); diff --git a/crates/openshell-driver-mxc/src/policy_map/loss.rs b/crates/openshell-driver-mxc/src/policy_map/loss.rs index 9e83c20a78..5166283371 100644 --- a/crates/openshell-driver-mxc/src/policy_map/loss.rs +++ b/crates/openshell-driver-mxc/src/policy_map/loss.rs @@ -25,7 +25,7 @@ pub struct LossItem { /// MXC capabilities that have no `OpenShell` *policy* equivalent. Surfaced in the /// loss report so reviewers understand the mapping is not symmetric. pub const OPEN_SHELL_SUPERSET_GAPS: &[&str] = &[ - "MXC UI policy has no OpenShell policy equivalent: ui.disable, ui.clipboard, and ui.injection.", + "MXC processContainer UI refinements have no portable OpenShell policy equivalent: isolation, desktopSystemControl, systemSettings, and ime.", "MXC lifecycle fields have no OpenShell policy equivalent: destroyOnExit, preservePolicy, phase, and sandboxId.", "MXC backend selection and backend-specific blocks are outside OpenShell policy YAML.", "MXC process command, cwd, env, and timeout are runtime config fields, not OpenShell policy fields.", diff --git a/crates/openshell-driver-mxc/src/policy_map/map.rs b/crates/openshell-driver-mxc/src/policy_map/map.rs index 7d9025e5a6..078622b5ef 100644 --- a/crates/openshell-driver-mxc/src/policy_map/map.rs +++ b/crates/openshell-driver-mxc/src/policy_map/map.rs @@ -11,7 +11,9 @@ use std::net::SocketAddr; -use openshell_core::proto::{NetworkEndpoint, NetworkPolicyRule, SandboxPolicy}; +use openshell_core::proto::{ + NetworkEndpoint, NetworkPolicyRule, SandboxPolicy, UiClipboardAccess, UiPolicy, +}; use serde_json::{Value, json}; use super::config::{ @@ -211,12 +213,10 @@ fn build_split_mxc_config( "process": process, "filesystem": filesystem, "network": network, - "ui": { - "disable": true, - "clipboard": "none", - "injection": false, - }, }); + if let Some(ui) = map_ui(policy.ui.as_ref(), &opts.containment, items) { + config["ui"] = ui; + } // No network hosts, so backend-specific network blocks (processContainer // internetClient, etc.) are not added — correct for the proxy path. @@ -264,18 +264,89 @@ fn build_mxc_config( "process": process, "filesystem": filesystem, "network": network, - "ui": { - "disable": true, - "clipboard": "none", - "injection": false, - }, }); + if let Some(ui) = map_ui(policy.ui.as_ref(), &opts.containment, items) { + config["ui"] = ui; + } add_backend_specific_config(&mut config, &opts.containment, &allowed_hosts, items); add_static_policy_loss(policy, opts, items); config } +fn map_ui(ui: Option<&UiPolicy>, containment: &str, items: &mut Vec) -> Option { + let restrictive = || { + json!({ + "disable": true, + "clipboard": "none", + "injection": false, + }) + }; + + match containment { + "processcontainer" | "process" => { + let Some(ui) = ui else { + // Preserve the mapper's existing deny posture for policies + // authored before the optional OpenShell UI section existed. + return Some(restrictive()); + }; + let clipboard = match UiClipboardAccess::try_from(ui.clipboard) { + Ok(UiClipboardAccess::Unspecified | UiClipboardAccess::None) => "none", + Ok(UiClipboardAccess::Read) => "read", + Ok(UiClipboardAccess::Write) => "write", + Ok(UiClipboardAccess::All) => "all", + Err(_) => { + add_loss( + items, + "ui.clipboard", + "error", + &format!( + "OpenShell UI clipboard policy has unknown enum value {}.", + ui.clipboard + ), + "directional clipboard access", + "MXC receives the restrictive clipboard=none fallback; sandbox creation is rejected.", + ); + "none" + } + }; + Some(json!({ + "disable": !ui.allow_graphical_ui, + "clipboard": clipboard, + "injection": ui.allow_input_injection, + })) + } + "isolation_session" => { + if ui.is_some() { + add_loss( + items, + "ui", + "error", + "MXC isolation_session rejects every explicitly supplied top-level UI policy, including an empty or deny-only policy.", + "OpenShell UI policy", + "The UI block is omitted and sandbox creation is rejected before wxc-exec is invoked.", + ); + } + None + } + _ => { + if ui.is_some() { + add_loss( + items, + "ui", + "error", + &format!( + "OpenShell UI policy enforcement is not supported by the MXC `{containment}` mapping target." + ), + "OpenShell UI policy", + "The generated config remains at the mapper's restrictive UI defaults and the caller must reject the mapping.", + ); + } + Some(restrictive()) + } + } +} + fn map_filesystem( policy: &SandboxPolicy, opts: &MxcMappingOptions, diff --git a/crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs b/crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs index 39a94bee9b..714cd94ec9 100644 --- a/crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs +++ b/crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs @@ -24,7 +24,7 @@ use openshell_core::proto::{ FilesystemPolicy, GraphqlOperation, L7Allow, L7DenyRule, L7Rule, LandlockPolicy, MiddlewareEndpointSelector, NetworkBinary, NetworkEndpoint, NetworkMiddlewareConfig, - NetworkPolicyRule, ProcessPolicy, SandboxPolicy, + NetworkPolicyRule, ProcessPolicy, SandboxPolicy, UiClipboardAccess, UiPolicy, }; use openshell_driver_mxc::{ EmbeddedPolicyMapper, MapCtx, MapError, MxcMappingOptions, PolicyMapper, map_to_mxc, @@ -82,6 +82,12 @@ fn pc_split_opts() -> MxcMappingOptions { } } +fn pc_opts() -> MxcMappingOptions { + MxcMappingOptions { + containment: "processcontainer".to_owned(), + ..Default::default() + } +} /// Build a minimal policy with one network rule whose endpoints carry a single /// endpoint set up by the caller. fn net_policy(key: &str, ep: NetworkEndpoint) -> SandboxPolicy { @@ -934,6 +940,7 @@ fn b_seam_returns_unsupported_on_error_field() { let ctx = MapCtx { sandbox_id: "sb-test".into(), egress: None, // coarse path → isolation_session → network policy errors + containment: "isolation_session".into(), }; let err = mapper.map(Some(&policy), &ctx).unwrap_err(); assert!( @@ -1039,6 +1046,90 @@ fn c_split_empty_allowed_hosts_with_network_rules() { ); } +#[test] +fn a_processcontainer_maps_ui_capabilities_exactly() { + for (clipboard, expected) in [ + (UiClipboardAccess::Unspecified, "none"), + (UiClipboardAccess::None, "none"), + (UiClipboardAccess::Read, "read"), + (UiClipboardAccess::Write, "write"), + (UiClipboardAccess::All, "all"), + ] { + let policy = SandboxPolicy { + ui: Some(UiPolicy { + allow_graphical_ui: true, + clipboard: clipboard as i32, + allow_input_injection: true, + }), + ..Default::default() + }; + let result = map_to_mxc(&policy, &pc_opts()); + assert_eq!(result.config["ui"]["disable"], false); + assert_eq!(result.config["ui"]["clipboard"], expected); + assert_eq!(result.config["ui"]["injection"], true); + assert_eq!(result.config["ui"].as_object().unwrap().len(), 3); + assert!(result.loss.iter().all(|item| item.path != "ui")); + } +} + +#[test] +fn c_processcontainer_absent_or_empty_ui_is_default_deny() { + for policy in [ + SandboxPolicy::default(), + SandboxPolicy { + ui: Some(UiPolicy::default()), + ..Default::default() + }, + ] { + let result = map_to_mxc(&policy, &pc_opts()); + assert_eq!(result.config["ui"]["disable"], true); + assert_eq!(result.config["ui"]["clipboard"], "none"); + assert_eq!(result.config["ui"]["injection"], false); + } +} + +#[test] +fn b_isolation_session_omits_absent_ui_and_rejects_explicit_ui() { + let opts = MxcMappingOptions { + containment: "isolation_session".into(), + ..Default::default() + }; + let absent = map_to_mxc(&SandboxPolicy::default(), &opts); + assert!(absent.config.get("ui").is_none()); + + let explicit = map_to_mxc( + &SandboxPolicy { + ui: Some(UiPolicy::default()), + ..Default::default() + }, + &opts, + ); + assert!(explicit.config.get("ui").is_none()); + assert_single_loss( + &explicit.loss, + "ui", + "error", + "isolation_session explicit UI", + ); +} + +#[test] +fn a_split_maps_ui_to_mxc_and_omits_it_from_proxy_policy() { + let policy = SandboxPolicy { + version: 1, + ui: Some(UiPolicy { + clipboard: UiClipboardAccess::Write as i32, + ..Default::default() + }), + ..Default::default() + }; + let result = split_policy(&policy, &pc_split_opts()).expect("split"); + assert_eq!(result.mxc_config["ui"]["disable"], true); + assert_eq!(result.mxc_config["ui"]["clipboard"], "write"); + assert_eq!(result.mxc_config["ui"]["injection"], false); + assert!(result.proxy_policy.ui.is_none()); +} + // ─── DRIFT GUARD ───────────────────────────────────────────────────────────── // // Serialize policies via openshell_policy::serialize_sandbox_policy, collect @@ -1053,11 +1144,14 @@ fn c_split_empty_allowed_hosts_with_network_rules() { /// "landlock" — loss item emitted in add_static_policy_loss /// "process" — loss items for run_as_user / run_as_group /// "network_policies" — mapped via map_network / delegated in split +/// "network_middlewares" — error loss in coarse map / delegated in split +/// "ui" — exact processContainer map / explicit unsupported loss const HANDLED_TOPLEVEL: &[&str] = &[ "version", "filesystem_policy", "landlock", "process", + "ui", "network_policies", "network_middlewares", ]; @@ -1128,6 +1222,7 @@ fn handled_fields_inventory() { run_as_user: "sandbox".into(), run_as_group: "sandbox".into(), }), + ui: Some(UiPolicy::default()), network_policies: { let mut m = std::collections::HashMap::new(); m.insert( diff --git a/crates/openshell-driver-mxc/tests/wxc_exec_real.rs b/crates/openshell-driver-mxc/tests/wxc_exec_real.rs index dd16836e4b..6279e06a2a 100644 --- a/crates/openshell-driver-mxc/tests/wxc_exec_real.rs +++ b/crates/openshell-driver-mxc/tests/wxc_exec_real.rs @@ -31,7 +31,7 @@ #![cfg(target_os = "windows")] use base64::Engine as _; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; // ── Path resolution ────────────────────────────────────────────────────────── @@ -64,10 +64,19 @@ fn wxc_path() -> Option { /// Invoke `wxc-exec --config-base64 --dry-run` synchronously. /// Returns `(exit_code, stdout, stderr)`. fn dry_run(wxc: &PathBuf, config: &serde_json::Value) -> (i32, String, String) { + dry_run_with_args(wxc, config, &[]) +} + +fn dry_run_with_args( + wxc: &PathBuf, + config: &serde_json::Value, + args: &[&str], +) -> (i32, String, String) { let json = serde_json::to_string(config).expect("config serialize"); let b64 = base64::engine::general_purpose::STANDARD.encode(json.as_bytes()); let out = Command::new(wxc) + .args(args) .arg("--config-base64") .arg(&b64) .arg("--dry-run") @@ -80,6 +89,32 @@ fn dry_run(wxc: &PathBuf, config: &serde_json::Value) -> (i32, String, String) { (code, stdout, stderr) } +fn wxc_version(wxc: &Path) -> Option<(u64, u64, u64, String)> { + // wxc-exec does not expose --version. Release builds carry the Cargo + // version in the standard Windows ProductVersion resource. + let path_literal = wxc.to_string_lossy().replace('\'', "''"); + let output = Command::new("powershell.exe") + .args(["-NoProfile", "-NonInteractive", "-Command"]) + .arg(format!( + "(Get-Item -LiteralPath '{path_literal}').VersionInfo.ProductVersion" + )) + .output() + .ok()?; + let raw = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let version = raw.split_whitespace().find_map(|token| { + let core = token + .trim_matches(|ch: char| !ch.is_ascii_digit() && ch != '.') + .split(['+', '-']) + .next()?; + let mut parts = core.split('.'); + let major = parts.next()?.parse().ok()?; + let minor = parts.next()?.parse().ok()?; + let patch = parts.next()?.parse().ok()?; + Some((major, minor, patch)) + })?; + Some((version.0, version.1, version.2, raw)) +} + // ── (a) Dry-run contract tests ──────────────────────────────────────────────── // // These PASS on any box that has the wxc-exec binary — no enforcement backend @@ -117,6 +152,103 @@ fn dryrun_accepts_minimal_processcontainer_config() { ); } +/// Every `OpenShell` clipboard direction maps to MXC's shared top-level UI +/// contract, with graphical UI and injection carried as independent booleans. +#[test] +#[ignore = "requires real wxc-exec"] +fn dryrun_accepts_processcontainer_ui_policy_matrix() { + let Some(wxc) = wxc_path() else { + eprintln!("SKIP: wxc-exec not found"); + return; + }; + + let tempdir = tempfile::tempdir().expect("tempdir"); + let temp_path = tempdir.path().to_string_lossy().into_owned(); + for clipboard in ["none", "read", "write", "all"] { + let config = serde_json::json!({ + "version": "0.7.0-alpha", + "containerId": format!("test-ui-{clipboard}"), + "containment": "processcontainer", + "process": { + "commandLine": "cmd /c exit 0", + "cwd": temp_path.clone(), + "timeout": 0, + }, + "filesystem": { + "readwritePaths": [temp_path.clone()], + }, + "ui": { + "disable": false, + "clipboard": clipboard, + "injection": true, + }, + }); + + let (code, stdout, stderr) = dry_run(&wxc, &config); + assert_eq!( + code, 0, + "processcontainer UI policy clipboard={clipboard} rejected by --dry-run\nstdout={stdout}\nstderr={stderr}" + ); + } +} + +/// MXC 0.8 and the 0.9 development schema reject the shared top-level +/// UI object on `isolation_session`, while omission remains accepted. Older +/// 0.7 builds accepted and ignored the object, so `OpenShell`'s gateway-level +/// capability check is the stable enforcement boundary across versions. +#[test] +#[ignore = "requires real wxc-exec"] +fn dryrun_current_schema_rejects_isolation_session_ui() { + let Some(wxc) = wxc_path() else { + eprintln!("SKIP: wxc-exec not found"); + return; + }; + let Some((major, minor, _patch, raw_version)) = wxc_version(&wxc) else { + eprintln!("SKIP: could not determine wxc-exec version"); + return; + }; + if (major, minor) < (0, 8) { + eprintln!("SKIP: {raw_version} predates the isolation_session UI rejection contract"); + return; + } + + let base = serde_json::json!({ + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true, + }, + }); + let (code, stdout, stderr) = dry_run_with_args(&wxc, &base, &["--experimental"]); + let output = format!("{stdout} {stderr}").to_ascii_lowercase(); + if code != 0 + && output.contains("backend_unavailable") + && output.contains("not available in this build") + { + eprintln!("SKIP: {raw_version} was built without isolation_session support"); + return; + } + assert_eq!( + code, 0, + "current isolation_session schema must accept omission of UI\nversion={raw_version}\nstdout={stdout}\nstderr={stderr}" + ); + + let mut with_ui = base; + with_ui["ui"] = serde_json::json!({ "disable": true }); + let (code, stdout, stderr) = dry_run_with_args(&wxc, &with_ui, &["--experimental"]); + assert_ne!( + code, 0, + "current isolation_session schema unexpectedly accepted UI\nversion={raw_version}\nstdout={stdout}\nstderr={stderr}" + ); + assert!( + format!("{stdout} {stderr}") + .to_ascii_lowercase() + .contains("ui"), + "rejection should identify UI\nversion={raw_version}\nstdout={stdout}\nstderr={stderr}" + ); +} + /// Network block without proxy (defaultPolicy block, empty host lists) accepted. #[test] #[ignore = "requires real wxc-exec"] diff --git a/crates/openshell-driver-podman/src/driver.rs b/crates/openshell-driver-podman/src/driver.rs index 8d0957f87a..d5e76111b1 100644 --- a/crates/openshell-driver-podman/src/driver.rs +++ b/crates/openshell-driver-podman/src/driver.rs @@ -529,6 +529,7 @@ impl PodmanComputeDriver { count_selection_supported: true, }), }), + supports_ui_policy: false, }) } diff --git a/crates/openshell-driver-vm/src/driver.rs b/crates/openshell-driver-vm/src/driver.rs index aa9f4288c1..5d42b7310c 100644 --- a/crates/openshell-driver-vm/src/driver.rs +++ b/crates/openshell-driver-vm/src/driver.rs @@ -685,6 +685,7 @@ impl VmDriver { count_selection_supported: self.config.gpu_enabled, }), }), + supports_ui_policy: false, } } diff --git a/crates/openshell-policy/src/lib.rs b/crates/openshell-policy/src/lib.rs index 5f3ec4e452..ca7a886fbd 100644 --- a/crates/openshell-policy/src/lib.rs +++ b/crates/openshell-policy/src/lib.rs @@ -29,7 +29,7 @@ use openshell_core::mcp::{DEFAULT_MCP_PROTOCOL_VERSION, McpProtocolVersion}; use openshell_core::proto::{ FilesystemPolicy, GraphqlOperation, L7Allow, L7DenyRule, L7QueryMatcher, L7Rule, LandlockPolicy, McpOptions, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, ProcessPolicy, - SandboxPolicy, + SandboxPolicy, UiClipboardAccess, UiPolicy, }; use serde::{Deserialize, Deserializer, Serialize}; @@ -63,6 +63,8 @@ struct PolicyFile { landlock: Option, #[serde(default, skip_serializing_if = "Option::is_none")] process: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + ui: Option, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] network_policies: BTreeMap, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] @@ -96,6 +98,53 @@ struct ProcessDef { run_as_group: String, } +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct UiDef { + #[serde(default, skip_serializing_if = "is_false")] + allow_graphical_ui: bool, + #[serde(default, skip_serializing_if = "UiClipboardAccessDef::is_none")] + clipboard: UiClipboardAccessDef, + #[serde(default, skip_serializing_if = "is_false")] + allow_input_injection: bool, +} + +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum UiClipboardAccessDef { + #[default] + None, + Read, + Write, + All, +} + +impl UiClipboardAccessDef { + // Signature dictated by serde's `skip_serializing_if`, which requires `&T`. + #[allow(clippy::trivially_copy_pass_by_ref)] + const fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + const fn to_proto(self) -> UiClipboardAccess { + match self { + Self::None => UiClipboardAccess::None, + Self::Read => UiClipboardAccess::Read, + Self::Write => UiClipboardAccess::Write, + Self::All => UiClipboardAccess::All, + } + } + + fn from_proto(value: i32) -> Self { + match UiClipboardAccess::try_from(value).unwrap_or(UiClipboardAccess::Unspecified) { + UiClipboardAccess::Unspecified | UiClipboardAccess::None => Self::None, + UiClipboardAccess::Read => Self::Read, + UiClipboardAccess::Write => Self::Write, + UiClipboardAccess::All => Self::All, + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] struct NetworkPolicyRuleDef { @@ -194,6 +243,12 @@ 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_false(v: &bool) -> bool { + !*v +} + // 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 { @@ -921,6 +976,11 @@ fn to_proto(raw: PolicyFile) -> Result { run_as_user: p.run_as_user, run_as_group: p.run_as_group, }), + ui: raw.ui.map(|ui| UiPolicy { + allow_graphical_ui: ui.allow_graphical_ui, + clipboard: ui.clipboard.to_proto() as i32, + allow_input_injection: ui.allow_input_injection, + }), network_policies, network_middlewares, }) @@ -952,6 +1012,12 @@ fn from_proto(policy: &SandboxPolicy) -> PolicyFile { } }); + let ui = policy.ui.as_ref().map(|ui| UiDef { + allow_graphical_ui: ui.allow_graphical_ui, + clipboard: UiClipboardAccessDef::from_proto(ui.clipboard), + allow_input_injection: ui.allow_input_injection, + }); + let network_policies = policy .network_policies .iter() @@ -1068,6 +1134,7 @@ fn from_proto(policy: &SandboxPolicy) -> PolicyFile { filesystem_policy, landlock, process, + ui, network_policies, network_middlewares, } @@ -1266,6 +1333,7 @@ pub fn restrictive_default_policy() -> SandboxPolicy { compatibility: "best_effort".into(), }), process: None, + ui: None, network_policies: HashMap::new(), network_middlewares: HashMap::default(), } @@ -1301,6 +1369,8 @@ const MAX_PATH_LENGTH: usize = 4096; pub enum PolicyViolation { /// An explicit `run_as_user` or `run_as_group` is unsafe. InvalidProcessIdentity { field: &'static str, value: String }, + /// The protobuf carries a clipboard enum value unknown to this version. + InvalidUiClipboardAccess { value: i32 }, /// A filesystem path contains `..` components. PathTraversal { path: String }, /// A filesystem path is not absolute (does not start with `/`). @@ -1400,6 +1470,12 @@ impl fmt::Display for PolicyViolation { "{field} must be 'sandbox' or a numeric UID/GID in range [{MIN_SANDBOX_UID}, {MAX_SANDBOX_UID}], got '{value}'" ) } + Self::InvalidUiClipboardAccess { value } => { + write!( + f, + "ui clipboard access has unknown enum value {value}; expected unspecified, none, read, write, or all" + ) + } Self::PathTraversal { path } => { write!(f, "path contains '..' traversal component: {path}") } @@ -1599,6 +1675,7 @@ impl fmt::Display for PolicyViolation { /// /// Checks performed: /// - Explicit `run_as_user` / `run_as_group` fields must be safe identities +/// - UI clipboard access must use a recognized enum value /// - Filesystem paths must be absolute (start with `/`) /// - Filesystem paths must not contain `..` components /// - Read-write paths must not be overly broad (just `/`) @@ -1647,6 +1724,14 @@ fn validate_sandbox_policy_with_mcp_presence( } } + if let Some(ref ui) = policy.ui + && UiClipboardAccess::try_from(ui.clipboard).is_err() + { + violations.push(PolicyViolation::InvalidUiClipboardAccess { + value: ui.clipboard, + }); + } + // Check filesystem paths if let Some(ref fs) = policy.filesystem { let total_paths = fs.read_only.len() + fs.read_write.len(); @@ -2239,6 +2324,75 @@ network_policies: assert!(json.get("network_policies").is_some()); } + #[test] + fn ui_absence_and_explicit_empty_remain_distinct() { + let absent = parse_sandbox_policy("version: 1\n").expect("absent UI parses"); + assert!(absent.ui.is_none()); + let absent_yaml = serialize_sandbox_policy(&absent).expect("absent UI serializes"); + assert!(!absent_yaml.contains("\nui:")); + + let explicit = parse_sandbox_policy("version: 1\nui: {}\n").expect("empty UI parses"); + let ui = explicit.ui.as_ref().expect("UI presence preserved"); + assert!(!ui.allow_graphical_ui); + assert_eq!(ui.clipboard, UiClipboardAccess::None as i32); + assert!(!ui.allow_input_injection); + + let explicit_yaml = serialize_sandbox_policy(&explicit).expect("empty UI serializes"); + assert!(explicit_yaml.contains("ui: {}"), "got:\n{explicit_yaml}"); + let reparsed = parse_sandbox_policy(&explicit_yaml).expect("empty UI reparses"); + assert!(reparsed.ui.is_some()); + } + + #[test] + fn ui_policy_round_trips_all_clipboard_directions() { + for (wire, expected) in [ + ("none", UiClipboardAccess::None), + ("read", UiClipboardAccess::Read), + ("write", UiClipboardAccess::Write), + ("all", UiClipboardAccess::All), + ] { + let yaml = format!( + "version: 1\nui:\n allow_graphical_ui: true\n clipboard: {wire}\n allow_input_injection: true\n" + ); + let policy = parse_sandbox_policy(&yaml).expect("UI policy parses"); + let ui = policy.ui.as_ref().expect("UI policy present"); + assert!(ui.allow_graphical_ui); + assert_eq!(ui.clipboard, expected as i32); + assert!(ui.allow_input_injection); + + let serialized = serialize_sandbox_policy(&policy).expect("UI policy serializes"); + let reparsed = parse_sandbox_policy(&serialized).expect("UI policy reparses"); + assert_eq!(reparsed, policy); + } + } + + #[test] + fn ui_policy_rejects_unknown_yaml_clipboard_value() { + let error = parse_sandbox_policy("version: 1\nui:\n clipboard: execute\n") + .expect_err("unknown clipboard value must fail"); + assert!( + error + .to_string() + .contains("failed to parse sandbox policy YAML") + ); + } + + #[test] + fn ui_policy_validation_rejects_unknown_proto_clipboard_value() { + let policy = SandboxPolicy { + ui: Some(UiPolicy { + clipboard: 99, + ..Default::default() + }), + ..Default::default() + }; + let violations = validate_sandbox_policy(&policy).expect_err("unknown enum must fail"); + assert_eq!( + violations, + vec![PolicyViolation::InvalidUiClipboardAccess { value: 99 }] + ); + } + /// Verify that `allowed_ips` survives the round-trip. #[test] fn round_trip_preserves_allowed_ips() { @@ -3712,6 +3866,7 @@ network_policies: process: None, filesystem: None, landlock: None, + ui: None, network_policies: HashMap::new(), network_middlewares: HashMap::default(), }; @@ -4170,6 +4325,7 @@ network_policies: }), filesystem: None, landlock: None, + ui: None, network_policies: HashMap::new(), network_middlewares: HashMap::default(), }; @@ -4186,6 +4342,7 @@ network_policies: }), filesystem: None, landlock: None, + ui: None, network_policies: HashMap::new(), network_middlewares: HashMap::default(), }; @@ -4258,6 +4415,7 @@ network_policies: }), filesystem: None, landlock: None, + ui: None, network_policies: HashMap::new(), network_middlewares: HashMap::default(), }; diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 42efab3d6f..14d68ff271 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -292,6 +292,7 @@ enum BeginDelete { } #[derive(Debug, Clone)] +#[allow(clippy::struct_excessive_bools)] pub struct ComputeDriverInfoSnapshot { /// Gateway-selected driver name used for routing and `driver_config` keys. pub name: String, @@ -307,6 +308,9 @@ pub struct ComputeDriverInfoSnapshot { pub driver_reports_runtime_readiness: bool, /// Static portable resource request forms from the startup capability snapshot. pub resource_capabilities: Option, + /// Whether this configured driver instance completely enforces the portable + /// UI policy contract. + pub supports_ui_policy: bool, } /// Interval between store-vs-backend reconciliation sweeps. @@ -656,6 +660,7 @@ impl ComputeRuntime { supports_sandbox_authentication: capabilities.supports_sandbox_authentication, driver_reports_runtime_readiness: capabilities.driver_reports_runtime_readiness, resource_capabilities: capabilities.resource_capabilities, + supports_ui_policy: capabilities.supports_ui_policy, }; let default_image = capabilities.default_image; let gateway_listener_requirements = match driver @@ -877,6 +882,7 @@ impl ComputeRuntime { } pub async fn validate_sandbox_create(&self, sandbox: &Sandbox) -> Result<(), Status> { + self.validate_policy_capabilities(sandbox)?; let driver_sandbox = driver_sandbox_from_public(sandbox, &self.driver_info.name) .map_err(|status| *status)?; self.driver @@ -895,12 +901,31 @@ impl ComputeRuntime { .map(|_| ()) } + fn validate_policy_capabilities(&self, sandbox: &Sandbox) -> Result<(), Status> { + let has_explicit_ui = sandbox + .spec + .as_ref() + .and_then(|spec| spec.policy.as_ref()) + .and_then(|policy| policy.ui.as_ref()) + .is_some(); + if has_explicit_ui && !self.driver_info.supports_ui_policy { + return Err(Status::invalid_argument(format!( + "compute driver '{}' does not support the complete UI policy contract; remove the explicit ui section or select a supporting driver/backend", + self.driver_info.name + ))); + } + Ok(()) + } + pub async fn create_sandbox( &self, sandbox: Sandbox, sandbox_token: Option, await_main_process_attachment: bool, ) -> Result { + // Defense in depth for internal callers that bypass the public create + // handler's ValidateSandboxCreate step. This check has no side effects. + self.validate_policy_capabilities(&sandbox)?; let sandbox_id = sandbox.object_id().to_string(); let mut driver_sandbox = driver_sandbox_from_public(&sandbox, &self.driver_info.name) .map_err(|status| *status)?; @@ -4659,6 +4684,7 @@ impl ComputeDriver for NoopTestDriver { supports_sandbox_authentication: self.sandbox_authentication.is_some(), driver_reports_runtime_readiness: false, resource_capabilities: None, + supports_ui_policy: false, }, )) } @@ -4804,6 +4830,7 @@ pub async fn new_test_runtime_with_driver( supports_sandbox_authentication, driver_reports_runtime_readiness: false, resource_capabilities: None, + supports_ui_policy: false, }, telemetry_compute_driver: TelemetryComputeDriver::custom(), driver_process: None, @@ -4829,6 +4856,7 @@ mod tests { GetSandboxResponse, StartSandboxResponse, StopSandboxRequest, StopSandboxResponse, ValidateSandboxCreateResponse, WatchSandboxesDeletedEvent, WatchSandboxesSandboxEvent, }; + use openshell_core::proto::{SandboxPolicy as PublicSandboxPolicy, UiPolicy}; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex as TestMutex}; @@ -4953,6 +4981,8 @@ mod tests { listed_sandboxes: Vec, current_sandboxes: Vec, workspace_rpcs_unimplemented: bool, + validate_create_calls: AtomicUsize, + create_calls: AtomicUsize, } #[tonic::async_trait] @@ -4983,6 +5013,7 @@ mod tests { supports_sandbox_authentication: false, driver_reports_runtime_readiness: false, resource_capabilities: None, + supports_ui_policy: false, })) } @@ -4999,6 +5030,7 @@ mod tests { &self, _request: Request, ) -> Result, Status> { + self.validate_create_calls.fetch_add(1, Ordering::Relaxed); Ok(tonic::Response::new(ValidateSandboxCreateResponse {})) } @@ -5050,6 +5082,7 @@ mod tests { &self, _request: Request, ) -> Result, Status> { + self.create_calls.fetch_add(1, Ordering::Relaxed); Ok(tonic::Response::new(CreateSandboxResponse {})) } @@ -5325,6 +5358,7 @@ mod tests { supports_sandbox_authentication: false, driver_reports_runtime_readiness: false, resource_capabilities: None, + supports_ui_policy: false, })) } @@ -5536,6 +5570,7 @@ mod tests { supports_sandbox_authentication: false, driver_reports_runtime_readiness: false, resource_capabilities: None, + supports_ui_policy: false, }, telemetry_compute_driver: TelemetryComputeDriver::custom(), driver_process: None, @@ -5552,6 +5587,83 @@ mod tests { } } + fn sandbox_with_explicit_ui(id: &str) -> Sandbox { + let mut sandbox = sandbox_record(id, "ui-policy", SandboxPhase::Provisioning); + sandbox.spec = Some(SandboxSpec { + policy: Some(PublicSandboxPolicy { + ui: Some(UiPolicy::default()), + ..Default::default() + }), + ..Default::default() + }); + sandbox + } + + #[tokio::test] + async fn explicit_ui_policy_rejects_before_unsupported_driver_validation() { + let driver = Arc::new(TestDriver::default()); + let runtime = test_runtime(driver.clone()).await; + + let error = runtime + .validate_sandbox_create(&sandbox_with_explicit_ui("sb-ui-validate")) + .await + .expect_err("an unsupported driver must reject explicit UI policy"); + + assert_eq!(error.code(), Code::InvalidArgument); + assert!(error.message().contains("complete UI policy contract")); + assert_eq!( + driver.validate_create_calls.load(Ordering::Relaxed), + 0, + "gateway capability validation must run before the driver RPC" + ); + } + + #[tokio::test] + async fn explicit_ui_policy_rejects_before_unsupported_driver_create() { + let driver = Arc::new(TestDriver::default()); + let runtime = test_runtime(driver.clone()).await; + + let error = runtime + .create_sandbox(sandbox_with_explicit_ui("sb-ui-create"), None, false) + .await + .expect_err("an internal caller must not bypass UI capability validation"); + + assert_eq!(error.code(), Code::InvalidArgument); + assert_eq!( + driver.create_calls.load(Ordering::Relaxed), + 0, + "unsupported UI policy must fail before provisioning" + ); + } + + #[tokio::test] + async fn explicit_ui_policy_reaches_driver_when_capability_is_complete() { + let driver = Arc::new(TestDriver::default()); + let mut runtime = test_runtime(driver.clone()).await; + runtime.driver_info.supports_ui_policy = true; + + runtime + .validate_sandbox_create(&sandbox_with_explicit_ui("sb-ui-supported")) + .await + .expect("a driver advertising complete UI support accepts validation"); + + assert_eq!(driver.validate_create_calls.load(Ordering::Relaxed), 1); + } + + #[tokio::test] + async fn absent_ui_policy_preserves_unsupported_driver_behavior() { + let driver = Arc::new(TestDriver::default()); + let runtime = test_runtime(driver.clone()).await; + let sandbox = sandbox_record("sb-no-ui", "no-ui-policy", SandboxPhase::Provisioning); + + runtime + .validate_sandbox_create(&sandbox) + .await + .expect("an absent UI section must preserve existing behavior"); + + assert_eq!(driver.validate_create_calls.load(Ordering::Relaxed), 1); + } + async fn test_runtime_with_gateway_managed_lifecycle( driver: SharedComputeDriver, driver_name: &str, @@ -9650,6 +9762,7 @@ mod tests { }), workspace: "default".to_string(), }], + ..Default::default() })) .await; @@ -9821,6 +9934,7 @@ mod tests { })), workspace: "default".to_string(), }], + ..Default::default() })) .await; diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index a88a2e3414..30ff71fc67 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -264,6 +264,7 @@ impl OpenShell for OpenShellService { .resource_capabilities .as_ref() .map(|resources| public_resource_capabilities(*resources)), + supports_ui_policy: driver.supports_ui_policy, }), }) .collect(); diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 7e588fcac8..5c47b1a109 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -18068,6 +18068,35 @@ mod tests { ); } + #[test] + fn policy_hash_distinguishes_ui_absence_presence_and_values() { + use openshell_core::proto::{UiClipboardAccess, UiPolicy}; + + let absent = ProtoSandboxPolicy::default(); + let explicit_deny = ProtoSandboxPolicy { + ui: Some(UiPolicy::default()), + ..Default::default() + }; + let clipboard_read = ProtoSandboxPolicy { + ui: Some(UiPolicy { + clipboard: UiClipboardAccess::Read as i32, + ..Default::default() + }), + ..Default::default() + }; + + assert_ne!( + deterministic_policy_hash(&absent), + deterministic_policy_hash(&explicit_deny), + "an explicitly present deny-only UI block remains hash-significant" + ); + assert_ne!( + deterministic_policy_hash(&explicit_deny), + deterministic_policy_hash(&clipboard_read), + "UI capability changes must produce a new policy hash" + ); + } + #[test] fn policy_hash_is_stable_across_middleware_config_field_insertion_order() { use prost_types::{Struct, Value, value::Kind}; diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index 2f22199df1..dcef6d37c1 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -907,7 +907,7 @@ pub(super) fn validate_no_reserved_provider_policy_keys( Ok(()) } -/// Validate that static policy fields (filesystem, landlock, process) haven't changed +/// Validate that static policy fields (filesystem, landlock, process, UI) haven't changed /// from the baseline (version 1) policy. pub(super) fn validate_static_fields_unchanged( baseline: &ProtoSandboxPolicy, @@ -931,6 +931,11 @@ pub(super) fn validate_static_fields_unchanged( "process policy cannot be changed on a live sandbox (applied at startup)", )); } + if baseline.ui != new.ui { + return Err(Status::invalid_argument( + "UI policy cannot be changed on a live sandbox (applied at startup)", + )); + } Ok(()) } @@ -2198,6 +2203,33 @@ mod tests { assert!(result.unwrap_err().message().contains("include_workdir")); } + #[test] + fn validate_static_fields_rejects_ui_presence_or_value_change() { + use openshell_core::proto::{UiClipboardAccess, UiPolicy}; + + let absent = ProtoSandboxPolicy::default(); + let deny = ProtoSandboxPolicy { + ui: Some(UiPolicy::default()), + ..Default::default() + }; + let allow_clipboard = ProtoSandboxPolicy { + ui: Some(UiPolicy { + clipboard: UiClipboardAccess::Read as i32, + ..Default::default() + }), + ..Default::default() + }; + + let presence_error = validate_static_fields_unchanged(&absent, &deny) + .expect_err("adding explicit UI policy must be static"); + assert!(presence_error.message().contains("UI policy")); + + let value_error = validate_static_fields_unchanged(&deny, &allow_clipboard) + .expect_err("changing UI policy must be static"); + assert!(value_error.message().contains("UI policy")); + assert!(validate_static_fields_unchanged(&deny, &deny).is_ok()); + } + // ---- Exec validation ---- #[test] diff --git a/crates/openshell-server/src/test_support.rs b/crates/openshell-server/src/test_support.rs index 3af65242ef..bb91228dcb 100644 --- a/crates/openshell-server/src/test_support.rs +++ b/crates/openshell-server/src/test_support.rs @@ -98,6 +98,7 @@ impl FakeComputeDriver { supports_sandbox_authentication: false, driver_reports_runtime_readiness: false, resource_capabilities: None, + supports_ui_policy: false, }, gateway_listener_requirements: Vec::new(), gateway_listener_requirements_supported: true, diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 3303aa89b5..0818e63b57 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -2283,6 +2283,7 @@ mod tests { run_as_user: "sandbox".to_string(), run_as_group: "sandbox".to_string(), }), + ui: None, network_policies, network_middlewares: std::collections::HashMap::default(), } @@ -3379,6 +3380,7 @@ process: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto_with_pid_and_binary_identity_required(&proto, 0, false) .expect("engine from relaxed proto"); @@ -3918,6 +3920,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); @@ -3990,6 +3993,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); @@ -4067,6 +4071,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); @@ -5019,6 +5024,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); @@ -5077,6 +5083,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); @@ -5136,6 +5143,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); @@ -5197,6 +5205,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); @@ -5257,6 +5266,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); @@ -6593,6 +6603,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); let input = NetworkInput { @@ -6648,6 +6659,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("engine from proto"); let input = NetworkInput { @@ -6719,6 +6731,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).expect("Failed to create engine from proto"); @@ -6950,6 +6963,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let engine = OpaEngine::from_proto(&proto).unwrap(); // Port 443 @@ -7919,6 +7933,7 @@ network_policies: process: None, network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; let pid = std::process::id(); // accessible root, leaf paths absent @@ -8561,6 +8576,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; // Build engine with our PID (symlink resolution will work via /proc/self/root/) @@ -8639,6 +8655,7 @@ network_policies: }), network_policies, network_middlewares: std::collections::HashMap::default(), + ui: None, }; // Initial load at pid=0 — no symlink expansion diff --git a/docs/reference/policy-schema.mdx b/docs/reference/policy-schema.mdx index 29eafa60de..3682ae68fa 100644 --- a/docs/reference/policy-schema.mdx +++ b/docs/reference/policy-schema.mdx @@ -19,6 +19,7 @@ version: 1 filesystem_policy: { ... } landlock: { ... } process: { ... } +ui: { ... } network_policies: { ... } network_middlewares: { ... } ``` @@ -29,6 +30,7 @@ network_middlewares: { ... } | `filesystem_policy` | object | No | Static | Controls which directories the agent can read and write. | | `landlock` | object | No | Static | Configures Landlock LSM enforcement behavior. | | `process` | object | No | Static | Sets the user and group the agent process runs as. | +| `ui` | object | No | Static | Controls graphical UI, directional clipboard access, and synthetic input on compute drivers that advertise complete enforcement. | | `network_policies` | map | No | Dynamic | Declares which binaries can reach which network endpoints. | | `network_middlewares` | map | No | Dynamic | Attaches ordered middleware by destination host; each implementation's manifest selects its supported HTTP and WebSocket operations. | @@ -138,6 +140,37 @@ process: run_as_group: "1500" ``` +## UI + +**Category:** Static + +Declares platform-neutral UI capabilities. Within an explicit section, every +field is deny by default, so `ui: {}` grants nothing on a supporting backend. +Omit the entire section when the workload does not need a UI surface. + +| Field | Type | Required | Values | Description | +|---|---|---|---|---| +| `allow_graphical_ui` | bool | No | `true`, `false` | Allows the workload to display graphical windows. Defaults to `false`. | +| `clipboard` | string | No | `none`, `read`, `write`, `all` | Controls host clipboard direction from the sandbox's perspective. Defaults to `none`. | +| `allow_input_injection` | bool | No | `true`, `false` | Allows synthetic keyboard or pointer input. Defaults to `false`. | + +MXC `process_container` enforces this section on Windows and advertises complete +support through the compute-driver capability contract. OpenShell rejects any +explicit UI section, including `{}`, with MXC `isolation_session` because +current MXC rejects the top-level `ui` object there. Docker, Podman, Kubernetes, +VM, and older or partial extension drivers also advertise no support, so the +gateway rejects explicit UI policy before validation or provisioning. Omitting +the section preserves each runtime's existing behavior. + +Example: + +```yaml showLineNumbers={false} +ui: + allow_graphical_ui: true + clipboard: read + allow_input_injection: false +``` + ## Network Policies **Category:** Dynamic diff --git a/docs/sandboxes/policies.mdx b/docs/sandboxes/policies.mdx index 1e57d8cd20..783531de4a 100644 --- a/docs/sandboxes/policies.mdx +++ b/docs/sandboxes/policies.mdx @@ -12,7 +12,7 @@ Use this page to apply and iterate policy changes on running sandboxes. For a fu ## Policy Structure -A policy has static sections `filesystem_policy`, `landlock`, and `process` that are locked at sandbox creation, and dynamic `network_policies` and `network_middlewares` sections that are hot-reloadable on a running sandbox. +A policy has static sections `filesystem_policy`, `landlock`, `process`, and `ui` that are locked at sandbox creation, and dynamic `network_policies` and `network_middlewares` sections that are hot-reloadable on a running sandbox. ```yaml wordWrap showLineNumbers={false} version: 1 @@ -32,6 +32,13 @@ landlock: # run_as_user: "1500" # run_as_group: "1500" +# Static, optional: portable UI capabilities. Within this explicit section, +# omitted values deny access. Only drivers advertising complete support accept it. +# ui: +# allow_graphical_ui: true +# clipboard: read +# allow_input_injection: false + # Dynamic: hot-reloadable. Named blocks of endpoints + binaries allowed to reach them. network_policies: my_api: @@ -69,6 +76,7 @@ When a hot reload changes rules, the supervisor publishes a new policy generatio | `filesystem_policy` | Static | Controls which directories the agent can access on disk. Paths are split into `read_only` and `read_write` lists. Any path not listed in either list is inaccessible. Set `include_workdir: true` to automatically add the agent's working directory to `read_write`. [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforces these restrictions at the kernel level. | | `landlock` | Static | Configures Landlock LSM enforcement behavior. Set `compatibility` to `best_effort` (skip individual inaccessible paths while applying remaining rules) or `hard_requirement` (fail if any path is inaccessible or the required kernel ABI is unavailable). Refer to the [Policy Schema Reference](/reference/policy-schema#landlock) for the full behavior table. | | `process` | Static | Optionally overrides the OS-level identity for the agent process. Explicit values must be `sandbox` or numeric UID/GID values from `1` through `4294967294`; root and the invalid identity sentinel are rejected. Docker and Podman may use named identities through per-field OCI `USER` fallback; Kubernetes uses its platform-selected numeric identity. The agent also runs with seccomp filters that block dangerous system calls. | +| `ui` | Static | Controls portable graphical UI, directional clipboard, and synthetic-input capabilities. Within an explicit section, every omitted value denies. MXC `process_container` advertises complete enforcement. MXC `isolation_session`, Docker, Podman, Kubernetes, VM, and partial extension drivers reject any explicit section before provisioning. Omission preserves existing runtime behavior. | | `network_policies` | Dynamic | Controls network access for ordinary outbound traffic from the sandbox. Each block has a name, a list of endpoints (host, port, protocol, and optional rules), and a list of binaries allowed to use those endpoints.
Every outbound connection except `https://inference.local` passes through the network supervisor, which queries the [policy engine](/about/how-it-works#core-components) with the destination and calling binary. A connection is allowed only when both match an entry in the same policy block.
For endpoints with `protocol: rest`, the proxy auto-detects TLS and terminates it so each HTTP request can be checked against that endpoint's `rules` (method and path). For endpoints with `protocol: websocket`, the proxy validates the RFC 6455 upgrade and evaluates `GET` rules for the handshake plus either `WEBSOCKET_TEXT` rules for raw client text messages or GraphQL operation rules for GraphQL-over-WebSocket messages. Set `websocket_credential_rewrite: true` only when a WebSocket or REST compatibility endpoint must keep placeholder credentials in sandbox-owned text frames and resolve them at the OpenShell relay boundary.
Endpoints with `protocol: tcp` allow ordinary DNS resolution and native TCP connections without inspecting payloads. Endpoints without `protocol` retain L4 passthrough through an explicit proxy.
If no endpoint matches, the connection is denied. Configure managed inference separately through [Inference Routing](/sandboxes/inference-routing). | | `network_middlewares` | Dynamic | Declares keyed HTTP and WebSocket middleware configs. After network and L7 policy admit a request or upgrade, OpenShell matches each config's host selectors independently and runs matching entries by their unique ascending `order` before credential injection. WebSocket-capable entries continue on complete client text messages. | diff --git a/proto/compute_driver.proto b/proto/compute_driver.proto index fe311201c1..af92188057 100644 --- a/proto/compute_driver.proto +++ b/proto/compute_driver.proto @@ -93,6 +93,10 @@ message GetCapabilitiesResponse { bool driver_reports_runtime_readiness = 8; // Static portable resource request forms supported by this configured driver. ResourceCapabilities resource_capabilities = 9; + // Whether this configured driver instance completely enforces the current + // portable SandboxPolicy.ui contract. Partial support must report false so + // the gateway rejects every explicit UI section before provisioning. + bool supports_ui_policy = 10; } message AuthenticateSandboxRequest { diff --git a/proto/openshell.proto b/proto/openshell.proto index e07055a47b..c6edb87c0a 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -839,6 +839,10 @@ message ComputeDriverCapabilities { // Static portable resource request forms reported by the driver. ResourceCapabilities resource_capabilities = 3; + + // Whether the configured driver instance completely enforces the portable + // SandboxPolicy.ui contract. + bool supports_ui_policy = 4; } // Static portable resource request forms reported by a compute driver. diff --git a/proto/sandbox.proto b/proto/sandbox.proto index c2b61d0b3a..a4f3de719e 100644 --- a/proto/sandbox.proto +++ b/proto/sandbox.proto @@ -31,6 +31,10 @@ message SandboxPolicy { // policy-local names. At most 10 configs are accepted, and at most 10 stages // can be selected per request. map network_middlewares = 6; + // Static, platform-neutral user-interface access policy. Within an explicit + // section, omitted capabilities deny. Omitting the section preserves the + // compute platform's existing behavior. + UiPolicy ui = 7; } // Filesystem access policy. @@ -57,6 +61,32 @@ message ProcessPolicy { string run_as_group = 2; } +// Directional clipboard access for a sandboxed workload. +enum UiClipboardAccess { + // Unspecified resolves to no clipboard access. + UI_CLIPBOARD_ACCESS_UNSPECIFIED = 0; + // No clipboard reads or writes. + UI_CLIPBOARD_ACCESS_NONE = 1; + // The sandbox may read host clipboard contents. + UI_CLIPBOARD_ACCESS_READ = 2; + // The sandbox may write host clipboard contents. + UI_CLIPBOARD_ACCESS_WRITE = 3; + // The sandbox may read and write host clipboard contents. + UI_CLIPBOARD_ACCESS_ALL = 4; +} + +// Platform-neutral user-interface capabilities. Every omitted field in an +// explicit policy defaults to deny. Compute platforms without complete support +// reject the entire explicit policy before provisioning. +message UiPolicy { + // Allow the sandbox to display graphical windows. + bool allow_graphical_ui = 1; + // Directional host clipboard access. + UiClipboardAccess clipboard = 2; + // Allow the sandbox to synthesize keyboard or pointer input. + bool allow_input_injection = 3; +} + // A named network access policy rule. message NetworkPolicyRule { // Human-readable name for this policy rule. diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index 16929f672c..25879e8cf8 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -145,6 +145,7 @@ func TestConverterCoversAllProtoFields_SandboxPolicy(t *testing.T) { "network_policies": true, "process": true, "landlock": true, + "ui": true, "network_middlewares": true, } diff --git a/sdk/go/openshell/v1/internal/converter/health.go b/sdk/go/openshell/v1/internal/converter/health.go index 63ab8c3298..f2f873ae8e 100644 --- a/sdk/go/openshell/v1/internal/converter/health.go +++ b/sdk/go/openshell/v1/internal/converter/health.go @@ -48,6 +48,7 @@ func ComputeDriverInfoFromProto(d *pb.ComputeDriverInfo) types.ComputeDriverInfo if caps := d.GetCapabilities(); caps != nil { result.DriverName = caps.GetDriverName() result.DriverVersion = caps.GetDriverVersion() + result.SupportsUIPolicy = caps.GetSupportsUiPolicy() } return result } diff --git a/sdk/go/openshell/v1/internal/converter/health_test.go b/sdk/go/openshell/v1/internal/converter/health_test.go index d0360a9b66..0a9cfb6a3f 100644 --- a/sdk/go/openshell/v1/internal/converter/health_test.go +++ b/sdk/go/openshell/v1/internal/converter/health_test.go @@ -20,8 +20,9 @@ func TestGatewayInfoFromProto(t *testing.T) { { Name: "k8s", Capabilities: &pb.ComputeDriverCapabilities{ - DriverName: "kubernetes", - DriverVersion: "2.1.0", + DriverName: "kubernetes", + DriverVersion: "2.1.0", + SupportsUiPolicy: true, }, }, { @@ -43,8 +44,10 @@ func TestGatewayInfoFromProto(t *testing.T) { assert.Equal(t, "k8s", info.ComputeDrivers[0].Name) assert.Equal(t, "kubernetes", info.ComputeDrivers[0].DriverName) assert.Equal(t, "2.1.0", info.ComputeDrivers[0].DriverVersion) + assert.True(t, info.ComputeDrivers[0].SupportsUIPolicy) assert.Equal(t, "docker", info.ComputeDrivers[1].Name) assert.Equal(t, "docker-engine", info.ComputeDrivers[1].DriverName) + assert.False(t, info.ComputeDrivers[1].SupportsUIPolicy) } func TestGatewayInfoFromProto_NoDrivers(t *testing.T) { @@ -107,6 +110,7 @@ func TestComputeDriverInfoFromProto_NilCapabilities(t *testing.T) { assert.Equal(t, "bare-metal", info.Name) assert.Empty(t, info.DriverName) assert.Empty(t, info.DriverVersion) + assert.False(t, info.SupportsUIPolicy) } func TestCurrentUserFromProto(t *testing.T) { diff --git a/sdk/go/openshell/v1/internal/converter/policy.go b/sdk/go/openshell/v1/internal/converter/policy.go index 8d68ca2f57..73cb9fc980 100644 --- a/sdk/go/openshell/v1/internal/converter/policy.go +++ b/sdk/go/openshell/v1/internal/converter/policy.go @@ -117,6 +117,7 @@ func SandboxPolicyFromProto(p *sbv1.SandboxPolicy) *types.SandboxPolicy { Filesystem: filesystemPolicyFromProto(p.GetFilesystem()), Landlock: landlockPolicyFromProto(p.GetLandlock()), Process: processPolicyFromProto(p.GetProcess()), + UI: uiPolicyFromProto(p.GetUi()), } if np := p.GetNetworkPolicies(); np != nil { result.NetworkPolicies = make(map[string]types.NetworkPolicyRule, len(np)) @@ -148,6 +149,7 @@ func SandboxPolicyToProto(p *types.SandboxPolicy) *sbv1.SandboxPolicy { Filesystem: filesystemPolicyToProto(p.Filesystem), Landlock: landlockPolicyToProto(p.Landlock), Process: processPolicyToProto(p.Process), + Ui: uiPolicyToProto(p.UI), } if p.NetworkPolicies != nil { result.NetworkPolicies = make(map[string]*sbv1.NetworkPolicyRule, len(p.NetworkPolicies)) @@ -184,6 +186,58 @@ func SandboxPolicyToProtoChecked(p *types.SandboxPolicy) (*sbv1.SandboxPolicy, e return result, nil } +func uiPolicyFromProto(p *sbv1.UiPolicy) *types.UIPolicy { + if p == nil { + return nil + } + return &types.UIPolicy{ + AllowGraphicalUI: p.GetAllowGraphicalUi(), + Clipboard: uiClipboardAccessFromProto(p.GetClipboard()), + AllowInputInjection: p.GetAllowInputInjection(), + } +} + +func uiPolicyToProto(p *types.UIPolicy) *sbv1.UiPolicy { + if p == nil { + return nil + } + return &sbv1.UiPolicy{ + AllowGraphicalUi: p.AllowGraphicalUI, + Clipboard: uiClipboardAccessToProto(p.Clipboard), + AllowInputInjection: p.AllowInputInjection, + } +} + +func uiClipboardAccessFromProto(v sbv1.UiClipboardAccess) types.UIClipboardAccess { + switch v { + case sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_NONE: + return types.UIClipboardAccessNone + case sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_READ: + return types.UIClipboardAccessRead + case sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_WRITE: + return types.UIClipboardAccessWrite + case sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_ALL: + return types.UIClipboardAccessAll + default: + return types.UIClipboardAccessUnspecified + } +} + +func uiClipboardAccessToProto(v types.UIClipboardAccess) sbv1.UiClipboardAccess { + switch v { + case types.UIClipboardAccessNone: + return sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_NONE + case types.UIClipboardAccessRead: + return sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_READ + case types.UIClipboardAccessWrite: + return sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_WRITE + case types.UIClipboardAccessAll: + return sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_ALL + default: + return sbv1.UiClipboardAccess_UI_CLIPBOARD_ACCESS_UNSPECIFIED + } +} + func middlewareConfigFromProto(m *sbv1.NetworkMiddlewareConfig) types.NetworkMiddlewareConfig { result := types.NetworkMiddlewareConfig{ Name: m.GetName(), diff --git a/sdk/go/openshell/v1/internal/converter/policy_test.go b/sdk/go/openshell/v1/internal/converter/policy_test.go index 6935b3def0..c3606548a9 100644 --- a/sdk/go/openshell/v1/internal/converter/policy_test.go +++ b/sdk/go/openshell/v1/internal/converter/policy_test.go @@ -200,6 +200,11 @@ func TestSandboxPolicyRoundTrip(t *testing.T) { RunAsUser: "sandbox-user", RunAsGroup: "sandbox-group", }, + UI: &v1.UIPolicy{ + AllowGraphicalUI: true, + Clipboard: v1.UIClipboardAccessRead, + AllowInputInjection: true, + }, NetworkPolicies: map[string]v1.NetworkPolicyRule{ "web-api": { Name: "web-api", @@ -239,6 +244,10 @@ func TestSandboxPolicyRoundTrip(t *testing.T) { assert.Equal(t, original.Process.RunAsUser, roundTrip.Process.RunAsUser) assert.Equal(t, original.Process.RunAsGroup, roundTrip.Process.RunAsGroup) + // UI + require.NotNil(t, roundTrip.UI) + assert.Equal(t, original.UI, roundTrip.UI) + // NetworkPolicies require.Len(t, roundTrip.NetworkPolicies, 2) webAPI, ok := roundTrip.NetworkPolicies["web-api"] @@ -315,6 +324,39 @@ func TestSandboxPolicyPartialSubPolicies(t *testing.T) { assert.Nil(t, roundTrip.NetworkPolicies) }) + t.Run("only UI", func(t *testing.T) { + for _, clipboard := range []v1.UIClipboardAccess{ + v1.UIClipboardAccessUnspecified, + v1.UIClipboardAccessNone, + v1.UIClipboardAccessRead, + v1.UIClipboardAccessWrite, + v1.UIClipboardAccessAll, + } { + original := &v1.SandboxPolicy{ + UI: &v1.UIPolicy{ + AllowGraphicalUI: true, + Clipboard: clipboard, + AllowInputInjection: true, + }, + } + roundTrip := SandboxPolicyFromProto(SandboxPolicyToProto(original)) + require.NotNil(t, roundTrip) + assert.Equal(t, original.UI, roundTrip.UI) + } + }) + + t.Run("explicit empty UI remains present", func(t *testing.T) { + original := &v1.SandboxPolicy{UI: &v1.UIPolicy{}} + protoPolicy := SandboxPolicyToProto(original) + require.NotNil(t, protoPolicy) + require.NotNil(t, protoPolicy.Ui) + + roundTrip := SandboxPolicyFromProto(protoPolicy) + require.NotNil(t, roundTrip) + require.NotNil(t, roundTrip.UI) + assert.Equal(t, &v1.UIPolicy{}, roundTrip.UI) + }) + t.Run("only landlock", func(t *testing.T) { original := &v1.SandboxPolicy{ Version: 2, diff --git a/sdk/go/openshell/v1/types/health.go b/sdk/go/openshell/v1/types/health.go index 1db3ec3872..14a4e2544a 100644 --- a/sdk/go/openshell/v1/types/health.go +++ b/sdk/go/openshell/v1/types/health.go @@ -29,9 +29,10 @@ type GatewayInfo struct { // ComputeDriverInfo describes a compute backend available on the gateway. type ComputeDriverInfo struct { - Name string - DriverName string - DriverVersion string + Name string + DriverName string + DriverVersion string + SupportsUIPolicy bool } // CurrentUser holds the authenticated caller's identity. diff --git a/sdk/go/openshell/v1/types/policy.go b/sdk/go/openshell/v1/types/policy.go index 9b6082b9ec..f18e42f1e0 100644 --- a/sdk/go/openshell/v1/types/policy.go +++ b/sdk/go/openshell/v1/types/policy.go @@ -108,7 +108,7 @@ type DraftPolicy struct { // SandboxPolicy is the top-level security policy configuration for a sandbox. // It contains filesystem access rules, Landlock LSM configuration, process -// identity rules, and named network access policies. +// identity rules, portable UI capabilities, and named network access policies. type SandboxPolicy struct { // Version is the policy version number. The server may override this on write. Version uint32 @@ -121,6 +121,9 @@ type SandboxPolicy struct { // Process controls the user and group identity for sandboxed processes. // Nil means no process policy is specified. Process *ProcessPolicy + // UI controls portable graphical UI, clipboard, and input-injection capabilities. + // Nil means no UI policy is specified. + UI *UIPolicy // NetworkPolicies contains named network access rules. // Nil means no network policies are specified; an empty map is distinct from nil. NetworkPolicies map[string]NetworkPolicyRule @@ -174,6 +177,33 @@ type ProcessPolicy struct { RunAsGroup string } +// UIClipboardAccess controls host clipboard direction from the sandbox's perspective. +type UIClipboardAccess int + +const ( + // UIClipboardAccessUnspecified resolves to no clipboard access. + UIClipboardAccessUnspecified UIClipboardAccess = iota + // UIClipboardAccessNone denies clipboard reads and writes. + UIClipboardAccessNone + // UIClipboardAccessRead permits reading host clipboard contents. + UIClipboardAccessRead + // UIClipboardAccessWrite permits writing host clipboard contents. + UIClipboardAccessWrite + // UIClipboardAccessAll permits reading and writing host clipboard contents. + UIClipboardAccessAll +) + +// UIPolicy declares platform-neutral user-interface capabilities. +// Every zero value denies access. +type UIPolicy struct { + // AllowGraphicalUI permits the workload to display graphical windows. + AllowGraphicalUI bool + // Clipboard controls host clipboard direction. + Clipboard UIClipboardAccess + // AllowInputInjection permits synthetic keyboard or pointer input. + AllowInputInjection bool +} + // SandboxPolicyRevision represents a versioned policy revision for a sandbox. type SandboxPolicyRevision struct { // Version is the policy version (monotonically increasing per sandbox). diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 21e9d31ed3..8bf8318a4b 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -1097,8 +1097,11 @@ type ComputeDriverCapabilities struct { DriverVersion string `protobuf:"bytes,2,opt,name=driver_version,json=driverVersion,proto3" json:"driver_version,omitempty"` // Static portable resource request forms reported by the driver. ResourceCapabilities *ResourceCapabilities `protobuf:"bytes,3,opt,name=resource_capabilities,json=resourceCapabilities,proto3" json:"resource_capabilities,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Whether the configured driver instance completely enforces the portable + // SandboxPolicy.ui contract. + SupportsUiPolicy bool `protobuf:"varint,4,opt,name=supports_ui_policy,json=supportsUiPolicy,proto3" json:"supports_ui_policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ComputeDriverCapabilities) Reset() { @@ -1152,6 +1155,13 @@ func (x *ComputeDriverCapabilities) GetResourceCapabilities() *ResourceCapabilit return nil } +func (x *ComputeDriverCapabilities) GetSupportsUiPolicy() bool { + if x != nil { + return x.SupportsUiPolicy + } + return false +} + // Static portable resource request forms reported by a compute driver. // An omitted domain means the driver does not report that domain. type ResourceCapabilities struct { @@ -9138,8 +9148,8 @@ type UpdateConfigRequest struct { // The new policy to apply. // // Sandbox scope (`global=false`): - // - only network_policies and inference fields may differ from create-time - // policy; static fields must match version 1. + // - only network_policies and inference fields may differ from create-time + // policy; static fields must match version 1. // // Global scope (`global=true`): // - applies to all sandboxes in full (no merge). @@ -15124,12 +15134,13 @@ const file_openshell_proto_rawDesc = "" + "\x0fcompute_drivers\x18\x03 \x03(\v2\x1f.openshell.v1.ComputeDriverInfoR\x0ecomputeDrivers\"t\n" + "\x11ComputeDriverInfo\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12K\n" + - "\fcapabilities\x18\x02 \x01(\v2'.openshell.v1.ComputeDriverCapabilitiesR\fcapabilities\"\xbc\x01\n" + + "\fcapabilities\x18\x02 \x01(\v2'.openshell.v1.ComputeDriverCapabilitiesR\fcapabilities\"\xea\x01\n" + "\x19ComputeDriverCapabilities\x12\x1f\n" + "\vdriver_name\x18\x01 \x01(\tR\n" + "driverName\x12%\n" + "\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\x12W\n" + - "\x15resource_capabilities\x18\x03 \x01(\v2\".openshell.v1.ResourceCapabilitiesR\x14resourceCapabilities\"\xca\x01\n" + + "\x15resource_capabilities\x18\x03 \x01(\v2\".openshell.v1.ResourceCapabilitiesR\x14resourceCapabilities\x12,\n" + + "\x12supports_ui_policy\x18\x04 \x01(\bR\x10supportsUiPolicy\"\xca\x01\n" + "\x14ResourceCapabilities\x127\n" + "\x03cpu\x18\x01 \x01(\v2%.openshell.v1.CpuResourceCapabilitiesR\x03cpu\x12@\n" + "\x06memory\x18\x02 \x01(\v2(.openshell.v1.MemoryResourceCapabilitiesR\x06memory\x127\n" + diff --git a/sdk/go/proto/sandboxv1/sandbox.pb.go b/sdk/go/proto/sandboxv1/sandbox.pb.go index 989589002b..e899d83378 100644 --- a/sdk/go/proto/sandboxv1/sandbox.pb.go +++ b/sdk/go/proto/sandboxv1/sandbox.pb.go @@ -25,6 +25,67 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// Directional clipboard access for a sandboxed workload. +type UiClipboardAccess int32 + +const ( + // Unspecified resolves to no clipboard access. + UiClipboardAccess_UI_CLIPBOARD_ACCESS_UNSPECIFIED UiClipboardAccess = 0 + // No clipboard reads or writes. + UiClipboardAccess_UI_CLIPBOARD_ACCESS_NONE UiClipboardAccess = 1 + // The sandbox may read host clipboard contents. + UiClipboardAccess_UI_CLIPBOARD_ACCESS_READ UiClipboardAccess = 2 + // The sandbox may write host clipboard contents. + UiClipboardAccess_UI_CLIPBOARD_ACCESS_WRITE UiClipboardAccess = 3 + // The sandbox may read and write host clipboard contents. + UiClipboardAccess_UI_CLIPBOARD_ACCESS_ALL UiClipboardAccess = 4 +) + +// Enum value maps for UiClipboardAccess. +var ( + UiClipboardAccess_name = map[int32]string{ + 0: "UI_CLIPBOARD_ACCESS_UNSPECIFIED", + 1: "UI_CLIPBOARD_ACCESS_NONE", + 2: "UI_CLIPBOARD_ACCESS_READ", + 3: "UI_CLIPBOARD_ACCESS_WRITE", + 4: "UI_CLIPBOARD_ACCESS_ALL", + } + UiClipboardAccess_value = map[string]int32{ + "UI_CLIPBOARD_ACCESS_UNSPECIFIED": 0, + "UI_CLIPBOARD_ACCESS_NONE": 1, + "UI_CLIPBOARD_ACCESS_READ": 2, + "UI_CLIPBOARD_ACCESS_WRITE": 3, + "UI_CLIPBOARD_ACCESS_ALL": 4, + } +) + +func (x UiClipboardAccess) Enum() *UiClipboardAccess { + p := new(UiClipboardAccess) + *p = x + return p +} + +func (x UiClipboardAccess) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (UiClipboardAccess) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[0].Descriptor() +} + +func (UiClipboardAccess) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[0] +} + +func (x UiClipboardAccess) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use UiClipboardAccess.Descriptor instead. +func (UiClipboardAccess) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{0} +} + // Scope that currently controls a setting. type SettingScope int32 @@ -59,11 +120,11 @@ func (x SettingScope) String() string { } func (SettingScope) Descriptor() protoreflect.EnumDescriptor { - return file_sandbox_proto_enumTypes[0].Descriptor() + return file_sandbox_proto_enumTypes[1].Descriptor() } func (SettingScope) Type() protoreflect.EnumType { - return &file_sandbox_proto_enumTypes[0] + return &file_sandbox_proto_enumTypes[1] } func (x SettingScope) Number() protoreflect.EnumNumber { @@ -72,7 +133,7 @@ func (x SettingScope) Number() protoreflect.EnumNumber { // Deprecated: Use SettingScope.Descriptor instead. func (SettingScope) EnumDescriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{0} + return file_sandbox_proto_rawDescGZIP(), []int{1} } // Source used for the policy payload in GetSandboxConfigResponse. @@ -109,11 +170,11 @@ func (x PolicySource) String() string { } func (PolicySource) Descriptor() protoreflect.EnumDescriptor { - return file_sandbox_proto_enumTypes[1].Descriptor() + return file_sandbox_proto_enumTypes[2].Descriptor() } func (PolicySource) Type() protoreflect.EnumType { - return &file_sandbox_proto_enumTypes[1] + return &file_sandbox_proto_enumTypes[2] } func (x PolicySource) Number() protoreflect.EnumNumber { @@ -122,7 +183,7 @@ func (x PolicySource) Number() protoreflect.EnumNumber { // Deprecated: Use PolicySource.Descriptor instead. func (PolicySource) EnumDescriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{1} + return file_sandbox_proto_rawDescGZIP(), []int{2} } // Sandbox security policy configuration. @@ -142,8 +203,12 @@ type SandboxPolicy struct { // policy-local names. At most 10 configs are accepted, and at most 10 stages // can be selected per request. NetworkMiddlewares map[string]*NetworkMiddlewareConfig `protobuf:"bytes,6,rep,name=network_middlewares,json=networkMiddlewares,proto3" json:"network_middlewares,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Static, platform-neutral user-interface access policy. Within an explicit + // section, omitted capabilities deny. Omitting the section preserves the + // compute platform's existing behavior. + Ui *UiPolicy `protobuf:"bytes,7,opt,name=ui,proto3" json:"ui,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SandboxPolicy) Reset() { @@ -218,6 +283,13 @@ func (x *SandboxPolicy) GetNetworkMiddlewares() map[string]*NetworkMiddlewareCon return nil } +func (x *SandboxPolicy) GetUi() *UiPolicy { + if x != nil { + return x.Ui + } + return nil +} + // Filesystem access policy. type FilesystemPolicy struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -383,6 +455,72 @@ func (x *ProcessPolicy) GetRunAsGroup() string { return "" } +// Platform-neutral user-interface capabilities. Every omitted field in an +// explicit policy defaults to deny. Compute platforms without complete support +// reject the entire explicit policy before provisioning. +type UiPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Allow the sandbox to display graphical windows. + AllowGraphicalUi bool `protobuf:"varint,1,opt,name=allow_graphical_ui,json=allowGraphicalUi,proto3" json:"allow_graphical_ui,omitempty"` + // Directional host clipboard access. + Clipboard UiClipboardAccess `protobuf:"varint,2,opt,name=clipboard,proto3,enum=openshell.sandbox.v1.UiClipboardAccess" json:"clipboard,omitempty"` + // Allow the sandbox to synthesize keyboard or pointer input. + AllowInputInjection bool `protobuf:"varint,3,opt,name=allow_input_injection,json=allowInputInjection,proto3" json:"allow_input_injection,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UiPolicy) Reset() { + *x = UiPolicy{} + mi := &file_sandbox_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UiPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UiPolicy) ProtoMessage() {} + +func (x *UiPolicy) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UiPolicy.ProtoReflect.Descriptor instead. +func (*UiPolicy) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{4} +} + +func (x *UiPolicy) GetAllowGraphicalUi() bool { + if x != nil { + return x.AllowGraphicalUi + } + return false +} + +func (x *UiPolicy) GetClipboard() UiClipboardAccess { + if x != nil { + return x.Clipboard + } + return UiClipboardAccess_UI_CLIPBOARD_ACCESS_UNSPECIFIED +} + +func (x *UiPolicy) GetAllowInputInjection() bool { + if x != nil { + return x.AllowInputInjection + } + return false +} + // A named network access policy rule. type NetworkPolicyRule struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -398,7 +536,7 @@ type NetworkPolicyRule struct { func (x *NetworkPolicyRule) Reset() { *x = NetworkPolicyRule{} - mi := &file_sandbox_proto_msgTypes[4] + mi := &file_sandbox_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -410,7 +548,7 @@ func (x *NetworkPolicyRule) String() string { func (*NetworkPolicyRule) ProtoMessage() {} func (x *NetworkPolicyRule) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[4] + mi := &file_sandbox_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -423,7 +561,7 @@ func (x *NetworkPolicyRule) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkPolicyRule.ProtoReflect.Descriptor instead. func (*NetworkPolicyRule) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{4} + return file_sandbox_proto_rawDescGZIP(), []int{5} } func (x *NetworkPolicyRule) GetName() string { @@ -468,7 +606,7 @@ type NetworkMiddlewareConfig struct { func (x *NetworkMiddlewareConfig) Reset() { *x = NetworkMiddlewareConfig{} - mi := &file_sandbox_proto_msgTypes[5] + mi := &file_sandbox_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -480,7 +618,7 @@ func (x *NetworkMiddlewareConfig) String() string { func (*NetworkMiddlewareConfig) ProtoMessage() {} func (x *NetworkMiddlewareConfig) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[5] + mi := &file_sandbox_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -493,7 +631,7 @@ func (x *NetworkMiddlewareConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkMiddlewareConfig.ProtoReflect.Descriptor instead. func (*NetworkMiddlewareConfig) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{5} + return file_sandbox_proto_rawDescGZIP(), []int{6} } func (x *NetworkMiddlewareConfig) GetName() string { @@ -553,7 +691,7 @@ type MiddlewareEndpointSelector struct { func (x *MiddlewareEndpointSelector) Reset() { *x = MiddlewareEndpointSelector{} - mi := &file_sandbox_proto_msgTypes[6] + mi := &file_sandbox_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -565,7 +703,7 @@ func (x *MiddlewareEndpointSelector) String() string { func (*MiddlewareEndpointSelector) ProtoMessage() {} func (x *MiddlewareEndpointSelector) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[6] + mi := &file_sandbox_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -578,7 +716,7 @@ func (x *MiddlewareEndpointSelector) ProtoReflect() protoreflect.Message { // Deprecated: Use MiddlewareEndpointSelector.ProtoReflect.Descriptor instead. func (*MiddlewareEndpointSelector) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{6} + return file_sandbox_proto_rawDescGZIP(), []int{7} } func (x *MiddlewareEndpointSelector) GetInclude() []string { @@ -607,7 +745,7 @@ type NetworkCredentialBinding struct { func (x *NetworkCredentialBinding) Reset() { *x = NetworkCredentialBinding{} - mi := &file_sandbox_proto_msgTypes[7] + mi := &file_sandbox_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -619,7 +757,7 @@ func (x *NetworkCredentialBinding) String() string { func (*NetworkCredentialBinding) ProtoMessage() {} func (x *NetworkCredentialBinding) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[7] + mi := &file_sandbox_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -632,7 +770,7 @@ func (x *NetworkCredentialBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkCredentialBinding.ProtoReflect.Descriptor instead. func (*NetworkCredentialBinding) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{7} + return file_sandbox_proto_rawDescGZIP(), []int{8} } func (x *NetworkCredentialBinding) GetProvider() string { @@ -668,7 +806,6 @@ type NetworkEndpoint struct { // When non-empty, the SSRF internal-IP check is replaced by an allowlist check: // - If host is also set: domain must resolve to an IP in this list. // - If host is empty: any domain is allowed as long as it resolves to an IP in this list. - // // Supports exact IPs ("10.0.5.20") and CIDR notation ("10.0.5.0/24"). // Loopback (127.0.0.0/8) and link-local (169.254.0.0/16) are always blocked // regardless of this field. @@ -744,7 +881,7 @@ type NetworkEndpoint struct { func (x *NetworkEndpoint) Reset() { *x = NetworkEndpoint{} - mi := &file_sandbox_proto_msgTypes[8] + mi := &file_sandbox_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -756,7 +893,7 @@ func (x *NetworkEndpoint) String() string { func (*NetworkEndpoint) ProtoMessage() {} func (x *NetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[8] + mi := &file_sandbox_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -769,7 +906,7 @@ func (x *NetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkEndpoint.ProtoReflect.Descriptor instead. func (*NetworkEndpoint) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{8} + return file_sandbox_proto_rawDescGZIP(), []int{9} } func (x *NetworkEndpoint) GetHost() string { @@ -995,7 +1132,7 @@ type McpOptions struct { func (x *McpOptions) Reset() { *x = McpOptions{} - mi := &file_sandbox_proto_msgTypes[9] + mi := &file_sandbox_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1007,7 +1144,7 @@ func (x *McpOptions) String() string { func (*McpOptions) ProtoMessage() {} func (x *McpOptions) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[9] + mi := &file_sandbox_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1020,7 +1157,7 @@ func (x *McpOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use McpOptions.ProtoReflect.Descriptor instead. func (*McpOptions) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{9} + return file_sandbox_proto_rawDescGZIP(), []int{10} } func (x *McpOptions) GetStrictToolNames() bool { @@ -1059,7 +1196,7 @@ type GraphqlOperation struct { func (x *GraphqlOperation) Reset() { *x = GraphqlOperation{} - mi := &file_sandbox_proto_msgTypes[10] + mi := &file_sandbox_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1071,7 +1208,7 @@ func (x *GraphqlOperation) String() string { func (*GraphqlOperation) ProtoMessage() {} func (x *GraphqlOperation) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[10] + mi := &file_sandbox_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1084,7 +1221,7 @@ func (x *GraphqlOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use GraphqlOperation.ProtoReflect.Descriptor instead. func (*GraphqlOperation) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{10} + return file_sandbox_proto_rawDescGZIP(), []int{11} } func (x *GraphqlOperation) GetOperationType() string { @@ -1139,7 +1276,7 @@ type L7DenyRule struct { func (x *L7DenyRule) Reset() { *x = L7DenyRule{} - mi := &file_sandbox_proto_msgTypes[11] + mi := &file_sandbox_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1151,7 +1288,7 @@ func (x *L7DenyRule) String() string { func (*L7DenyRule) ProtoMessage() {} func (x *L7DenyRule) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[11] + mi := &file_sandbox_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1164,7 +1301,7 @@ func (x *L7DenyRule) ProtoReflect() protoreflect.Message { // Deprecated: Use L7DenyRule.ProtoReflect.Descriptor instead. func (*L7DenyRule) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{11} + return file_sandbox_proto_rawDescGZIP(), []int{12} } func (x *L7DenyRule) GetMethod() string { @@ -1233,7 +1370,7 @@ type L7Rule struct { func (x *L7Rule) Reset() { *x = L7Rule{} - mi := &file_sandbox_proto_msgTypes[12] + mi := &file_sandbox_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1245,7 +1382,7 @@ func (x *L7Rule) String() string { func (*L7Rule) ProtoMessage() {} func (x *L7Rule) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[12] + mi := &file_sandbox_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1258,7 +1395,7 @@ func (x *L7Rule) ProtoReflect() protoreflect.Message { // Deprecated: Use L7Rule.ProtoReflect.Descriptor instead. func (*L7Rule) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{12} + return file_sandbox_proto_rawDescGZIP(), []int{13} } func (x *L7Rule) GetAllow() *L7Allow { @@ -1298,7 +1435,7 @@ type L7Allow struct { func (x *L7Allow) Reset() { *x = L7Allow{} - mi := &file_sandbox_proto_msgTypes[13] + mi := &file_sandbox_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1310,7 +1447,7 @@ func (x *L7Allow) String() string { func (*L7Allow) ProtoMessage() {} func (x *L7Allow) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[13] + mi := &file_sandbox_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1323,7 +1460,7 @@ func (x *L7Allow) ProtoReflect() protoreflect.Message { // Deprecated: Use L7Allow.ProtoReflect.Descriptor instead. func (*L7Allow) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{13} + return file_sandbox_proto_rawDescGZIP(), []int{14} } func (x *L7Allow) GetMethod() string { @@ -1395,7 +1532,7 @@ type L7QueryMatcher struct { func (x *L7QueryMatcher) Reset() { *x = L7QueryMatcher{} - mi := &file_sandbox_proto_msgTypes[14] + mi := &file_sandbox_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1407,7 +1544,7 @@ func (x *L7QueryMatcher) String() string { func (*L7QueryMatcher) ProtoMessage() {} func (x *L7QueryMatcher) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[14] + mi := &file_sandbox_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1420,7 +1557,7 @@ func (x *L7QueryMatcher) ProtoReflect() protoreflect.Message { // Deprecated: Use L7QueryMatcher.ProtoReflect.Descriptor instead. func (*L7QueryMatcher) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{14} + return file_sandbox_proto_rawDescGZIP(), []int{15} } func (x *L7QueryMatcher) GetGlob() string { @@ -1451,7 +1588,7 @@ type NetworkBinary struct { func (x *NetworkBinary) Reset() { *x = NetworkBinary{} - mi := &file_sandbox_proto_msgTypes[15] + mi := &file_sandbox_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1463,7 +1600,7 @@ func (x *NetworkBinary) String() string { func (*NetworkBinary) ProtoMessage() {} func (x *NetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[15] + mi := &file_sandbox_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1476,7 +1613,7 @@ func (x *NetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkBinary.ProtoReflect.Descriptor instead. func (*NetworkBinary) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{15} + return file_sandbox_proto_rawDescGZIP(), []int{16} } func (x *NetworkBinary) GetPath() string { @@ -1505,7 +1642,7 @@ type GetSandboxConfigRequest struct { func (x *GetSandboxConfigRequest) Reset() { *x = GetSandboxConfigRequest{} - mi := &file_sandbox_proto_msgTypes[16] + mi := &file_sandbox_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1517,7 +1654,7 @@ func (x *GetSandboxConfigRequest) String() string { func (*GetSandboxConfigRequest) ProtoMessage() {} func (x *GetSandboxConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[16] + mi := &file_sandbox_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1530,7 +1667,7 @@ func (x *GetSandboxConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxConfigRequest.ProtoReflect.Descriptor instead. func (*GetSandboxConfigRequest) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{16} + return file_sandbox_proto_rawDescGZIP(), []int{17} } func (x *GetSandboxConfigRequest) GetSandboxId() string { @@ -1549,7 +1686,7 @@ type GetGatewayConfigRequest struct { func (x *GetGatewayConfigRequest) Reset() { *x = GetGatewayConfigRequest{} - mi := &file_sandbox_proto_msgTypes[17] + mi := &file_sandbox_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1561,7 +1698,7 @@ func (x *GetGatewayConfigRequest) String() string { func (*GetGatewayConfigRequest) ProtoMessage() {} func (x *GetGatewayConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[17] + mi := &file_sandbox_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1574,7 +1711,7 @@ func (x *GetGatewayConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGatewayConfigRequest.ProtoReflect.Descriptor instead. func (*GetGatewayConfigRequest) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{17} + return file_sandbox_proto_rawDescGZIP(), []int{18} } // Response containing gateway-global settings. @@ -1591,7 +1728,7 @@ type GetGatewayConfigResponse struct { func (x *GetGatewayConfigResponse) Reset() { *x = GetGatewayConfigResponse{} - mi := &file_sandbox_proto_msgTypes[18] + mi := &file_sandbox_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1603,7 +1740,7 @@ func (x *GetGatewayConfigResponse) String() string { func (*GetGatewayConfigResponse) ProtoMessage() {} func (x *GetGatewayConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[18] + mi := &file_sandbox_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1616,7 +1753,7 @@ func (x *GetGatewayConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGatewayConfigResponse.ProtoReflect.Descriptor instead. func (*GetGatewayConfigResponse) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{18} + return file_sandbox_proto_rawDescGZIP(), []int{19} } func (x *GetGatewayConfigResponse) GetSettings() map[string]*SettingValue { @@ -1649,7 +1786,7 @@ type SettingValue struct { func (x *SettingValue) Reset() { *x = SettingValue{} - mi := &file_sandbox_proto_msgTypes[19] + mi := &file_sandbox_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1661,7 +1798,7 @@ func (x *SettingValue) String() string { func (*SettingValue) ProtoMessage() {} func (x *SettingValue) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[19] + mi := &file_sandbox_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1674,7 +1811,7 @@ func (x *SettingValue) ProtoReflect() protoreflect.Message { // Deprecated: Use SettingValue.ProtoReflect.Descriptor instead. func (*SettingValue) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{19} + return file_sandbox_proto_rawDescGZIP(), []int{20} } func (x *SettingValue) GetValue() isSettingValue_Value { @@ -1759,7 +1896,7 @@ type EffectiveSetting struct { func (x *EffectiveSetting) Reset() { *x = EffectiveSetting{} - mi := &file_sandbox_proto_msgTypes[20] + mi := &file_sandbox_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1771,7 +1908,7 @@ func (x *EffectiveSetting) String() string { func (*EffectiveSetting) ProtoMessage() {} func (x *EffectiveSetting) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[20] + mi := &file_sandbox_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1784,7 +1921,7 @@ func (x *EffectiveSetting) ProtoReflect() protoreflect.Message { // Deprecated: Use EffectiveSetting.ProtoReflect.Descriptor instead. func (*EffectiveSetting) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{20} + return file_sandbox_proto_rawDescGZIP(), []int{21} } func (x *EffectiveSetting) GetValue() *SettingValue { @@ -1843,7 +1980,7 @@ type GetSandboxConfigResponse struct { func (x *GetSandboxConfigResponse) Reset() { *x = GetSandboxConfigResponse{} - mi := &file_sandbox_proto_msgTypes[21] + mi := &file_sandbox_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1855,7 +1992,7 @@ func (x *GetSandboxConfigResponse) String() string { func (*GetSandboxConfigResponse) ProtoMessage() {} func (x *GetSandboxConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[21] + mi := &file_sandbox_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1868,7 +2005,7 @@ func (x *GetSandboxConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxConfigResponse.ProtoReflect.Descriptor instead. func (*GetSandboxConfigResponse) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{21} + return file_sandbox_proto_rawDescGZIP(), []int{22} } func (x *GetSandboxConfigResponse) GetPolicy() *SandboxPolicy { @@ -1988,7 +2125,7 @@ type SupervisorMiddlewareService struct { func (x *SupervisorMiddlewareService) Reset() { *x = SupervisorMiddlewareService{} - mi := &file_sandbox_proto_msgTypes[22] + mi := &file_sandbox_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2000,7 +2137,7 @@ func (x *SupervisorMiddlewareService) String() string { func (*SupervisorMiddlewareService) ProtoMessage() {} func (x *SupervisorMiddlewareService) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[22] + mi := &file_sandbox_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2013,7 +2150,7 @@ func (x *SupervisorMiddlewareService) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMiddlewareService.ProtoReflect.Descriptor instead. func (*SupervisorMiddlewareService) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{22} + return file_sandbox_proto_rawDescGZIP(), []int{23} } func (x *SupervisorMiddlewareService) GetName() string { @@ -2069,7 +2206,7 @@ var File_sandbox_proto protoreflect.FileDescriptor const file_sandbox_proto_rawDesc = "" + "\n" + - "\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xa8\x05\n" + + "\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xd8\x05\n" + "\rSandboxPolicy\x12\x18\n" + "\aversion\x18\x01 \x01(\rR\aversion\x12F\n" + "\n" + @@ -2078,7 +2215,8 @@ const file_sandbox_proto_rawDesc = "" + "\blandlock\x18\x03 \x01(\v2$.openshell.sandbox.v1.LandlockPolicyR\blandlock\x12=\n" + "\aprocess\x18\x04 \x01(\v2#.openshell.sandbox.v1.ProcessPolicyR\aprocess\x12c\n" + "\x10network_policies\x18\x05 \x03(\v28.openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntryR\x0fnetworkPolicies\x12l\n" + - "\x13network_middlewares\x18\x06 \x03(\v2;.openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntryR\x12networkMiddlewares\x1ak\n" + + "\x13network_middlewares\x18\x06 \x03(\v2;.openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntryR\x12networkMiddlewares\x12.\n" + + "\x02ui\x18\a \x01(\v2\x1e.openshell.sandbox.v1.UiPolicyR\x02ui\x1ak\n" + "\x14NetworkPoliciesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12=\n" + "\x05value\x18\x02 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\x05value:\x028\x01\x1at\n" + @@ -2095,7 +2233,11 @@ const file_sandbox_proto_rawDesc = "" + "\rProcessPolicy\x12\x1e\n" + "\vrun_as_user\x18\x01 \x01(\tR\trunAsUser\x12 \n" + "\frun_as_group\x18\x02 \x01(\tR\n" + - "runAsGroup\"\xad\x01\n" + + "runAsGroup\"\xb3\x01\n" + + "\bUiPolicy\x12,\n" + + "\x12allow_graphical_ui\x18\x01 \x01(\bR\x10allowGraphicalUi\x12E\n" + + "\tclipboard\x18\x02 \x01(\x0e2'.openshell.sandbox.v1.UiClipboardAccessR\tclipboard\x122\n" + + "\x15allow_input_injection\x18\x03 \x01(\bR\x13allowInputInjection\"\xad\x01\n" + "\x11NetworkPolicyRule\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12C\n" + "\tendpoints\x18\x02 \x03(\v2%.openshell.sandbox.v1.NetworkEndpointR\tendpoints\x12?\n" + @@ -2246,7 +2388,13 @@ const file_sandbox_proto_rawDesc = "" + "\atimeout\x18\x04 \x01(\tR\atimeout\x12%\n" + "\x0ftls_ca_cert_pem\x18\x05 \x01(\fR\ftlsCaCertPem\x12\x1a\n" + "\baudience\x18\x06 \x01(\tR\baudience\x128\n" + - "\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransport*b\n" + + "\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransport*\xb0\x01\n" + + "\x11UiClipboardAccess\x12#\n" + + "\x1fUI_CLIPBOARD_ACCESS_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18UI_CLIPBOARD_ACCESS_NONE\x10\x01\x12\x1c\n" + + "\x18UI_CLIPBOARD_ACCESS_READ\x10\x02\x12\x1d\n" + + "\x19UI_CLIPBOARD_ACCESS_WRITE\x10\x03\x12\x1b\n" + + "\x17UI_CLIPBOARD_ACCESS_ALL\x10\x04*b\n" + "\fSettingScope\x12\x1d\n" + "\x19SETTING_SCOPE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15SETTING_SCOPE_SANDBOX\x10\x01\x12\x18\n" + @@ -2268,86 +2416,90 @@ func file_sandbox_proto_rawDescGZIP() []byte { return file_sandbox_proto_rawDescData } -var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 32) +var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_sandbox_proto_goTypes = []any{ - (SettingScope)(0), // 0: openshell.sandbox.v1.SettingScope - (PolicySource)(0), // 1: openshell.sandbox.v1.PolicySource - (*SandboxPolicy)(nil), // 2: openshell.sandbox.v1.SandboxPolicy - (*FilesystemPolicy)(nil), // 3: openshell.sandbox.v1.FilesystemPolicy - (*LandlockPolicy)(nil), // 4: openshell.sandbox.v1.LandlockPolicy - (*ProcessPolicy)(nil), // 5: openshell.sandbox.v1.ProcessPolicy - (*NetworkPolicyRule)(nil), // 6: openshell.sandbox.v1.NetworkPolicyRule - (*NetworkMiddlewareConfig)(nil), // 7: openshell.sandbox.v1.NetworkMiddlewareConfig - (*MiddlewareEndpointSelector)(nil), // 8: openshell.sandbox.v1.MiddlewareEndpointSelector - (*NetworkCredentialBinding)(nil), // 9: openshell.sandbox.v1.NetworkCredentialBinding - (*NetworkEndpoint)(nil), // 10: openshell.sandbox.v1.NetworkEndpoint - (*McpOptions)(nil), // 11: openshell.sandbox.v1.McpOptions - (*GraphqlOperation)(nil), // 12: openshell.sandbox.v1.GraphqlOperation - (*L7DenyRule)(nil), // 13: openshell.sandbox.v1.L7DenyRule - (*L7Rule)(nil), // 14: openshell.sandbox.v1.L7Rule - (*L7Allow)(nil), // 15: openshell.sandbox.v1.L7Allow - (*L7QueryMatcher)(nil), // 16: openshell.sandbox.v1.L7QueryMatcher - (*NetworkBinary)(nil), // 17: openshell.sandbox.v1.NetworkBinary - (*GetSandboxConfigRequest)(nil), // 18: openshell.sandbox.v1.GetSandboxConfigRequest - (*GetGatewayConfigRequest)(nil), // 19: openshell.sandbox.v1.GetGatewayConfigRequest - (*GetGatewayConfigResponse)(nil), // 20: openshell.sandbox.v1.GetGatewayConfigResponse - (*SettingValue)(nil), // 21: openshell.sandbox.v1.SettingValue - (*EffectiveSetting)(nil), // 22: openshell.sandbox.v1.EffectiveSetting - (*GetSandboxConfigResponse)(nil), // 23: openshell.sandbox.v1.GetSandboxConfigResponse - (*SupervisorMiddlewareService)(nil), // 24: openshell.sandbox.v1.SupervisorMiddlewareService - nil, // 25: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry - nil, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry - nil, // 27: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry - nil, // 28: openshell.sandbox.v1.L7DenyRule.QueryEntry - nil, // 29: openshell.sandbox.v1.L7DenyRule.ParamsEntry - nil, // 30: openshell.sandbox.v1.L7Allow.QueryEntry - nil, // 31: openshell.sandbox.v1.L7Allow.ParamsEntry - nil, // 32: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - nil, // 33: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry - (*structpb.Struct)(nil), // 34: google.protobuf.Struct + (UiClipboardAccess)(0), // 0: openshell.sandbox.v1.UiClipboardAccess + (SettingScope)(0), // 1: openshell.sandbox.v1.SettingScope + (PolicySource)(0), // 2: openshell.sandbox.v1.PolicySource + (*SandboxPolicy)(nil), // 3: openshell.sandbox.v1.SandboxPolicy + (*FilesystemPolicy)(nil), // 4: openshell.sandbox.v1.FilesystemPolicy + (*LandlockPolicy)(nil), // 5: openshell.sandbox.v1.LandlockPolicy + (*ProcessPolicy)(nil), // 6: openshell.sandbox.v1.ProcessPolicy + (*UiPolicy)(nil), // 7: openshell.sandbox.v1.UiPolicy + (*NetworkPolicyRule)(nil), // 8: openshell.sandbox.v1.NetworkPolicyRule + (*NetworkMiddlewareConfig)(nil), // 9: openshell.sandbox.v1.NetworkMiddlewareConfig + (*MiddlewareEndpointSelector)(nil), // 10: openshell.sandbox.v1.MiddlewareEndpointSelector + (*NetworkCredentialBinding)(nil), // 11: openshell.sandbox.v1.NetworkCredentialBinding + (*NetworkEndpoint)(nil), // 12: openshell.sandbox.v1.NetworkEndpoint + (*McpOptions)(nil), // 13: openshell.sandbox.v1.McpOptions + (*GraphqlOperation)(nil), // 14: openshell.sandbox.v1.GraphqlOperation + (*L7DenyRule)(nil), // 15: openshell.sandbox.v1.L7DenyRule + (*L7Rule)(nil), // 16: openshell.sandbox.v1.L7Rule + (*L7Allow)(nil), // 17: openshell.sandbox.v1.L7Allow + (*L7QueryMatcher)(nil), // 18: openshell.sandbox.v1.L7QueryMatcher + (*NetworkBinary)(nil), // 19: openshell.sandbox.v1.NetworkBinary + (*GetSandboxConfigRequest)(nil), // 20: openshell.sandbox.v1.GetSandboxConfigRequest + (*GetGatewayConfigRequest)(nil), // 21: openshell.sandbox.v1.GetGatewayConfigRequest + (*GetGatewayConfigResponse)(nil), // 22: openshell.sandbox.v1.GetGatewayConfigResponse + (*SettingValue)(nil), // 23: openshell.sandbox.v1.SettingValue + (*EffectiveSetting)(nil), // 24: openshell.sandbox.v1.EffectiveSetting + (*GetSandboxConfigResponse)(nil), // 25: openshell.sandbox.v1.GetSandboxConfigResponse + (*SupervisorMiddlewareService)(nil), // 26: openshell.sandbox.v1.SupervisorMiddlewareService + nil, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + nil, // 28: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + nil, // 29: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + nil, // 30: openshell.sandbox.v1.L7DenyRule.QueryEntry + nil, // 31: openshell.sandbox.v1.L7DenyRule.ParamsEntry + nil, // 32: openshell.sandbox.v1.L7Allow.QueryEntry + nil, // 33: openshell.sandbox.v1.L7Allow.ParamsEntry + nil, // 34: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + nil, // 35: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + (*structpb.Struct)(nil), // 36: google.protobuf.Struct } var file_sandbox_proto_depIdxs = []int32{ - 3, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy - 4, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy - 5, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy - 25, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry - 26, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry - 10, // 5: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 17, // 6: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 34, // 7: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct - 8, // 8: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector - 14, // 9: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule - 13, // 10: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 27, // 11: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry - 11, // 12: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions - 9, // 13: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding - 28, // 14: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry - 29, // 15: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry - 15, // 16: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow - 30, // 17: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry - 31, // 18: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry - 32, // 19: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - 21, // 20: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue - 0, // 21: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope - 2, // 22: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 33, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry - 1, // 24: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 24, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService - 6, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 7, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig - 12, // 28: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation - 16, // 29: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 30: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 31: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 32: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 21, // 33: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue - 22, // 34: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting - 35, // [35:35] is the sub-list for method output_type - 35, // [35:35] is the sub-list for method input_type - 35, // [35:35] is the sub-list for extension type_name - 35, // [35:35] is the sub-list for extension extendee - 0, // [0:35] is the sub-list for field type_name + 4, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy + 5, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy + 6, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy + 27, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + 28, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + 7, // 5: openshell.sandbox.v1.SandboxPolicy.ui:type_name -> openshell.sandbox.v1.UiPolicy + 0, // 6: openshell.sandbox.v1.UiPolicy.clipboard:type_name -> openshell.sandbox.v1.UiClipboardAccess + 12, // 7: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 19, // 8: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 36, // 9: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct + 10, // 10: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector + 16, // 11: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule + 15, // 12: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 29, // 13: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + 13, // 14: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions + 11, // 15: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding + 30, // 16: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry + 31, // 17: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry + 17, // 18: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow + 32, // 19: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry + 33, // 20: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry + 34, // 21: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + 23, // 22: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue + 1, // 23: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope + 3, // 24: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 35, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + 2, // 26: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 26, // 27: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService + 8, // 28: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 9, // 29: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig + 14, // 30: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation + 18, // 31: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 18, // 32: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 18, // 33: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 18, // 34: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 23, // 35: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue + 24, // 36: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting + 37, // [37:37] is the sub-list for method output_type + 37, // [37:37] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name } func init() { file_sandbox_proto_init() } @@ -2355,8 +2507,8 @@ func file_sandbox_proto_init() { if File_sandbox_proto != nil { return } - file_sandbox_proto_msgTypes[9].OneofWrappers = []any{} - file_sandbox_proto_msgTypes[19].OneofWrappers = []any{ + file_sandbox_proto_msgTypes[10].OneofWrappers = []any{} + file_sandbox_proto_msgTypes[20].OneofWrappers = []any{ (*SettingValue_StringValue)(nil), (*SettingValue_BoolValue)(nil), (*SettingValue_IntValue)(nil), @@ -2367,8 +2519,8 @@ func file_sandbox_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sandbox_proto_rawDesc), len(file_sandbox_proto_rawDesc)), - NumEnums: 2, - NumMessages: 32, + NumEnums: 3, + NumMessages: 33, NumExtensions: 0, NumServices: 0, }, diff --git a/skills/generate-sandbox-policy/SKILL.md b/skills/generate-sandbox-policy/SKILL.md index 73c0863df7..2bc77b446e 100644 --- a/skills/generate-sandbox-policy/SKILL.md +++ b/skills/generate-sandbox-policy/SKILL.md @@ -11,7 +11,7 @@ Generate YAML sandbox network policies and network middleware configuration from This skill translates a user's plain-language policy intent into a valid sandbox policy. The amount of detail the user provides determines the granularity of the generated policy — from broad L4 or preset-based policies (just a host:port) up to fine-grained per-endpoint L7 rules (full API docs). -The output is a `network_policies` YAML block, an optional `network_middlewares` block, and optionally a full policy file that conforms to the sandbox policy schema. +The output is a `network_policies` YAML block, an optional `network_middlewares` block, an optional static `ui` block when explicitly requested, and optionally a full policy file that conforms to the sandbox policy schema. ## Step 1: Gather Inputs @@ -168,6 +168,7 @@ Key sections to reference: - **Private IP Access via `allowed_ips`** — CIDR allowlist for private IP space - **Network Middleware** - top-level middleware configs, ordering, host selection, and failure behavior - **Validation Rules** — what combinations are valid/invalid +- **UI** — static, portable capabilities and runtime support boundaries When middleware is requested, also read the published [supervisor middleware guide](https://docs.nvidia.com/openshell/latest/extensibility/supervisor-middleware.md). @@ -257,6 +258,28 @@ Use the most specific pattern that covers the intent. Prefer narrow globs over ` ## Step 5: Generate the Policy +### UI Policy + +Emit `ui` only when the user explicitly requests a graphical surface, +clipboard access, or synthetic input. Choose the narrowest capability and keep +unrequested fields omitted so they remain deny by default: + +```yaml +ui: + allow_graphical_ui: true + clipboard: read # none | read | write | all + allow_input_injection: false +``` + +Treat clipboard direction from the sandbox's perspective. Warn that UI is a +static sandbox-creation control. It is currently enforceable only by the MXC +driver's OpenShell `process_container` backend, which emits MXC's +`processcontainer` containment value and advertises complete support. MXC +`isolation_session` and non-Windows drivers advertise no support, so the gateway +rejects any explicit UI section, including `{}`, before provisioning. Omit the +section rather than emitting deny-only UI for those drivers; omission preserves +their existing behavior. + ### Output Format Generate a complete `network_policies` entry. Use this template: @@ -381,6 +404,7 @@ Before presenting the policy to the user, verify correctness **and** flag breadt - [ ] No fail-closed middleware selector can cover a `tls: skip` endpoint - [ ] Any required WebSocket control advertises `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`, and the user understands that V1 does not inspect binary messages - [ ] Endpoints contributed by a credentialed provider are not L4-only or `tls: skip` unless `allow_uninspected_credentials: true` explicitly records the exception +- [ ] An explicit `ui` section targets a configured driver/backend that advertises complete UI-policy support ### Schema Warnings (log-only, but should be fixed) @@ -443,7 +467,7 @@ The policy needs to go somewhere. Determine which mode applies: 1. **Read the existing file** to understand current state: - What policies already exist under `network_policies` - - What the `filesystem_policy`, `landlock`, and `process` sections look like + - What the `filesystem_policy`, `landlock`, `process`, and `ui` sections look like - Whether the file uses compact (`{ host: ..., port: ... }`) or expanded YAML style 2. **Check for conflicts**: @@ -462,7 +486,7 @@ The policy needs to go somewhere. Determine which mode applies: - **Modifying an existing policy**: Edit the specific policy in place — add/remove endpoints, change access presets, update rules, add binaries, etc. A rule authorizes every binary it lists to reach every endpoint and port it lists, so adding one binary grants it all of that rule's endpoints, and adding one endpoint grants it to all of that rule's binaries. State the resulting pairs to the user before writing them. When the user wants a binary to reach only part of a rule's endpoints, put that binary and those endpoints in a separate rule instead of extending the existing one. An empty `binaries` list means any binary, so leaving it off widens the rule to every process. - **Removing a policy**: Delete the policy block if the user asks. -4. **Preserve everything else**: Do not modify `filesystem_policy`, `landlock`, `process`, or other policies unless the user explicitly asks. +4. **Preserve everything else**: Do not modify `filesystem_policy`, `landlock`, `process`, `ui`, or other policies unless the user explicitly asks. ### Mode B: Create a New Policy File diff --git a/skills/generate-sandbox-policy/examples.md b/skills/generate-sandbox-policy/examples.md index b4c4b6f822..c4d1462c7e 100644 --- a/skills/generate-sandbox-policy/examples.md +++ b/skills/generate-sandbox-policy/examples.md @@ -748,7 +748,7 @@ An exact IP is treated as `/32` — only that specific address is permitted. - { path: /usr/bin/curl } ``` -The agent inserts the new entry after the last existing policy in the `network_policies` block. All other sections (`filesystem_policy`, `landlock`, `process`) are untouched. +The agent inserts the new entry after the last existing policy in the `network_policies` block. All other sections (`filesystem_policy`, `landlock`, `process`, `ui`) are untouched. --- diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index e080fd4001..fc8bddb7f1 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -430,7 +430,7 @@ the operation that removes retained state. This is the most important multi-step workflow. It enables a tight feedback cycle where sandbox policy is refined based on observed activity. -**Key concept**: Policies have static fields (immutable after creation: `filesystem_policy`, `landlock`, `process`) and two dynamic fields: `network_policies` and `network_middlewares`. Both dynamic fields can be updated without recreating the sandbox when the selected compute driver supports live policy updates. Drivers without the standard supervisor fetch revisions through the sandbox configuration API and report whether they loaded them. +**Key concept**: Policies have static fields (immutable after creation: `filesystem_policy`, `landlock`, `process`, `ui`) and two dynamic fields: `network_policies` and `network_middlewares`. Both dynamic fields can be updated without recreating the sandbox when the selected compute driver supports live policy updates. MXC rejects live policy replacement and merge updates; delete and recreate an MXC sandbox instead. UI capabilities are enforced only by a configured driver/backend advertising complete UI-policy support. Today that is the MXC driver's OpenShell `process_container` backend, which emits MXC's `processcontainer` containment value. `isolation_session` and non-Windows drivers reject any explicit UI section before provisioning; omit it to preserve their existing behavior. An endpoint with omitted `protocol` retains explicit-proxy behavior. Explicit `protocol: tcp` requests policy DNS and transparent TCP and currently requires @@ -500,7 +500,7 @@ Edit `current-policy.yaml` to allow the blocked actions. **For policy content au - Binary matching patterns - Ordered `network_middlewares`, host selection, HTTP and WebSocket bindings, and `fail_open` or `fail_closed` behavior -`network_policies` and `network_middlewares` can be modified at runtime when the selected compute driver supports live policy updates. Use `--wait` to verify that the active runtime loaded the revision; do not infer enforcement from the gateway accepting the update. If `filesystem_policy`, `landlock`, or `process` need changes, the sandbox must be recreated. Built-in middleware such as `openshell/regex` needs no gateway registration. An operator-run middleware must already be registered under `[[openshell.supervisor.middleware]]`; changing that static registration requires a gateway restart. +`network_policies` and `network_middlewares` can be modified at runtime when the selected compute driver supports live policy updates. Use `--wait` to verify that the active runtime loaded the revision; do not infer enforcement from the gateway accepting the update. MXC rejects live policy replacement and merge updates; delete and recreate an MXC sandbox instead. If `filesystem_policy`, `landlock`, `process`, or `ui` need changes, the sandbox must be recreated. Built-in middleware such as `openshell/regex` needs no gateway registration. An operator-run middleware must already be registered under `[[openshell.supervisor.middleware]]`; changing that static registration requires a gateway restart. Middleware can inspect parsed HTTP request bodies and complete client-to-upstream WebSocket text messages over both `ws://` and `wss://` when the implementation advertises the matching binding. The built-in `openshell/regex` advertises both bindings and applies its fixed patterns to UTF-8 text. A host-matched HTTP-only attachment can inspect the upgrade GET but does not join the WebSocket chain; look for `binding_not_selected` coverage. Binary messages pass under both `on_error` modes and active stages emit `unsupported_message_type` coverage; upstream-to-client messages remain uninspected. A broken fail-open WebSocket stage is disabled for the rest of that connection; inspect sandbox OCSF logs for `openshell.middleware.websocket_stage_disabled`. diff --git a/tasks/scripts/windows-msvc.ps1 b/tasks/scripts/windows-msvc.ps1 index 0a0410f4ca..67a9466ca1 100644 --- a/tasks/scripts/windows-msvc.ps1 +++ b/tasks/scripts/windows-msvc.ps1 @@ -6,7 +6,7 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] - [ValidateSet("check", "lint", "build", "test", "test-precommit", "test-unsupported", "artifacts", "ci")] + [ValidateSet("check", "lint", "build", "test", "test-precommit", "test-unsupported", "test-mxc-real", "artifacts", "ci")] [string] $Action, [Parameter(Position = 1)] @@ -68,12 +68,14 @@ $WindowsClippyPackageExcludes = $UnsupportedDriverPackageExcludes $WindowsClippyLintArgs = "-D warnings -A dead-code -A unused-imports -A clippy::unused-async" $BundledZ3WorkspaceFeatures = "--features openshell-prover/bundled-z3" $BundledZ3ServerFeatures = "--features openshell-server/bundled-z3,openshell-prover/bundled-z3" +$BundledZ3GatewayFeatures = "--features bundled-z3" $BundledZ3Repository = "https://github.com/Z3Prover/z3.git" $BundledZ3SysVersion = "0.11.0" # This is the matching Z3 4.16.0 source revision. Update both pins together. $BundledZ3Revision = "ddb49568d3520e99799e364fb22f35fc67d887b1" $Z3WorkspaceFeatures = $BundledZ3WorkspaceFeatures $Z3ServerFeatures = $BundledZ3ServerFeatures +$Z3GatewayFeatures = $BundledZ3GatewayFeatures function Get-VsInstallRoots { $programFiles = @( @@ -470,6 +472,7 @@ function Configure-Z3 { return [pscustomobject]@{ WorkspaceFeatures = $BundledZ3WorkspaceFeatures ServerFeatures = $BundledZ3ServerFeatures + GatewayFeatures = $BundledZ3GatewayFeatures } } @@ -497,6 +500,7 @@ function Configure-Z3 { return [pscustomobject]@{ WorkspaceFeatures = "" ServerFeatures = "" + GatewayFeatures = "" } } @@ -643,11 +647,19 @@ function Invoke-UnsupportedContractTests([string] $RustTarget) { foreach ($test in $tests) { Invoke-VsCargo ` -RustTarget $RustTarget ` - -CargoArgs "cargo test -p openshell-gateway --target $RustTarget $test $Z3ServerFeatures" ` + -CargoArgs "cargo test -p openshell-gateway --target $RustTarget $test $Z3GatewayFeatures" ` -LogName "test-$RustTarget-unsupported-$test.log" } } +function Invoke-MxcRealTests([string] $RustTarget) { + Assert-NativeTestTarget $RustTarget + Invoke-VsCargo ` + -RustTarget $RustTarget ` + -CargoArgs "cargo test -p openshell-driver-mxc --test wxc_exec_real --target $RustTarget -- --ignored --test-threads=1 --nocapture" ` + -LogName "test-$RustTarget-mxc-real.log" +} + function Get-Sha256([string] $Path) { $stream = [System.IO.File]::OpenRead($Path) try { @@ -692,16 +704,17 @@ if ($Action -eq "ci" -and (Get-HostArch) -ne "amd64") { } $targets = Get-SelectedTargets $Target -if ($Action -in @("test", "test-precommit", "test-unsupported")) { +if ($Action -in @("test", "test-precommit", "test-unsupported", "test-mxc-real")) { foreach ($rustTarget in $targets) { Assert-NativeTestTarget $rustTarget } } -if ($Action -in @("check", "lint", "build", "test", "test-precommit", "test-unsupported", "ci")) { +if ($Action -in @("check", "lint", "build", "test", "test-precommit", "test-unsupported", "test-mxc-real", "ci")) { $z3Features = Configure-Z3 $Z3WorkspaceFeatures = $z3Features.WorkspaceFeatures $Z3ServerFeatures = $z3Features.ServerFeatures + $Z3GatewayFeatures = $z3Features.GatewayFeatures $env:LIBCLANG_PATH = Resolve-LibclangPath Add-PathEntry $env:LIBCLANG_PATH Write-Host "==> LIBCLANG_PATH=$env:LIBCLANG_PATH" @@ -740,6 +753,11 @@ switch ($Action) { Invoke-UnsupportedContractTests $rustTarget } } + "test-mxc-real" { + foreach ($rustTarget in $targets) { + Invoke-MxcRealTests $rustTarget + } + } "artifacts" { Show-Artifacts $targets } diff --git a/tasks/windows.toml b/tasks/windows.toml index 59c2a8f2c2..e07b37c1ff 100644 --- a/tasks/windows.toml +++ b/tasks/windows.toml @@ -65,9 +65,14 @@ run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 ci all" ["windows:test:mxc-real:x64"] -description = "Run real-wxc-exec Tier-2 integration tests (skip-safe: tests print SKIP when binary/backend absent)" +description = "Run native x64 real-wxc-exec Tier-2 integration tests (skip-safe: tests print SKIP when binary/backend absent)" run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" -run_windows = "cargo test -p openshell-driver-mxc --test wxc_exec_real --target x86_64-pc-windows-msvc -- --ignored --test-threads=1" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-mxc-real x86_64-pc-windows-msvc" + +["windows:test:mxc-real:arm64"] +description = "Run native ARM64 real-wxc-exec Tier-2 integration tests (skip-safe: tests print SKIP when binary/backend absent)" +run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-mxc-real aarch64-pc-windows-msvc" ["windows:e2e:mxc"] description = "Run MXC Tier-3 e2e scenario runner against real wxc-exec (probe-gated; skip-safe on hosts without the binary)"