mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-11-03 08:17:44 +08:00
refactor(runtime): extract driver-independent MXC foundations
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
@@ -58,6 +58,19 @@ configuration validation live in `openshell_core::middleware`. Policy admission
|
||||
and the supervisor runtime consume the same contract without introducing a
|
||||
dependency from the policy crate to the supervisor implementation.
|
||||
|
||||
## Extension Capabilities
|
||||
|
||||
Extensions advertise namespaced string capabilities through `PeerMetadata`.
|
||||
Use shared constants in `extension_protocol` and pass optional capabilities to
|
||||
`extension_metadata`; do not add per-driver booleans for policy features.
|
||||
Negotiation validates and retains the extension's supported capability list.
|
||||
|
||||
`POLICY_UI_V1` (`openshell.policy.ui.v1`) declares complete startup UI-policy
|
||||
support. The gateway checks the effective policy before driver validation or
|
||||
creation and rejects any explicit UI section when the capability is absent.
|
||||
Supporting drivers validate and enforce the concrete settings. Omitted UI
|
||||
preserves existing driver behavior; changing UI requires sandbox recreation.
|
||||
|
||||
## Protobuf Struct Conversion
|
||||
|
||||
Use `openshell_core::proto_struct` when crossing between `serde_json` values and
|
||||
|
||||
@@ -19,6 +19,10 @@ pub const PROTOCOL_MINOR: u32 = 0;
|
||||
/// configured signer before accepting sandbox creation.
|
||||
pub const COMPUTE_LAUNCH_AUTHENTICATION: &str = "openshell.compute.launch-authentication";
|
||||
|
||||
/// Compute drivers advertising this capability enforce the complete startup UI
|
||||
/// policy contract. Absence means explicit UI policy must be rejected.
|
||||
pub const POLICY_UI_V1: &str = "openshell.policy.ui.v1";
|
||||
|
||||
const MAX_IMPLEMENTATION_NAME_BYTES: usize = 128;
|
||||
const MAX_IMPLEMENTATION_VERSION_BYTES: usize = 128;
|
||||
const MAX_CAPABILITY_BYTES: usize = 128;
|
||||
@@ -398,6 +402,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_policy_capability_uses_existing_optional_negotiation() {
|
||||
let gateway = gateway_metadata(ExtensionFamily::Compute);
|
||||
let extension = extension_metadata(
|
||||
ExtensionFamily::Compute,
|
||||
"openshell/test-ui-driver",
|
||||
"test",
|
||||
[POLICY_UI_V1.to_string()],
|
||||
);
|
||||
let result = negotiate(
|
||||
ExtensionFamily::Compute,
|
||||
"test-ui-driver",
|
||||
&gateway,
|
||||
Some(extension),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
result
|
||||
.supported_capabilities
|
||||
.iter()
|
||||
.any(|value| value == POLICY_UI_V1)
|
||||
);
|
||||
assert!(
|
||||
!result
|
||||
.required_capabilities
|
||||
.iter()
|
||||
.any(|value| value == POLICY_UI_V1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_metadata_and_major_skew_are_actionable() {
|
||||
let (gateway, mut extension) = compatible();
|
||||
|
||||
@@ -57,6 +57,24 @@ pub fn openshell_state_dir() -> Result<PathBuf> {
|
||||
Ok(xdg_state_dir()?.join("openshell"))
|
||||
}
|
||||
|
||||
/// Platform default for gateway audit files. Sink configuration and writing
|
||||
/// remain portable; only the directory convention depends on the host.
|
||||
pub fn gateway_audit_log_dir() -> PathBuf {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(directory) = std::env::var_os("ProgramData") {
|
||||
return PathBuf::from(directory).join("OpenShell").join("logs");
|
||||
}
|
||||
std::env::temp_dir().join("openshell").join("logs")
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
openshell_state_dir()
|
||||
.unwrap_or_else(|_| std::env::temp_dir().join("openshell"))
|
||||
.join("logs")
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the XDG data base directory.
|
||||
///
|
||||
/// Returns `$XDG_DATA_HOME` if set, otherwise `$HOME/.local/share`.
|
||||
|
||||
@@ -113,6 +113,10 @@ impl TryFrom<ProtoSandboxPolicy> for SandboxPolicy {
|
||||
type Error = miette::Report;
|
||||
|
||||
fn try_from(proto: ProtoSandboxPolicy) -> Result<Self, Self::Error> {
|
||||
// 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. A capable
|
||||
// compute driver consumes the typed proto directly.
|
||||
// In cluster mode we always run with proxy networking so all egress
|
||||
// can be evaluated by OPA.
|
||||
let network = NetworkPolicy {
|
||||
@@ -192,6 +196,7 @@ impl From<ProtoProcessPolicy> for ProcessPolicy {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::proto::{UiClipboardAccess, UiPolicy};
|
||||
|
||||
#[test]
|
||||
fn try_from_maps_known_compatibility_values() {
|
||||
@@ -233,4 +238,26 @@ mod tests {
|
||||
assert!(!is_valid_landlock_compatibility("nope"));
|
||||
assert!(!is_valid_landlock_compatibility("BestEffort"));
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +399,18 @@ impl ProviderCredentialState {
|
||||
.revision
|
||||
}
|
||||
|
||||
/// Whether this snapshot contains endpoint-bound material that must be
|
||||
/// resolved by a network proxy rather than exposed to the child process.
|
||||
#[must_use]
|
||||
pub fn requires_proxy_resolution(&self) -> bool {
|
||||
let inner = self
|
||||
.inner
|
||||
.read()
|
||||
.expect("provider credential state poisoned");
|
||||
!inner.static_credential_bindings.is_empty()
|
||||
|| !inner.current.dynamic_credentials.is_empty()
|
||||
}
|
||||
|
||||
/// Remove a key from the credential snapshot's child env.
|
||||
///
|
||||
/// Used when a sandbox-side service (e.g., metadata server) fails to start
|
||||
|
||||
@@ -1109,6 +1109,7 @@ fn handled_fields_inventory() {
|
||||
// ── (1) Top-level keys: serialize a SandboxPolicy with every section
|
||||
// present-but-minimal, then collect YAML keys. ──────────────────────────
|
||||
let full_toplevel_policy = SandboxPolicy {
|
||||
ui: None,
|
||||
version: 1,
|
||||
filesystem: Some(FilesystemPolicy {
|
||||
include_workdir: false,
|
||||
|
||||
@@ -857,7 +857,7 @@ pub trait BoundaryLoopbackConnector: Send + Sync {
|
||||
/// that owns a connection or one of its executable ancestors. A missing digest
|
||||
/// is `None`, never an empty value; binary-scoped policy cannot authorize an
|
||||
/// executable whose digest is unavailable.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExecutableIdentity {
|
||||
/// Absolute executable path in the workload filesystem namespace.
|
||||
pub path: PathBuf,
|
||||
@@ -871,7 +871,7 @@ pub struct ExecutableIdentity {
|
||||
///
|
||||
/// How a backend resolves identity is private to that backend; the shape and
|
||||
/// fail-closed semantics do not change.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BinaryIdentity {
|
||||
/// Executable that owns the accepted connection.
|
||||
pub executable: ExecutableIdentity,
|
||||
|
||||
@@ -193,6 +193,12 @@ pub struct PolicyDocument {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub process: Option<ProcessPolicy>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_non_null_optional_field",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub ui: Option<UiPolicy>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub network_policies: BTreeMap<String, NetworkPolicyRule>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
@@ -234,6 +240,34 @@ pub struct ProcessPolicy {
|
||||
pub run_as_group: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct UiPolicy {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub allow_graphical_ui: bool,
|
||||
#[serde(default, skip_serializing_if = "UiClipboardAccess::is_none")]
|
||||
pub clipboard: UiClipboardAccess,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub allow_input_injection: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UiClipboardAccess {
|
||||
#[default]
|
||||
None,
|
||||
Read,
|
||||
Write,
|
||||
All,
|
||||
}
|
||||
|
||||
impl UiClipboardAccess {
|
||||
#[must_use]
|
||||
pub const fn is_none(&self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NetworkPolicyRule {
|
||||
@@ -698,6 +732,7 @@ fn inspect_document(root: &serde_yml::Value) -> InspectionResult {
|
||||
"filesystem_policy",
|
||||
"landlock",
|
||||
"process",
|
||||
"ui",
|
||||
"network_policies",
|
||||
"network_middlewares",
|
||||
],
|
||||
@@ -717,6 +752,11 @@ fn inspect_document(root: &serde_yml::Value) -> InspectionResult {
|
||||
"process",
|
||||
&["run_as_user", "run_as_group"],
|
||||
)?;
|
||||
inspect_named(
|
||||
root.get("ui"),
|
||||
"ui",
|
||||
&["allow_graphical_ui", "clipboard", "allow_input_injection"],
|
||||
)?;
|
||||
|
||||
for (name, rule) in open_map(root.get("network_policies")) {
|
||||
let path = join("network_policies", name);
|
||||
@@ -1176,6 +1216,7 @@ mod tests {
|
||||
for source in [
|
||||
"version: 1\nfilesystem_policy: null\n",
|
||||
"version: 1\nprocess: null\n",
|
||||
"version: 1\nui: null\n",
|
||||
"version: 1\nmetadata: null\n",
|
||||
"version: 1\nnetwork_policies:\n x:\n endpoints:\n - host: x\n port: 443\n mcp: null\n",
|
||||
] {
|
||||
@@ -1212,6 +1253,7 @@ mod tests {
|
||||
"landlock.future",
|
||||
),
|
||||
("version: 1\nprocess: { future: true }\n", "process.future"),
|
||||
("version: 1\nui: { future: true }\n", "ui.future"),
|
||||
(
|
||||
"version: 1\nnetwork_policies: { api: { future: true } }\n",
|
||||
"network_policies.api.future",
|
||||
|
||||
@@ -31,9 +31,27 @@ use openshell_core::mcp::{
|
||||
use openshell_core::proto::{
|
||||
FilesystemPolicy, GraphqlOperation, L7Allow, L7DenyRule, L7QueryMatcher, L7Rule,
|
||||
LandlockPolicy, McpOptions, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, ProcessPolicy,
|
||||
SandboxPolicy,
|
||||
SandboxPolicy, UiClipboardAccess, UiPolicy,
|
||||
};
|
||||
|
||||
const fn ui_clipboard_to_proto(value: UiClipboardAccessDef) -> UiClipboardAccess {
|
||||
match value {
|
||||
UiClipboardAccessDef::None => UiClipboardAccess::None,
|
||||
UiClipboardAccessDef::Read => UiClipboardAccess::Read,
|
||||
UiClipboardAccessDef::Write => UiClipboardAccess::Write,
|
||||
UiClipboardAccessDef::All => UiClipboardAccess::All,
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_clipboard_from_proto(value: i32) -> UiClipboardAccessDef {
|
||||
match UiClipboardAccess::try_from(value).unwrap_or(UiClipboardAccess::Unspecified) {
|
||||
UiClipboardAccess::Unspecified | UiClipboardAccess::None => UiClipboardAccessDef::None,
|
||||
UiClipboardAccess::Read => UiClipboardAccessDef::Read,
|
||||
UiClipboardAccess::Write => UiClipboardAccessDef::Write,
|
||||
UiClipboardAccess::All => UiClipboardAccessDef::All,
|
||||
}
|
||||
}
|
||||
|
||||
pub use compose::{
|
||||
PROVIDER_RULE_NAME_PREFIX, ProviderPolicyLayer, compose_effective_policy,
|
||||
is_provider_rule_name, provider_rule_name, strip_provider_rule_names,
|
||||
@@ -66,6 +84,7 @@ use openshell_policy_schema::{
|
||||
NetworkCredentialBinding as NetworkCredentialBindingDef, NetworkEndpoint as NetworkEndpointDef,
|
||||
NetworkPolicyRule as NetworkPolicyRuleDef, ParameterMatcher as ParamMatcherDef,
|
||||
PolicyDocument as PolicyFile, ProcessPolicy as ProcessDef, QueryMatcher as QueryMatcherDef,
|
||||
UiClipboardAccess as UiClipboardAccessDef, UiPolicy as UiDef,
|
||||
};
|
||||
|
||||
fn json_rpc_config_from_proto(max_body_bytes: u32) -> Option<JsonRpcConfigDef> {
|
||||
@@ -608,6 +627,11 @@ fn to_proto(raw: PolicyFile) -> Result<SandboxPolicy> {
|
||||
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(ui.clipboard) as i32,
|
||||
allow_input_injection: ui.allow_input_injection,
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares,
|
||||
})
|
||||
@@ -651,6 +675,12 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
|
||||
}
|
||||
});
|
||||
|
||||
let ui = policy.ui.as_ref().map(|ui| UiDef {
|
||||
allow_graphical_ui: ui.allow_graphical_ui,
|
||||
clipboard: ui_clipboard_from_proto(ui.clipboard),
|
||||
allow_input_injection: ui.allow_input_injection,
|
||||
});
|
||||
|
||||
let network_policies = policy
|
||||
.network_policies
|
||||
.iter()
|
||||
@@ -795,6 +825,7 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
|
||||
filesystem_policy,
|
||||
landlock,
|
||||
process,
|
||||
ui,
|
||||
network_policies,
|
||||
network_middlewares,
|
||||
})
|
||||
@@ -988,6 +1019,7 @@ pub fn restrictive_default_policy() -> SandboxPolicy {
|
||||
compatibility: "best_effort".into(),
|
||||
}),
|
||||
process: None,
|
||||
ui: None,
|
||||
network_policies: HashMap::new(),
|
||||
network_middlewares: HashMap::default(),
|
||||
}
|
||||
@@ -1023,6 +1055,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 `/`).
|
||||
@@ -1124,6 +1158,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}")
|
||||
}
|
||||
@@ -1330,6 +1370,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 `/`)
|
||||
@@ -1444,6 +1485,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,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref fs) = policy.filesystem
|
||||
&& let Err(errors) = validate_filesystem_paths(&fs.read_only, &fs.read_write)
|
||||
{
|
||||
@@ -1855,6 +1904,7 @@ fn validate_and_canonicalize_mcp_policy_schema(
|
||||
.map_err(|violations| PolicyValidationError { violations })?;
|
||||
materialize_default_mcp_versions(&mut policy);
|
||||
canonicalize_mcp_version_allowlists(&mut policy);
|
||||
canonicalize_ui_defaults(&mut policy);
|
||||
validate_mcp_policy_schema(&policy, McpVersionPresence::RequireMaterialized)
|
||||
.map_err(|violations| PolicyValidationError { violations })?;
|
||||
Ok(policy)
|
||||
@@ -1877,6 +1927,7 @@ pub fn validate_and_canonicalize_sandbox_policy(
|
||||
.map_err(|violations| PolicyValidationError { violations })?;
|
||||
materialize_default_mcp_versions(&mut policy);
|
||||
canonicalize_mcp_version_allowlists(&mut policy);
|
||||
canonicalize_ui_defaults(&mut policy);
|
||||
debug_assert!(
|
||||
validate_sandbox_policy(&policy).is_ok(),
|
||||
"validated MCP canonicalization must preserve every policy invariant"
|
||||
@@ -1884,6 +1935,20 @@ pub fn validate_and_canonicalize_sandbox_policy(
|
||||
Ok(policy)
|
||||
}
|
||||
|
||||
/// Materialize protobuf UI defaults that have a distinct canonical enum value.
|
||||
///
|
||||
/// Proto3 clients commonly leave `clipboard` at `Unspecified(0)`. `OpenShell`
|
||||
/// defines that value as deny, so canonical policy state stores the equivalent
|
||||
/// explicit `None(1)`. This keeps protobuf, YAML, hashes, and static-field
|
||||
/// comparisons stable across a serialize/parse round trip.
|
||||
fn canonicalize_ui_defaults(policy: &mut SandboxPolicy) {
|
||||
if let Some(ui) = policy.ui.as_mut()
|
||||
&& ui.clipboard == UiClipboardAccess::Unspecified as i32
|
||||
{
|
||||
ui.clipboard = UiClipboardAccess::None as i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace absent protobuf MCP options and empty revision lists with the
|
||||
/// single pinned policy default while preserving every explicit MCP option.
|
||||
pub(crate) fn materialize_default_mcp_versions(policy: &mut SandboxPolicy) {
|
||||
@@ -2003,6 +2068,99 @@ 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_unspecified_clipboard_canonicalizes_to_none_across_yaml_round_trip() {
|
||||
let raw = SandboxPolicy {
|
||||
version: 1,
|
||||
ui: Some(UiPolicy {
|
||||
allow_graphical_ui: true,
|
||||
clipboard: UiClipboardAccess::Unspecified as i32,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let canonical = validate_and_canonicalize_sandbox_policy(raw)
|
||||
.expect("unspecified clipboard must be a valid deny default");
|
||||
assert_eq!(
|
||||
canonical.ui.as_ref().expect("UI remains present").clipboard,
|
||||
UiClipboardAccess::None as i32
|
||||
);
|
||||
|
||||
let yaml = serialize_sandbox_policy(&canonical).expect("canonical UI serializes");
|
||||
let reparsed = parse_sandbox_policy(&yaml).expect("canonical UI reparses");
|
||||
assert_eq!(reparsed, canonical);
|
||||
}
|
||||
|
||||
#[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 decode sandbox policy fields")
|
||||
);
|
||||
}
|
||||
|
||||
#[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() {
|
||||
@@ -3702,6 +3860,7 @@ network_policies:
|
||||
process: None,
|
||||
filesystem: None,
|
||||
landlock: None,
|
||||
ui: None,
|
||||
network_policies: HashMap::new(),
|
||||
network_middlewares: HashMap::default(),
|
||||
};
|
||||
@@ -4160,6 +4319,7 @@ network_policies:
|
||||
}),
|
||||
filesystem: None,
|
||||
landlock: None,
|
||||
ui: None,
|
||||
network_policies: HashMap::new(),
|
||||
network_middlewares: HashMap::default(),
|
||||
};
|
||||
@@ -4176,6 +4336,7 @@ network_policies:
|
||||
}),
|
||||
filesystem: None,
|
||||
landlock: None,
|
||||
ui: None,
|
||||
network_policies: HashMap::new(),
|
||||
network_middlewares: HashMap::default(),
|
||||
};
|
||||
@@ -4248,6 +4409,7 @@ network_policies:
|
||||
}),
|
||||
filesystem: None,
|
||||
landlock: None,
|
||||
ui: None,
|
||||
network_policies: HashMap::new(),
|
||||
network_middlewares: HashMap::default(),
|
||||
};
|
||||
|
||||
@@ -83,6 +83,11 @@ impl ContainmentEndpoint for Endpoint {
|
||||
pub fn parse_policy_str(source: &str) -> Result<ContainmentPolicy, ParsePolicyError> {
|
||||
let document = openshell_policy_schema::parse_policy(source)
|
||||
.map_err(|error| ParsePolicyError(format!("invalid policy: {error:#}")))?;
|
||||
if document.ui.is_some() {
|
||||
return Err(ParsePolicyError(
|
||||
"UI policy containment is not supported".to_string(),
|
||||
));
|
||||
}
|
||||
let mut filesystem_policy = document.effective_filesystem_policy();
|
||||
normalize_filesystem_paths(&mut filesystem_policy)?;
|
||||
let PolicyDocument {
|
||||
@@ -92,6 +97,7 @@ pub fn parse_policy_str(source: &str) -> Result<ContainmentPolicy, ParsePolicyEr
|
||||
landlock,
|
||||
process,
|
||||
network_middlewares,
|
||||
ui: _,
|
||||
} = document;
|
||||
Ok(ContainmentPolicy {
|
||||
filesystem_policy,
|
||||
@@ -2225,6 +2231,17 @@ mod tests {
|
||||
parse_policy_str(value).expect("valid policy")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_policy_is_not_silently_omitted_from_containment_checks() {
|
||||
let error = parse_policy_str("version: 1\nui:\n allow_graphical_ui: false\n")
|
||||
.expect_err("unmodeled UI authority must fail closed");
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("UI policy containment is not supported")
|
||||
);
|
||||
}
|
||||
|
||||
fn options() -> CheckOptions {
|
||||
CheckOptions::new(Duration::from_secs(10))
|
||||
}
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod accept_interrupt;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod boundary_exec;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod boundary_io;
|
||||
mod boundary_server;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod child_env;
|
||||
pub mod container_log;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) mod delegated;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod main_session;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod managed_children;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod network_broker;
|
||||
@@ -23,7 +27,9 @@ pub mod perf;
|
||||
pub mod process;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod provider_files;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod pty;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod sandbox;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod sftp;
|
||||
|
||||
@@ -24,7 +24,7 @@ use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
const COPY_SELF_SUBCOMMAND: &str = "copy-self";
|
||||
const BOOTSTRAP_SUBCOMMAND: &str = "bootstrap";
|
||||
const SEED_WORKSPACE_SUBCOMMAND: &str = "seed-workspace";
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
#[cfg(target_os = "linux")]
|
||||
const KUBERNETES_BOOTSTRAP_SECRET_FILES: [&str; 3] = ["boundary.json", "tls.crt", "tls.key"];
|
||||
#[cfg(target_os = "linux")]
|
||||
const BOOTSTRAP_INPUT_ROOT: &str = "/.openshell/bootstrap-input";
|
||||
@@ -1622,7 +1622,7 @@ fn run_kubernetes_bootstrap() -> Result<()> {
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn stage_kubernetes_bootstrap_at(source: &Path, runtime: &Path, state: &Path) -> Result<()> {
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
@@ -1679,7 +1679,7 @@ fn stage_kubernetes_bootstrap_at(source: &Path, runtime: &Path, state: &Path) ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn copy_projected_secret_file(
|
||||
source_root: &Path,
|
||||
name: &str,
|
||||
@@ -1697,7 +1697,7 @@ fn copy_projected_secret_file(
|
||||
copy_regular_file(&canonical_source, destination, mode)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn copy_regular_file(source: &Path, destination: &Path, mode: u32) -> Result<()> {
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{Read as _, Write as _};
|
||||
@@ -1734,10 +1734,12 @@ fn copy_regular_file(source: &Path, destination: &Path, mode: u32) -> Result<()>
|
||||
|
||||
/// Seed the persistent workspace from the agent image as the final workload
|
||||
/// identity. This replaces the former root shell/tar init container.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn seed_kubernetes_workspace() -> Result<()> {
|
||||
seed_kubernetes_workspace_at(Path::new("/sandbox"), Path::new("/mnt/openshell-workspace"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn copy_workspace_tree(source: &Path, destination: &Path) -> Result<()> {
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{Read as _, Write as _};
|
||||
@@ -1790,6 +1792,7 @@ fn copy_workspace_tree(source: &Path, destination: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn seed_kubernetes_workspace_at(source: &Path, destination: &Path) -> Result<()> {
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write as _;
|
||||
@@ -1834,6 +1837,13 @@ fn seed_kubernetes_workspace_at(source: &Path, destination: &Path) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn seed_kubernetes_workspace() -> Result<()> {
|
||||
Err(miette::miette!(
|
||||
"Kubernetes workspace seeding is supported only on Linux"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn run_boundary(bootstrap: &Path, log_level: &str) -> Result<()> {
|
||||
let console_filter =
|
||||
@@ -1900,7 +1910,7 @@ fn main() -> Result<()> {
|
||||
run_boundary(&args.bootstrap, &args.log_level)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, target_os = "linux"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
@@ -41,8 +41,9 @@ use openshell_core::proto::compute::v1::{
|
||||
compute_driver_server::ComputeDriver, watch_sandboxes_event,
|
||||
};
|
||||
use openshell_core::proto::{
|
||||
PlatformEvent, Sandbox, SandboxCondition, SandboxPhase, SandboxRestartPolicy, SandboxSpec,
|
||||
SandboxStatus, SandboxTemplate, SandboxWorkloadTemplate, ServiceEndpoint, SshSession,
|
||||
PlatformEvent, Sandbox, SandboxCondition, SandboxPhase, SandboxPolicy as ProtoSandboxPolicy,
|
||||
SandboxRestartPolicy, SandboxSpec, SandboxStatus, SandboxTemplate, SandboxWorkloadTemplate,
|
||||
ServiceEndpoint, SshSession,
|
||||
};
|
||||
use openshell_core::telemetry::TelemetryComputeDriver;
|
||||
use openshell_core::{ObjectLabels, ObjectWorkspace};
|
||||
@@ -75,6 +76,23 @@ pub type SharedComputeDriver =
|
||||
Arc<dyn ComputeDriver<WatchSandboxesStream = DriverWatchStream> + Send + Sync>;
|
||||
|
||||
use provisioning_operation::ProvisioningOperationError;
|
||||
/// Driver-specific values that must be delivered atomically with sandbox
|
||||
/// creation without expanding the public compute-driver protobuf contract.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SandboxCreateRuntimeInputs {
|
||||
pub effective_policy: Option<ProtoSandboxPolicy>,
|
||||
pub launch_authentication: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl SandboxCreateRuntimeInputs {
|
||||
#[must_use]
|
||||
pub(crate) fn new(effective_policy: ProtoSandboxPolicy) -> Self {
|
||||
Self {
|
||||
effective_policy: Some(effective_policy),
|
||||
launch_authentication: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
use traced_driver::TracedDriver;
|
||||
|
||||
const LIFECYCLE_SWEEP_PAGE_SIZE: u32 = 1000;
|
||||
@@ -262,6 +280,16 @@ struct SandboxDeleteTarget {
|
||||
sandbox_name: String,
|
||||
}
|
||||
|
||||
/// Optional caller-owned preconditions for an identity-safe sandbox delete.
|
||||
///
|
||||
/// These values are validated again while holding the sandbox lifecycle and
|
||||
/// gateway-global locks, immediately before the durable `Deleting` mutation.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SandboxDeletePreconditions {
|
||||
pub expected_sandbox_id: Option<String>,
|
||||
pub expected_resource_version: Option<u64>,
|
||||
}
|
||||
|
||||
/// Identity and driver result for a completed delete request.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct DeleteSandboxResult {
|
||||
@@ -299,6 +327,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,
|
||||
@@ -987,14 +1016,32 @@ impl ComputeRuntime {
|
||||
}
|
||||
|
||||
pub async fn validate_sandbox_create(&self, sandbox: &Sandbox) -> Result<(), Status> {
|
||||
self.validate_sandbox_create_with_runtime_inputs(
|
||||
sandbox,
|
||||
&SandboxCreateRuntimeInputs::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn validate_sandbox_create_with_runtime_inputs(
|
||||
&self,
|
||||
sandbox: &Sandbox,
|
||||
runtime_inputs: &SandboxCreateRuntimeInputs,
|
||||
) -> Result<(), Status> {
|
||||
self.validate_caller_driver_config(
|
||||
sandbox
|
||||
.spec
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.template.as_ref()),
|
||||
)?;
|
||||
self.validate_policy_capabilities(sandbox, runtime_inputs.effective_policy.as_ref())?;
|
||||
let mut driver_sandbox = driver_sandbox_from_public(sandbox, &self.driver_info.name)
|
||||
.map_err(|status| *status)?;
|
||||
if let Some(effective_policy) = runtime_inputs.effective_policy.as_ref()
|
||||
&& let Some(spec) = driver_sandbox.spec.as_mut()
|
||||
{
|
||||
spec.policy = Some(effective_policy.clone());
|
||||
}
|
||||
// Peek, never consume: create runs the same path immediately after and
|
||||
// must still find the token.
|
||||
if let Some(token) = take_staging_token(&mut driver_sandbox) {
|
||||
@@ -1017,6 +1064,30 @@ impl ComputeRuntime {
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn validate_policy_capabilities(
|
||||
&self,
|
||||
sandbox: &Sandbox,
|
||||
effective_policy: Option<&ProtoSandboxPolicy>,
|
||||
) -> Result<(), Status> {
|
||||
let has_explicit_ui = effective_policy
|
||||
.or_else(|| sandbox.spec.as_ref().and_then(|spec| spec.policy.as_ref()))
|
||||
.and_then(|policy| policy.ui.as_ref())
|
||||
.is_some();
|
||||
let supports_ui = self
|
||||
.driver_info
|
||||
.negotiated_extension
|
||||
.supported_capabilities
|
||||
.iter()
|
||||
.any(|capability| capability == openshell_core::extension_protocol::POLICY_UI_V1);
|
||||
if has_explicit_ui && !supports_ui {
|
||||
return Err(Status::failed_precondition(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,
|
||||
@@ -1088,11 +1159,59 @@ impl ComputeRuntime {
|
||||
await_main_process_attachment: bool,
|
||||
lifecycle_guard: SandboxLifecycleGuard,
|
||||
global_guard: SandboxSyncGuard,
|
||||
) -> Result<Sandbox, Status> {
|
||||
Box::pin(self.create_sandbox_with_runtime_inputs_and_guards(
|
||||
sandbox,
|
||||
sandbox_token,
|
||||
await_main_process_attachment,
|
||||
SandboxCreateRuntimeInputs {
|
||||
launch_authentication,
|
||||
..Default::default()
|
||||
},
|
||||
lifecycle_guard,
|
||||
global_guard,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_sandbox_with_runtime_inputs(
|
||||
&self,
|
||||
sandbox: Sandbox,
|
||||
sandbox_token: Option<String>,
|
||||
await_main_process_attachment: bool,
|
||||
runtime_inputs: SandboxCreateRuntimeInputs,
|
||||
) -> Result<Sandbox, Status> {
|
||||
let (lifecycle_guard, global_guard) = self
|
||||
.sandbox_create_guards(sandbox.object_id())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
crate::grpc::persistence_error_to_status(error, "acquire sandbox mutation lock")
|
||||
})?;
|
||||
Box::pin(self.create_sandbox_with_runtime_inputs_and_guards(
|
||||
sandbox,
|
||||
sandbox_token,
|
||||
await_main_process_attachment,
|
||||
runtime_inputs,
|
||||
lifecycle_guard,
|
||||
global_guard,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_sandbox_with_runtime_inputs_and_guards(
|
||||
&self,
|
||||
sandbox: Sandbox,
|
||||
sandbox_token: Option<String>,
|
||||
await_main_process_attachment: bool,
|
||||
runtime_inputs: SandboxCreateRuntimeInputs,
|
||||
lifecycle_guard: SandboxLifecycleGuard,
|
||||
global_guard: SandboxSyncGuard,
|
||||
) -> Result<Sandbox, Status> {
|
||||
// Defend the internal create path too, before consuming a staged archive
|
||||
// or persisting the sandbox. The gRPC handler checks before driver validation.
|
||||
self.validate_launch_signer_configured(
|
||||
launch_authentication
|
||||
runtime_inputs
|
||||
.launch_authentication
|
||||
.as_ref()
|
||||
.is_some_and(|auth| !auth.is_empty()),
|
||||
)?;
|
||||
@@ -1102,6 +1221,7 @@ impl ComputeRuntime {
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.template.as_ref()),
|
||||
)?;
|
||||
self.validate_policy_capabilities(&sandbox, runtime_inputs.effective_policy.as_ref())?;
|
||||
let sandbox_id = sandbox.object_id().to_string();
|
||||
let mut sandbox = sandbox;
|
||||
|
||||
@@ -1116,6 +1236,11 @@ impl ComputeRuntime {
|
||||
|
||||
let mut driver_sandbox = driver_sandbox_from_public(&sandbox, &self.driver_info.name)
|
||||
.map_err(|status| *status)?;
|
||||
if let Some(effective_policy) = runtime_inputs.effective_policy
|
||||
&& let Some(spec) = driver_sandbox.spec.as_mut()
|
||||
{
|
||||
spec.policy = Some(effective_policy);
|
||||
}
|
||||
if let Some(staged) = staged.as_ref() {
|
||||
set_rootfs_tar_path(&mut driver_sandbox, staged.path());
|
||||
}
|
||||
@@ -1168,7 +1293,7 @@ impl ComputeRuntime {
|
||||
}
|
||||
if let Some(spec) = driver_sandbox.spec.as_mut() {
|
||||
spec.await_main_process_attachment = await_main_process_attachment;
|
||||
spec.launch_authentication = launch_authentication.unwrap_or_default();
|
||||
spec.launch_authentication = runtime_inputs.launch_authentication.unwrap_or_default();
|
||||
}
|
||||
let result = Box::pin(self.await_provisioning_operation(
|
||||
&sandbox,
|
||||
@@ -2282,8 +2407,12 @@ impl ComputeRuntime {
|
||||
workspace: &str,
|
||||
name: &str,
|
||||
) -> Result<DeleteSandboxResult, Status> {
|
||||
self.delete_sandbox_allow_missing(workspace, name, false)
|
||||
.await
|
||||
self.delete_sandbox_with_preconditions(
|
||||
workspace,
|
||||
name,
|
||||
SandboxDeletePreconditions::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_sandbox_allow_missing(
|
||||
@@ -2292,6 +2421,55 @@ impl ComputeRuntime {
|
||||
name: &str,
|
||||
allow_missing: bool,
|
||||
) -> Result<DeleteSandboxResult, Status> {
|
||||
self.delete_sandbox_with_options(
|
||||
workspace,
|
||||
name,
|
||||
allow_missing,
|
||||
SandboxDeletePreconditions::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_sandbox_with_preconditions(
|
||||
&self,
|
||||
workspace: &str,
|
||||
name: &str,
|
||||
preconditions: SandboxDeletePreconditions,
|
||||
) -> Result<DeleteSandboxResult, Status> {
|
||||
self.delete_sandbox_with_options(workspace, name, false, preconditions)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_sandbox_allow_missing_with_preconditions(
|
||||
&self,
|
||||
workspace: &str,
|
||||
name: &str,
|
||||
allow_missing: bool,
|
||||
preconditions: SandboxDeletePreconditions,
|
||||
) -> Result<DeleteSandboxResult, Status> {
|
||||
if preconditions == SandboxDeletePreconditions::default() {
|
||||
return self
|
||||
.delete_sandbox_allow_missing(workspace, name, allow_missing)
|
||||
.await;
|
||||
}
|
||||
self.delete_sandbox_with_options(workspace, name, allow_missing, preconditions)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_sandbox_with_options(
|
||||
&self,
|
||||
workspace: &str,
|
||||
name: &str,
|
||||
allow_missing: bool,
|
||||
preconditions: SandboxDeletePreconditions,
|
||||
) -> Result<DeleteSandboxResult, Status> {
|
||||
if preconditions.expected_resource_version.is_some()
|
||||
&& preconditions.expected_sandbox_id.is_none()
|
||||
{
|
||||
return Err(Status::invalid_argument(
|
||||
"expected_resource_version requires expected_sandbox_id",
|
||||
));
|
||||
}
|
||||
// Resolve and acquire both request-side locks before spawning the
|
||||
// owned worker. Cancellation while any of these awaits is pending is
|
||||
// harmless because no mutation or detached work has started.
|
||||
@@ -2309,11 +2487,20 @@ impl ComputeRuntime {
|
||||
}
|
||||
return Err(Status::not_found("sandbox not found"));
|
||||
};
|
||||
if preconditions
|
||||
.expected_sandbox_id
|
||||
.as_deref()
|
||||
.is_some_and(|expected| expected != candidate.object_id())
|
||||
{
|
||||
return Err(Status::aborted(
|
||||
"sandbox identity does not match expected_sandbox_id",
|
||||
));
|
||||
}
|
||||
let target = SandboxDeleteTarget {
|
||||
sandbox_id: candidate.object_id().to_string(),
|
||||
sandbox_name: candidate.object_name().to_string(),
|
||||
};
|
||||
self.delete_sandbox_target(target).await
|
||||
self.delete_sandbox_target(target, preconditions).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_sandbox_by_id(
|
||||
@@ -2321,16 +2508,20 @@ impl ComputeRuntime {
|
||||
sandbox_id: &str,
|
||||
sandbox_name: &str,
|
||||
) -> Result<DeleteSandboxResult, Status> {
|
||||
self.delete_sandbox_target(SandboxDeleteTarget {
|
||||
sandbox_id: sandbox_id.to_string(),
|
||||
sandbox_name: sandbox_name.to_string(),
|
||||
})
|
||||
self.delete_sandbox_target(
|
||||
SandboxDeleteTarget {
|
||||
sandbox_id: sandbox_id.to_string(),
|
||||
sandbox_name: sandbox_name.to_string(),
|
||||
},
|
||||
SandboxDeletePreconditions::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_sandbox_target(
|
||||
&self,
|
||||
target: SandboxDeleteTarget,
|
||||
preconditions: SandboxDeletePreconditions,
|
||||
) -> Result<DeleteSandboxResult, Status> {
|
||||
let delete_guard = self.lifecycle_gates.lock_for(&target.sandbox_id).await;
|
||||
let global_guard = self.lock_global_for_lifecycle(&delete_guard).await;
|
||||
@@ -2344,7 +2535,13 @@ impl ComputeRuntime {
|
||||
let request_span = tracing::Span::current();
|
||||
tokio::spawn(
|
||||
async move {
|
||||
Box::pin(runtime.delete_sandbox_inner(target, delete_guard, global_guard)).await
|
||||
Box::pin(runtime.delete_sandbox_inner(
|
||||
target,
|
||||
preconditions,
|
||||
delete_guard,
|
||||
global_guard,
|
||||
))
|
||||
.await
|
||||
}
|
||||
.instrument(request_span),
|
||||
)
|
||||
@@ -2359,6 +2556,7 @@ impl ComputeRuntime {
|
||||
async fn delete_sandbox_inner(
|
||||
&self,
|
||||
target: SandboxDeleteTarget,
|
||||
preconditions: SandboxDeletePreconditions,
|
||||
delete_guard: SandboxLifecycleGuard,
|
||||
guard: tokio::sync::OwnedMutexGuard<()>,
|
||||
) -> Result<DeleteSandboxResult, Status> {
|
||||
@@ -2382,6 +2580,23 @@ impl ComputeRuntime {
|
||||
"sandbox name changed while the delete request was waiting; retry explicitly",
|
||||
));
|
||||
}
|
||||
if preconditions
|
||||
.expected_sandbox_id
|
||||
.as_deref()
|
||||
.is_some_and(|expected| expected != current.object_id())
|
||||
{
|
||||
return Err(Status::aborted(
|
||||
"sandbox identity changed before delete mutation",
|
||||
));
|
||||
}
|
||||
if preconditions
|
||||
.expected_resource_version
|
||||
.is_some_and(|expected| expected != sandbox_resource_version(¤t))
|
||||
{
|
||||
return Err(Status::aborted(
|
||||
"sandbox resource version changed before delete mutation",
|
||||
));
|
||||
}
|
||||
|
||||
// `Started` carries both sides of the CAS transition: the durable
|
||||
// `Deleting` row used to fence recovery, and the prior row used only
|
||||
@@ -7235,6 +7450,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};
|
||||
@@ -7592,7 +7808,9 @@ mod tests {
|
||||
workspace_rpcs_unimplemented: bool,
|
||||
omit_protocol_metadata: bool,
|
||||
requires_launch_authentication: bool,
|
||||
validate_create_calls: AtomicUsize,
|
||||
create_calls: AtomicUsize,
|
||||
created_sandboxes: TestMutex<Vec<DriverSandbox>>,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
@@ -7650,6 +7868,7 @@ mod tests {
|
||||
&self,
|
||||
_request: Request<ValidateSandboxCreateRequest>,
|
||||
) -> Result<tonic::Response<ValidateSandboxCreateResponse>, Status> {
|
||||
self.validate_create_calls.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(tonic::Response::new(ValidateSandboxCreateResponse {}))
|
||||
}
|
||||
|
||||
@@ -7699,9 +7918,15 @@ mod tests {
|
||||
|
||||
async fn create_sandbox(
|
||||
&self,
|
||||
_request: Request<CreateSandboxRequest>,
|
||||
request: Request<CreateSandboxRequest>,
|
||||
) -> Result<tonic::Response<CreateSandboxResponse>, Status> {
|
||||
self.create_calls.fetch_add(1, Ordering::SeqCst);
|
||||
if let Some(sandbox) = request.into_inner().sandbox {
|
||||
self.created_sandboxes
|
||||
.lock()
|
||||
.expect("created sandbox observation lock poisoned")
|
||||
.push(sandbox);
|
||||
}
|
||||
Ok(tonic::Response::new(CreateSandboxResponse::default()))
|
||||
}
|
||||
|
||||
@@ -8316,6 +8541,206 @@ 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::FailedPrecondition);
|
||||
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::FailedPrecondition);
|
||||
assert_eq!(
|
||||
driver.create_calls.load(Ordering::Relaxed),
|
||||
0,
|
||||
"unsupported UI policy must fail before provisioning"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn effective_ui_policy_rejects_before_unsupported_driver_validation() {
|
||||
let driver = Arc::new(TestDriver::default());
|
||||
let runtime = test_runtime(driver.clone()).await;
|
||||
let sandbox = sandbox_record(
|
||||
"sb-effective-ui",
|
||||
"effective-ui-policy",
|
||||
SandboxPhase::Provisioning,
|
||||
);
|
||||
let runtime_inputs = SandboxCreateRuntimeInputs::new(PublicSandboxPolicy {
|
||||
ui: Some(UiPolicy::default()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let error = runtime
|
||||
.validate_sandbox_create_with_runtime_inputs(&sandbox, &runtime_inputs)
|
||||
.await
|
||||
.expect_err("an unsupported driver must reject the effective UI policy");
|
||||
|
||||
assert_eq!(error.code(), Code::FailedPrecondition);
|
||||
assert_eq!(driver.validate_create_calls.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[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
|
||||
.negotiated_extension
|
||||
.supported_capabilities
|
||||
.push(openshell_core::extension_protocol::POLICY_UI_V1.to_string());
|
||||
|
||||
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 effective_ui_policy_rejects_before_unsupported_driver_create() {
|
||||
let driver = Arc::new(TestDriver::default());
|
||||
let runtime = test_runtime(driver.clone()).await;
|
||||
let sandbox = sandbox_record(
|
||||
"sb-effective-ui-create",
|
||||
"ui-policy",
|
||||
SandboxPhase::Provisioning,
|
||||
);
|
||||
let runtime_inputs = SandboxCreateRuntimeInputs::new(PublicSandboxPolicy {
|
||||
ui: Some(UiPolicy::default()),
|
||||
..Default::default()
|
||||
});
|
||||
let error = runtime
|
||||
.create_sandbox_with_runtime_inputs(sandbox, None, false, runtime_inputs)
|
||||
.await
|
||||
.expect_err("effective UI must be checked on internal creation");
|
||||
assert_eq!(error.code(), Code::FailedPrecondition);
|
||||
assert_eq!(driver.create_calls.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_runtime_inputs_reach_driver_without_persisting_effective_policy_or_secrets() {
|
||||
let driver = Arc::new(TestDriver::default());
|
||||
let runtime = test_runtime(driver.clone()).await;
|
||||
|
||||
let mut sandbox = sandbox_record(
|
||||
"sb-provider-inputs",
|
||||
"provider-inputs",
|
||||
SandboxPhase::Provisioning,
|
||||
);
|
||||
sandbox.spec = Some(SandboxSpec {
|
||||
policy: Some(PublicSandboxPolicy {
|
||||
version: 1,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
let mut runtime_inputs = SandboxCreateRuntimeInputs::new(PublicSandboxPolicy {
|
||||
version: 2,
|
||||
..Default::default()
|
||||
});
|
||||
let launch_authentication = b"protected-launch-material".to_vec();
|
||||
runtime_inputs.launch_authentication = Some(launch_authentication.clone());
|
||||
|
||||
runtime
|
||||
.create_sandbox_with_runtime_inputs(sandbox, None, false, runtime_inputs)
|
||||
.await
|
||||
.expect("create should succeed");
|
||||
|
||||
let observed_policy_version = {
|
||||
let created = driver
|
||||
.created_sandboxes
|
||||
.lock()
|
||||
.expect("created sandbox observation lock poisoned");
|
||||
assert_eq!(
|
||||
created[0].spec.as_ref().unwrap().launch_authentication,
|
||||
launch_authentication,
|
||||
"launch authentication must reach only the driver"
|
||||
);
|
||||
created[0]
|
||||
.spec
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.policy.as_ref())
|
||||
.map(|policy| policy.version)
|
||||
};
|
||||
assert_eq!(
|
||||
observed_policy_version,
|
||||
Some(2),
|
||||
"the driver must receive the effective policy"
|
||||
);
|
||||
|
||||
let persisted = runtime
|
||||
.store
|
||||
.get_message::<Sandbox>("sb-provider-inputs")
|
||||
.await
|
||||
.expect("persisted sandbox lookup should succeed")
|
||||
.expect("sandbox should be persisted");
|
||||
assert!(
|
||||
!persisted
|
||||
.encode_to_vec()
|
||||
.windows(launch_authentication.len())
|
||||
.any(|bytes| bytes == launch_authentication),
|
||||
"launch authentication must not enter the public sandbox record"
|
||||
);
|
||||
assert_eq!(
|
||||
persisted
|
||||
.spec
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.policy.as_ref())
|
||||
.map(|policy| policy.version),
|
||||
Some(1),
|
||||
"the public sandbox must retain its base policy"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_runtime_with_gateway_managed_lifecycle(
|
||||
driver: SharedComputeDriver,
|
||||
driver_name: &str,
|
||||
@@ -10278,7 +10703,7 @@ mod tests {
|
||||
r#type: "Ready".to_string(),
|
||||
status: "True".to_string(),
|
||||
reason: "AgentRunning".to_string(),
|
||||
message: "MXC workload is running".to_string(),
|
||||
message: "Workload is running".to_string(),
|
||||
transition_time: None,
|
||||
});
|
||||
|
||||
@@ -12465,6 +12890,133 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn identity_guarded_delete_rejects_a_different_sandbox_before_mutation() {
|
||||
let driver = ControlledDriver::new();
|
||||
let runtime = test_runtime(driver.clone()).await;
|
||||
let sandbox = sandbox_record("sb-current", "sandbox-a", SandboxPhase::Ready);
|
||||
runtime.store.put_message(&sandbox).await.unwrap();
|
||||
|
||||
let error = runtime
|
||||
.delete_sandbox_with_preconditions(
|
||||
"default",
|
||||
"sandbox-a",
|
||||
SandboxDeletePreconditions {
|
||||
expected_sandbox_id: Some("sb-stale".to_string()),
|
||||
expected_resource_version: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.code(), Code::Aborted);
|
||||
assert_eq!(driver.delete_calls(), 0);
|
||||
let current = runtime
|
||||
.store
|
||||
.get_message::<Sandbox>("sb-current")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
SandboxPhase::try_from(current.phase()).unwrap(),
|
||||
SandboxPhase::Ready
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn identity_guarded_delete_revalidates_resource_version_under_lifecycle_lock() {
|
||||
let driver = ControlledDriver::new();
|
||||
let runtime = test_runtime(driver.clone()).await;
|
||||
let sandbox = sandbox_record("sb-1", "sandbox-a", SandboxPhase::Ready);
|
||||
runtime.store.put_message(&sandbox).await.unwrap();
|
||||
let current = runtime
|
||||
.store
|
||||
.get_message::<Sandbox>(sandbox.object_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let expected_resource_version = sandbox_resource_version(¤t);
|
||||
|
||||
let delete_gate = runtime.lifecycle_gates.gate_for(sandbox.object_id());
|
||||
let delete_guard = delete_gate.lock().await;
|
||||
let delete_runtime = runtime.clone();
|
||||
let delete = tokio::spawn(async move {
|
||||
delete_runtime
|
||||
.delete_sandbox_with_preconditions(
|
||||
"default",
|
||||
"sandbox-a",
|
||||
SandboxDeletePreconditions {
|
||||
expected_sandbox_id: Some("sb-1".to_string()),
|
||||
expected_resource_version: Some(expected_resource_version),
|
||||
},
|
||||
)
|
||||
.await
|
||||
});
|
||||
tokio::time::timeout(Duration::from_secs(1), async {
|
||||
while Arc::strong_count(&delete_gate) < 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("guarded delete did not start waiting on the sandbox gate");
|
||||
|
||||
runtime
|
||||
.store
|
||||
.update_message_cas::<Sandbox, _>(
|
||||
sandbox.object_id(),
|
||||
expected_resource_version,
|
||||
|sandbox| sandbox.set_current_policy_version(9),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(delete_guard);
|
||||
|
||||
let error = delete.await.unwrap().unwrap_err();
|
||||
assert_eq!(error.code(), Code::Aborted);
|
||||
assert_eq!(driver.delete_calls(), 0);
|
||||
let current = runtime
|
||||
.store
|
||||
.get_message::<Sandbox>(sandbox.object_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(current.current_policy_version(), 9);
|
||||
assert_eq!(
|
||||
SandboxPhase::try_from(current.phase()).unwrap(),
|
||||
SandboxPhase::Ready
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn identity_guarded_delete_accepts_the_exact_current_identity() {
|
||||
let driver = ControlledDriver::new();
|
||||
let runtime = test_runtime(driver.clone()).await;
|
||||
let sandbox = sandbox_record("sb-1", "sandbox-a", SandboxPhase::Ready);
|
||||
runtime.store.put_message(&sandbox).await.unwrap();
|
||||
let current = runtime
|
||||
.store
|
||||
.get_message::<Sandbox>(sandbox.object_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let result = runtime
|
||||
.delete_sandbox_with_preconditions(
|
||||
"default",
|
||||
"sandbox-a",
|
||||
SandboxDeletePreconditions {
|
||||
expected_sandbox_id: Some(current.object_id().to_string()),
|
||||
expected_resource_version: Some(sandbox_resource_version(¤t)),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.acknowledged());
|
||||
assert_eq!(result.sandbox_id, "sb-1");
|
||||
assert_eq!(driver.delete_calls(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_cancellation_does_not_cancel_the_delete_worker() {
|
||||
let driver = ControlledDriver::new();
|
||||
|
||||
@@ -110,6 +110,10 @@ pub trait CredentialDriver: std::fmt::Debug + Send + Sync {
|
||||
pub struct ResolvedProviderCredentials {
|
||||
pub values: HashMap<String, String>,
|
||||
pub expires_at_ms: HashMap<String, i64>,
|
||||
/// Keys returned by a credential driver whose effective expiration has
|
||||
/// already passed. Values stay withheld, but create-time consumers need
|
||||
/// the identities to fail closed instead of silently omitting credentials.
|
||||
pub expired_keys: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -735,6 +739,7 @@ impl CredentialRuntime {
|
||||
effective_expires_at_ms,
|
||||
"skipping expired handle-backed credential"
|
||||
);
|
||||
resolved.expired_keys.insert(credential_key);
|
||||
continue;
|
||||
}
|
||||
resolved
|
||||
|
||||
@@ -1899,6 +1899,20 @@ async fn auto_approve_chunk(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Preserve UI as the sandbox's startup contract when applying a global policy.
|
||||
///
|
||||
/// UI controls are enforced by the compute runtime before the workload starts,
|
||||
/// so a later global override cannot safely add, remove, or change them. Global
|
||||
/// policy writes reject their own UI section; this overlay also prevents a
|
||||
/// global dynamic policy from hiding the UI state that the runtime enforced.
|
||||
fn preserve_sandbox_startup_ui(policy: &mut ProtoSandboxPolicy, sandbox: &Sandbox) {
|
||||
policy.ui = sandbox
|
||||
.spec
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.policy.as_ref())
|
||||
.and_then(|policy| policy.ui);
|
||||
}
|
||||
|
||||
// TODO: share effective-policy lookup with `load_sandbox_policy` /
|
||||
// `GetSandboxConfig`. They re-implement very similar global-settings and
|
||||
// profile-composition logic; consolidating them is out of scope for the
|
||||
@@ -1932,10 +1946,11 @@ async fn current_effective_policy_from_records(
|
||||
records: &[super::provider::ProviderEnvironmentRecord],
|
||||
) -> Result<ProtoSandboxPolicy, Status> {
|
||||
let global_settings = load_global_settings(state.store.as_ref()).await?;
|
||||
if let Some(global_policy) = decode_policy_from_global_settings(&global_settings)? {
|
||||
if let Some(mut global_policy) = decode_policy_from_global_settings(&global_settings)? {
|
||||
// A global policy is the complete effective policy. Dormant sandbox
|
||||
// history and specs may predate the current schema, but they must not
|
||||
// prevent the valid global policy from being served.
|
||||
preserve_sandbox_startup_ui(&mut global_policy, sandbox);
|
||||
return apply_captured_policy_context(
|
||||
provider_policy_context_from_records(catalog, records),
|
||||
global_policy,
|
||||
@@ -2792,9 +2807,10 @@ pub(super) async fn load_sandbox_config(
|
||||
.await
|
||||
.map_err(|e| Status::internal(format!("fetch policy history failed: {e}")))?;
|
||||
|
||||
let (mut policy, version, mut policy_hash, policy_source) = if let Some(global_policy) =
|
||||
let (mut policy, version, mut policy_hash, policy_source) = if let Some(mut global_policy) =
|
||||
global_policy
|
||||
{
|
||||
preserve_sandbox_startup_ui(&mut global_policy, sandbox);
|
||||
let version = latest
|
||||
.as_ref()
|
||||
.map(|record| u32::try_from(record.version).unwrap_or(0))
|
||||
@@ -3501,21 +3517,61 @@ pub(super) async fn handle_get_gateway_config(
|
||||
}))
|
||||
}
|
||||
|
||||
/// Resolve the effective create-time policy without exporting credential state.
|
||||
/// Provider refresh and secret delivery remain in the supervisor session.
|
||||
pub(super) async fn resolve_sandbox_create_runtime_inputs(
|
||||
state: &ServerState,
|
||||
sandbox: &Sandbox,
|
||||
) -> Result<crate::compute::SandboxCreateRuntimeInputs, Status> {
|
||||
let sandbox_id = sandbox.object_id();
|
||||
let workspace = sandbox.object_workspace();
|
||||
let provider_names = sandbox
|
||||
.spec
|
||||
.as_ref()
|
||||
.map(|spec| spec.providers.as_slice())
|
||||
.unwrap_or_default();
|
||||
let provider_profile_catalog = state
|
||||
.provider_profile_sources
|
||||
.snapshot_catalog(state.store.as_ref(), workspace)
|
||||
.await?;
|
||||
let provider_records = super::provider::load_provider_environment_records(
|
||||
state.store.as_ref(),
|
||||
workspace,
|
||||
provider_names,
|
||||
)
|
||||
.await?;
|
||||
let effective_policy = current_effective_policy_from_records(
|
||||
state,
|
||||
&provider_profile_catalog,
|
||||
sandbox,
|
||||
sandbox_id,
|
||||
&provider_records,
|
||||
)
|
||||
.await?;
|
||||
let policy_credential_bindings =
|
||||
policy_static_credential_endpoint_bindings(Some(&effective_policy))?;
|
||||
validate_policy_credential_binding_context(
|
||||
&provider_profile_catalog,
|
||||
&provider_records,
|
||||
&effective_policy,
|
||||
&policy_credential_bindings,
|
||||
)?;
|
||||
Ok(crate::compute::SandboxCreateRuntimeInputs::new(
|
||||
effective_policy,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_get_sandbox_provider_environment(
|
||||
state: &Arc<ServerState>,
|
||||
request: Request<GetSandboxProviderEnvironmentRequest>,
|
||||
) -> Result<Response<GetSandboxProviderEnvironmentResponse>, Status> {
|
||||
let sandbox_id = request.get_ref().sandbox_id.clone();
|
||||
let supports_static_credential_bindings = request.get_ref().supports_static_credential_bindings;
|
||||
crate::auth::guard::enforce_sandbox_scope(&request, &sandbox_id)?;
|
||||
let principal = crate::auth::guard::enforce_sandbox_scope(&request, &sandbox_id)?;
|
||||
drop(request);
|
||||
|
||||
let sandbox = state
|
||||
.store
|
||||
.get_message::<Sandbox>(&sandbox_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))?
|
||||
.ok_or_else(|| Status::not_found("sandbox not found"))?;
|
||||
let sandbox =
|
||||
super::sandbox::fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?;
|
||||
let environment =
|
||||
load_sandbox_provider_environment(state, &sandbox, supports_static_credential_bindings)
|
||||
.await?;
|
||||
@@ -3775,6 +3831,11 @@ async fn handle_update_config_inner(
|
||||
clear_provider_credentialed_markers(&mut new_policy);
|
||||
validate_no_reserved_provider_policy_keys(&new_policy)?;
|
||||
new_policy = validate_and_canonicalize_policy(new_policy)?;
|
||||
if new_policy.ui.is_some() {
|
||||
return Err(Status::invalid_argument(
|
||||
"UI policy cannot be set globally because it is applied at sandbox startup; configure ui in each sandbox policy",
|
||||
));
|
||||
}
|
||||
validate_policy_safety(&new_policy)?;
|
||||
crate::middleware::validate_policy(state.middleware_registry.as_ref(), &new_policy)
|
||||
.await?;
|
||||
@@ -7006,17 +7067,17 @@ async fn sandbox_policy_merge_validation_data_with_catalog(
|
||||
) -> Result<SandboxPolicyMergeValidationData, Status> {
|
||||
let global_settings = load_global_settings(state.store.as_ref()).await?;
|
||||
let composition_enabled = provider_policy_composition_enabled_in(&global_settings)?;
|
||||
let ProviderPolicyContext {
|
||||
layers,
|
||||
credentialed_scopes,
|
||||
endpointless_provider_names,
|
||||
} = provider_policy_context_with_catalog(
|
||||
let records = super::provider::load_provider_environment_records(
|
||||
state.store.as_ref(),
|
||||
catalog,
|
||||
workspace,
|
||||
provider_names,
|
||||
)
|
||||
.await?;
|
||||
let ProviderPolicyContext {
|
||||
layers,
|
||||
credentialed_scopes,
|
||||
endpointless_provider_names,
|
||||
} = provider_policy_context_from_records(catalog, &records);
|
||||
let provider_layers = if composition_enabled {
|
||||
layers
|
||||
} else {
|
||||
@@ -7027,12 +7088,6 @@ async fn sandbox_policy_merge_validation_data_with_catalog(
|
||||
provider_layer_count = provider_layers.len(),
|
||||
"Composed provider policy and credential context for merge validation"
|
||||
);
|
||||
let records = super::provider::load_provider_environment_records(
|
||||
state.store.as_ref(),
|
||||
workspace,
|
||||
provider_names,
|
||||
)
|
||||
.await?;
|
||||
Ok(SandboxPolicyMergeValidationData {
|
||||
provider_layers,
|
||||
catalog: catalog.clone(),
|
||||
@@ -8476,6 +8531,32 @@ mod tests {
|
||||
.expect("test global policy must be present")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_runtime_inputs_preserve_base_policy_without_credential_sink() {
|
||||
let state = test_server_state().await;
|
||||
let policy = openshell_policy::restrictive_default_policy();
|
||||
let sandbox = test_sandbox("create-policy", "create-policy", policy.clone(), Vec::new());
|
||||
let inputs = resolve_sandbox_create_runtime_inputs(state.as_ref(), &sandbox)
|
||||
.await
|
||||
.expect("driver-independent effective policy");
|
||||
assert_eq!(inputs.effective_policy, Some(policy));
|
||||
assert!(inputs.launch_authentication.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_runtime_inputs_apply_global_policy_and_preserve_startup_ui() {
|
||||
let state = test_server_state().await;
|
||||
let mut global_policy = install_test_global_policy(&state).await;
|
||||
let mut policy = openshell_policy::restrictive_default_policy();
|
||||
policy.ui = Some(openshell_core::proto::UiPolicy::default());
|
||||
global_policy.ui = policy.ui;
|
||||
let sandbox = test_sandbox("create-global", "create-global", policy, Vec::new());
|
||||
let inputs = resolve_sandbox_create_runtime_inputs(state.as_ref(), &sandbox)
|
||||
.await
|
||||
.expect("global effective policy");
|
||||
assert_eq!(inputs.effective_policy, Some(global_policy));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_sandbox_policies_traverses_multiple_pages_exactly_once() {
|
||||
let state = test_server_state().await;
|
||||
@@ -9071,6 +9152,67 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn global_policy_preserves_each_sandbox_startup_ui_contract() {
|
||||
use openshell_core::proto::{UiClipboardAccess, UiPolicy};
|
||||
|
||||
let state = test_server_state().await;
|
||||
let global_policy = install_test_global_policy(&state).await;
|
||||
assert!(global_policy.ui.is_none());
|
||||
|
||||
let sandbox_id = "global-preserves-startup-ui";
|
||||
let startup_ui = UiPolicy {
|
||||
allow_graphical_ui: true,
|
||||
clipboard: UiClipboardAccess::Read as i32,
|
||||
allow_input_injection: false,
|
||||
};
|
||||
let mut sandbox_policy = openshell_policy::restrictive_default_policy();
|
||||
sandbox_policy.ui = Some(startup_ui);
|
||||
let sandbox = test_sandbox(sandbox_id, sandbox_id, sandbox_policy, Vec::new());
|
||||
state
|
||||
.store
|
||||
.put_message(&sandbox)
|
||||
.await
|
||||
.expect("store sandbox");
|
||||
|
||||
let response = handle_get_sandbox_config(
|
||||
&state,
|
||||
with_sandbox(
|
||||
Request::new(GetSandboxConfigRequest {
|
||||
name: sandbox_id.to_string(),
|
||||
workspace_scope: Some(openshell_core::proto::workspace_selector("default")),
|
||||
}),
|
||||
sandbox_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("global policy read must preserve startup UI")
|
||||
.into_inner();
|
||||
assert_eq!(
|
||||
response
|
||||
.policy
|
||||
.as_ref()
|
||||
.and_then(|policy| policy.ui.as_ref()),
|
||||
Some(&startup_ui)
|
||||
);
|
||||
|
||||
let catalog = state
|
||||
.provider_profile_sources
|
||||
.snapshot_catalog(state.store.as_ref(), "default")
|
||||
.await
|
||||
.expect("provider profile catalog");
|
||||
let effective = current_effective_policy_for_sandbox(
|
||||
state.as_ref(),
|
||||
&catalog,
|
||||
"default",
|
||||
&sandbox,
|
||||
sandbox_id,
|
||||
)
|
||||
.await
|
||||
.expect("effective policy lookup must preserve startup UI");
|
||||
assert_eq!(effective.ui.as_ref(), Some(&startup_ui));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn canonical_mcp_version_order_produces_identical_policy_bytes_and_hashes() {
|
||||
let state = test_server_state().await;
|
||||
@@ -9168,6 +9310,93 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn global_policy_ingress_rejects_startup_only_ui_before_persistence() {
|
||||
use openshell_core::proto::UiPolicy;
|
||||
|
||||
let state = test_server_state().await;
|
||||
let mut policy = openshell_policy::restrictive_default_policy();
|
||||
policy.ui = Some(UiPolicy::default());
|
||||
|
||||
let error = handle_update_config(
|
||||
&state,
|
||||
with_user(Request::new(UpdateConfigRequest {
|
||||
global: true,
|
||||
policy: Some(policy),
|
||||
..Default::default()
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.expect_err("global UI must be rejected before persistence");
|
||||
|
||||
assert_eq!(error.code(), Code::InvalidArgument);
|
||||
assert!(error.message().contains("sandbox startup"));
|
||||
assert!(
|
||||
state
|
||||
.store
|
||||
.get_latest_policy(GLOBAL_POLICY_SANDBOX_ID)
|
||||
.await
|
||||
.expect("global policy lookup")
|
||||
.is_none()
|
||||
);
|
||||
let settings = load_global_settings(state.store.as_ref())
|
||||
.await
|
||||
.expect("global settings lookup");
|
||||
assert!(!settings.settings.contains_key(POLICY_SETTING_KEY));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sandbox_policy_update_accepts_semantically_unchanged_ui_default() {
|
||||
use openshell_core::proto::{UiClipboardAccess, UiPolicy};
|
||||
|
||||
let state = test_server_state().await;
|
||||
let sandbox_id = "ui-default-roundtrip";
|
||||
let mut canonical = openshell_policy::restrictive_default_policy();
|
||||
canonical.ui = Some(UiPolicy {
|
||||
allow_graphical_ui: true,
|
||||
clipboard: UiClipboardAccess::None as i32,
|
||||
..Default::default()
|
||||
});
|
||||
state
|
||||
.store
|
||||
.put_message(&test_sandbox(
|
||||
sandbox_id,
|
||||
sandbox_id,
|
||||
canonical.clone(),
|
||||
Vec::new(),
|
||||
))
|
||||
.await
|
||||
.expect("store sandbox");
|
||||
|
||||
let mut protobuf_default = canonical;
|
||||
protobuf_default.ui.as_mut().expect("UI policy").clipboard =
|
||||
UiClipboardAccess::Unspecified as i32;
|
||||
handle_update_config(
|
||||
&state,
|
||||
with_user(Request::new(UpdateConfigRequest {
|
||||
sandbox: sandbox_id.to_string(),
|
||||
workspace_scope: Some(openshell_core::proto::workspace_selector("default")),
|
||||
policy: Some(protobuf_default),
|
||||
..Default::default()
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.expect("unspecified and none are the same static UI policy");
|
||||
|
||||
let stored = state
|
||||
.store
|
||||
.get_latest_policy(sandbox_id)
|
||||
.await
|
||||
.expect("policy history lookup")
|
||||
.expect("policy revision");
|
||||
let persisted = ProtoSandboxPolicy::decode(stored.policy_payload.as_slice())
|
||||
.expect("decode persisted policy");
|
||||
assert_eq!(
|
||||
persisted.ui.expect("persisted UI").clipboard,
|
||||
UiClipboardAccess::None as i32
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn policy_record_identity_global_deduplicates_defaulted_mcp_history() {
|
||||
for (case, legacy_policy) in defaulted_mcp_policy_cases() {
|
||||
@@ -21167,6 +21396,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};
|
||||
@@ -23625,6 +23883,22 @@ mod tests {
|
||||
"handle_get_sandbox_config must return NotFound, not PermissionDenied"
|
||||
);
|
||||
|
||||
// --- handle_get_sandbox_provider_environment ---
|
||||
let err = handle_get_sandbox_provider_environment(
|
||||
&state,
|
||||
non_member_request(GetSandboxProviderEnvironmentRequest {
|
||||
sandbox_id: "sandbox-other".into(),
|
||||
supports_static_credential_bindings: true,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err.code(),
|
||||
Code::NotFound,
|
||||
"handle_get_sandbox_provider_environment must hide cross-workspace sandboxes"
|
||||
);
|
||||
|
||||
// --- handle_get_sandbox_logs ---
|
||||
let err = handle_get_sandbox_logs(
|
||||
&state,
|
||||
|
||||
@@ -75,6 +75,11 @@ pub(super) struct ProviderEnvironment {
|
||||
pub static_credential_bindings: HashMap<String, StaticCredentialBinding>,
|
||||
pub static_credential_keys: HashSet<String>,
|
||||
pub files: HashMap<String, String>,
|
||||
/// Static credential keys withheld because they were already expired at
|
||||
/// resolution time. Excluded from `environment`/`static_credential_keys`
|
||||
/// like any other withheld key, but tracked separately from keys that
|
||||
/// never had an injectable credential.
|
||||
pub expired_static_keys: HashSet<String>,
|
||||
}
|
||||
|
||||
/// Immutable provider records used to build one provider-environment response.
|
||||
@@ -1131,6 +1136,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin
|
||||
let mut files = HashMap::new();
|
||||
let mut file_env_keys = HashSet::new();
|
||||
let mut readiness_reason = openshell_core::proto::ProviderReadinessReason::Unspecified;
|
||||
let mut expired_static_keys = HashSet::new();
|
||||
let now_ms = crate::persistence::current_time_ms();
|
||||
validate_provider_environment_records_unique_at(store, catalog, records, now_ms).await?;
|
||||
let registry = openshell_providers::ProviderRegistry::new();
|
||||
@@ -1257,6 +1263,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin
|
||||
);
|
||||
readiness_reason =
|
||||
openshell_core::proto::ProviderReadinessReason::CredentialExpired;
|
||||
expired_static_keys.insert(key.clone());
|
||||
continue;
|
||||
}
|
||||
expires.entry(key.clone()).or_insert(expires_at_ms);
|
||||
@@ -1367,6 +1374,22 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin
|
||||
}
|
||||
}
|
||||
|
||||
// The credential runtime withholds expired handle-backed values. Keep
|
||||
// the identity of otherwise injectable keys in the resolver result.
|
||||
for key in resolved_refs.expired_keys {
|
||||
if accepted_stored_credential_keys
|
||||
.as_ref()
|
||||
.is_some_and(|accepted| !accepted.contains(&key))
|
||||
|| is_non_injectable_provider_credential(provider, &key)
|
||||
|| broker_only_credential_keys.contains(&key)
|
||||
|| has_no_usable_endpoint
|
||||
|| !is_valid_env_key(&key)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
expired_static_keys.insert(key);
|
||||
}
|
||||
|
||||
// Build each provider's emitted environment independently so another
|
||||
// provider's earlier output cannot change how this provider classifies
|
||||
// or populates its own keys. Cross-provider credential/config
|
||||
@@ -1433,6 +1456,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin
|
||||
static_credential_bindings,
|
||||
static_credential_keys,
|
||||
files,
|
||||
expired_static_keys,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11858,6 +11882,49 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_provider_env_preserves_expired_handle_key_for_create_time_rejection() {
|
||||
let store = test_store().await;
|
||||
let config = openshell_core::Config::new(None).with_credential_drivers(["test-static"]);
|
||||
let credentials = crate::credentials::CredentialRuntime::from_config(&config).unwrap();
|
||||
let catalog = ProviderProfileSources::with_default_sources()
|
||||
.snapshot_catalog(&store, "default")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut provider = provider_with_credential_value(
|
||||
"github-expired",
|
||||
"github",
|
||||
"GITHUB_TOKEN",
|
||||
"github-token",
|
||||
);
|
||||
provider.credential_expiration_times.insert(
|
||||
"GITHUB_TOKEN".to_string(),
|
||||
ts(crate::persistence::current_time_ms() - 1),
|
||||
);
|
||||
create_provider_record_validating(
|
||||
&store,
|
||||
"default",
|
||||
&catalog,
|
||||
provider,
|
||||
Some(&credentials),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = resolve_provider_environment_with_credentials(
|
||||
&store,
|
||||
&catalog,
|
||||
"default",
|
||||
&["github-expired".to_string()],
|
||||
&credentials,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.contains_key("GITHUB_TOKEN"));
|
||||
assert!(result.expired_static_keys.contains("GITHUB_TOKEN"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_provider_env_skips_expired_credentials_and_returns_expiry_metadata() {
|
||||
let store = test_store().await;
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::auth::workspace_authz::{
|
||||
AuthorizedWorkspaceScope, MinWorkspaceRole, authorize_list_workspace_selector,
|
||||
authorize_sandbox_workspace, authorize_workspace,
|
||||
};
|
||||
use crate::compute::SandboxDeletePreconditions;
|
||||
use crate::pagination::Pagination;
|
||||
use crate::persistence::{
|
||||
ObjectLabels, ObjectListQuery, ObjectType, WriteCondition, generate_name,
|
||||
@@ -158,6 +159,37 @@ impl Drop for WatchSandboxStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a runtime sandbox ID and authorize its persisted workspace without
|
||||
/// revealing whether an inaccessible object exists.
|
||||
pub(super) async fn fetch_and_authorize_sandbox(
|
||||
state: &ServerState,
|
||||
principal: &crate::auth::principal::Principal,
|
||||
sandbox_id: &str,
|
||||
) -> Result<Sandbox, Status> {
|
||||
let sandbox = state
|
||||
.store
|
||||
.get_message::<Sandbox>(sandbox_id)
|
||||
.await
|
||||
.map_err(|error| Status::internal(format!("fetch sandbox failed: {error}")))?
|
||||
.ok_or_else(|| Status::not_found("sandbox not found"))?;
|
||||
authorize_sandbox_workspace(
|
||||
&state.store,
|
||||
&state.admin_role,
|
||||
principal,
|
||||
sandbox.object_workspace(),
|
||||
MinWorkspaceRole::User,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
if error.code() == tonic::Code::PermissionDenied {
|
||||
Status::not_found("sandbox not found")
|
||||
} else {
|
||||
error
|
||||
}
|
||||
})?;
|
||||
Ok(sandbox)
|
||||
}
|
||||
|
||||
/// Resolve a public sandbox name and authorize its persisted workspace.
|
||||
/// Missing and unauthorized objects deliberately share one response so names
|
||||
/// cannot be used as an existence oracle.
|
||||
@@ -634,12 +666,15 @@ async fn handle_create_sandbox_inner(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut runtime_inputs =
|
||||
super::policy::resolve_sandbox_create_runtime_inputs(state.as_ref(), &sandbox).await?;
|
||||
|
||||
state
|
||||
.compute
|
||||
.validate_launch_signer_configured(state.sandbox_session_jwt_authority.is_some())?;
|
||||
state
|
||||
.compute
|
||||
.validate_sandbox_create(&sandbox)
|
||||
.validate_sandbox_create_with_runtime_inputs(&sandbox, &runtime_inputs)
|
||||
.await
|
||||
.map_err(|status| {
|
||||
warn!(error = %status, "Rejecting sandbox create request");
|
||||
@@ -669,12 +704,13 @@ async fn handle_create_sandbox_inner(
|
||||
.map_err(|error| Status::internal(format!("encode launch authentication: {error}")))
|
||||
})
|
||||
.transpose()?;
|
||||
runtime_inputs.launch_authentication = launch_authentication;
|
||||
|
||||
let sandbox = Box::pin(state.compute.create_sandbox_authenticated_with_guards(
|
||||
let sandbox = Box::pin(state.compute.create_sandbox_with_runtime_inputs_and_guards(
|
||||
sandbox,
|
||||
sandbox_token,
|
||||
launch_authentication,
|
||||
await_main_process_attachment,
|
||||
runtime_inputs,
|
||||
sandbox_lifecycle_guard,
|
||||
sandbox_sync_guard,
|
||||
))
|
||||
@@ -1642,10 +1678,18 @@ async fn handle_delete_sandbox_inner(
|
||||
let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace)
|
||||
.await?
|
||||
.name;
|
||||
|
||||
// Main's canonical public delete request contains no caller-provided
|
||||
// identity/version fields. The compute layer still binds the resolved
|
||||
// object ID and lifecycle guards before committing deletion.
|
||||
let preconditions = SandboxDeletePreconditions::default();
|
||||
let result = state
|
||||
.compute
|
||||
.delete_sandbox_allow_missing(&workspace, &name, req.allow_missing)
|
||||
.delete_sandbox_allow_missing_with_preconditions(
|
||||
&workspace,
|
||||
&name,
|
||||
req.allow_missing,
|
||||
preconditions,
|
||||
)
|
||||
.await?;
|
||||
if !result.sandbox_id.is_empty() {
|
||||
state.telemetry.end_sandbox_session(&result.sandbox_id);
|
||||
@@ -2600,8 +2644,8 @@ pub(super) async fn handle_forward_tcp(
|
||||
.await
|
||||
.map_err(|e| Status::unavailable(format!("supervisor relay failed: {e}")))?;
|
||||
|
||||
let sandbox_id = sandbox.object_id().to_string();
|
||||
let (tx, rx) = mpsc::channel::<Result<TcpForwardFrame, Status>>(256);
|
||||
let sandbox_id = sandbox.object_id().to_string();
|
||||
tokio::spawn(async move {
|
||||
let _connection_guard = connection_guard;
|
||||
let Some(relay_stream) =
|
||||
@@ -2784,13 +2828,15 @@ fn validate_tcp_target_parts(host: &str, _port: u32) -> Result<String, Status> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn bridge_forward_tcp_stream(
|
||||
async fn bridge_forward_tcp_stream<S>(
|
||||
mut inbound: tonic::Streaming<TcpForwardFrame>,
|
||||
relay_stream: tokio::io::DuplexStream,
|
||||
relay_stream: S,
|
||||
tx: mpsc::Sender<Result<TcpForwardFrame, Status>>,
|
||||
sandbox_id: &str,
|
||||
channel_id: &str,
|
||||
) {
|
||||
) where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + 'static,
|
||||
{
|
||||
let (mut relay_read, mut relay_write) = tokio::io::split(relay_stream);
|
||||
|
||||
let sandbox_id_in = sandbox_id.to_string();
|
||||
@@ -5622,6 +5668,68 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn internal_delete_rejects_expected_identity_drift_before_mutation() {
|
||||
let state = test_server_state().await;
|
||||
let mut sandbox = test_sandbox("guarded-delete", Vec::new());
|
||||
sandbox.metadata.as_mut().unwrap().id = "sb-current".to_string();
|
||||
state.store.put_message(&sandbox).await.unwrap();
|
||||
|
||||
let error = state
|
||||
.compute
|
||||
.delete_sandbox_with_preconditions(
|
||||
"default",
|
||||
"guarded-delete",
|
||||
SandboxDeletePreconditions {
|
||||
expected_sandbox_id: Some("sb-stale".to_string()),
|
||||
expected_resource_version: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.code(), tonic::Code::Aborted);
|
||||
assert!(
|
||||
state
|
||||
.store
|
||||
.get_message::<Sandbox>("sb-current")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn internal_delete_rejects_resource_version_without_immutable_identity() {
|
||||
let state = test_server_state().await;
|
||||
let mut sandbox = test_sandbox("guarded-delete", Vec::new());
|
||||
sandbox.metadata.as_mut().unwrap().id = "sb-current".to_string();
|
||||
state.store.put_message(&sandbox).await.unwrap();
|
||||
|
||||
let error = state
|
||||
.compute
|
||||
.delete_sandbox_with_preconditions(
|
||||
"default",
|
||||
"guarded-delete",
|
||||
SandboxDeletePreconditions {
|
||||
expected_sandbox_id: None,
|
||||
expected_resource_version: Some(17),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.code(), tonic::Code::InvalidArgument);
|
||||
assert!(
|
||||
state
|
||||
.store
|
||||
.get_message::<Sandbox>("sb-current")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn attach_sandbox_provider_persists_current_provider_list() {
|
||||
let state = test_server_state().await;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
use openshell_core::proto::{
|
||||
CredentialHandle, ExecSandboxRequest, Provider, SandboxPolicy as ProtoSandboxPolicy,
|
||||
SandboxRestartPolicy, SandboxSpec, SandboxTemplate,
|
||||
SandboxRestartPolicy, SandboxSpec, SandboxTemplate, UiClipboardAccess,
|
||||
};
|
||||
use openshell_core::rpc_error::invalid_argument;
|
||||
use prost::Message;
|
||||
@@ -1030,7 +1030,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,
|
||||
@@ -1054,6 +1054,18 @@ pub(super) fn validate_static_fields_unchanged(
|
||||
"process policy cannot be changed on a live sandbox (applied at startup)",
|
||||
));
|
||||
}
|
||||
let mut baseline_ui = baseline.ui;
|
||||
let mut new_ui = new.ui;
|
||||
for ui in [&mut baseline_ui, &mut new_ui].into_iter().flatten() {
|
||||
if ui.clipboard == UiClipboardAccess::Unspecified as i32 {
|
||||
ui.clipboard = UiClipboardAccess::None as i32;
|
||||
}
|
||||
}
|
||||
if baseline_ui != new_ui {
|
||||
return Err(Status::invalid_argument(
|
||||
"UI policy cannot be changed on a live sandbox (applied at startup)",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2401,6 +2413,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]
|
||||
|
||||
@@ -1208,6 +1208,19 @@ pub enum ComputeDriverInstance {
|
||||
/// Factory for a compute driver linked into a gateway binary.
|
||||
#[async_trait::async_trait]
|
||||
pub trait ComputeDriverFactory: Send + Sync {
|
||||
/// Resolve the driver's external-resource admission contract. Defaults to
|
||||
/// the operator-configured policy, including secure admission defaults.
|
||||
/// Drivers without resource-label support may override this explicitly.
|
||||
fn admission_policy(
|
||||
&self,
|
||||
context: ComputeDriverConfigContext<'_>,
|
||||
) -> Result<openshell_core::resource_admission::DriverAdmissionConfig> {
|
||||
compute::driver_config::admission_config_from_context(
|
||||
context.driver_startup,
|
||||
context.driver_name,
|
||||
)
|
||||
}
|
||||
|
||||
/// Validate selected-driver configuration without starting a driver,
|
||||
/// connecting a transport, or modifying runtime state.
|
||||
///
|
||||
@@ -1609,8 +1622,20 @@ async fn build_compute_runtime(
|
||||
false,
|
||||
)?;
|
||||
let telemetry_compute_driver = driver.telemetry_compute_driver(registry);
|
||||
let admission =
|
||||
compute::driver_config::admission_config_from_context(driver_startup, driver.name())?;
|
||||
let admission = match &driver {
|
||||
ConfiguredComputeDriver::Registered(registration) => registration
|
||||
.factory
|
||||
.admission_policy(ComputeDriverConfigContext {
|
||||
driver_name: ®istration.name,
|
||||
gateway_name: &config.name,
|
||||
gateway_bind_address: config.bind_address,
|
||||
gateway_log_level: &config.log_level,
|
||||
driver_startup,
|
||||
})?,
|
||||
ConfiguredComputeDriver::Remote { name } => {
|
||||
compute::driver_config::admission_config_from_context(driver_startup, name)?
|
||||
}
|
||||
};
|
||||
info!(driver = %driver.name(), "Using compute driver");
|
||||
let runtime = match driver {
|
||||
ConfiguredComputeDriver::Registered(registration) => {
|
||||
@@ -2078,6 +2103,29 @@ mod tests {
|
||||
#[derive(Clone, Copy)]
|
||||
struct TestComputeDriverFactory;
|
||||
|
||||
#[test]
|
||||
fn factory_admission_defaults_remain_enabled() {
|
||||
use super::ComputeDriverFactory as _;
|
||||
let endpoint_overrides = std::collections::BTreeMap::new();
|
||||
let context = super::ComputeDriverConfigContext {
|
||||
driver_name: "custom",
|
||||
gateway_name: "test",
|
||||
gateway_bind_address: "127.0.0.1:8080".parse().unwrap(),
|
||||
gateway_log_level: "info",
|
||||
driver_startup: crate::compute::driver_config::DriverStartupContext {
|
||||
file: None,
|
||||
guest_tls: None,
|
||||
gateway_port: 8080,
|
||||
gateway_tls_enabled: false,
|
||||
endpoint_overrides: &endpoint_overrides,
|
||||
},
|
||||
};
|
||||
let policy = TestComputeDriverFactory.admission_policy(context).unwrap();
|
||||
assert!(policy.resource_admission.enabled);
|
||||
assert_eq!(policy.resource_admission.required_labels.len(), 2);
|
||||
assert!(!policy.allow_driver_config);
|
||||
}
|
||||
|
||||
// Omitting validate_config exercises source compatibility for out-of-tree
|
||||
// factories written before package preflight introduced that hook.
|
||||
#[async_trait::async_trait]
|
||||
|
||||
@@ -135,11 +135,11 @@ mod tests {
|
||||
// SandboxProvisioning in both closures. Old rows decode false and empty;
|
||||
// decoding or deadline updates cannot claim an existing attempt.
|
||||
const PUBLIC_RPC_SCHEMA_SHA256: &str =
|
||||
"18206c52e68fdb0af60f8bb8dfaf47d9bc8021222cb49cacffab6352d3ad5549";
|
||||
"3066eb8ed5a0b43381196e5c19716a0e5a284996c0dfe0122750a6e7366beb5d";
|
||||
const DURABLE_SCHEMA_SHA256: &str =
|
||||
"76487ab369fc3a4b03a179bb5e7ea6be8d20e380ad5563075dff8ee50e539406";
|
||||
"ebb9153e436a6c042a221ec3daa7bb0d21ad81e31ce841ee1b5144404156802e";
|
||||
const PUBLIC_DURABLE_OVERLAP_SHA256: &str =
|
||||
"761dea31a521b0650840fe2a823ad6e36a265ed323ba4506889781d630df0ee3";
|
||||
"5f5165548d2ec333689fc88ab652af1977760bb976007a634e3ed30f28782424";
|
||||
// A persisted Sandbox without endpoint status retains its lifecycle fields;
|
||||
// the absent repeated field decodes empty and needs no database rewrite.
|
||||
const SANDBOX_WITHOUT_ENDPOINT_STATUS: &str = "0a1e0a0a73616e64626f782d6964120773616e64626f783a0764656661756c741a2b0a0773616e64626f782a0d0a05526561647912045472756530023807420d73757065727669736f722d6964";
|
||||
@@ -604,9 +604,9 @@ mod tests {
|
||||
overlap_hash.as_str(),
|
||||
),
|
||||
(
|
||||
(306, 27),
|
||||
(93, 21),
|
||||
(81, 21),
|
||||
(307, 28),
|
||||
(94, 22),
|
||||
(82, 22),
|
||||
PUBLIC_RPC_SCHEMA_SHA256,
|
||||
DURABLE_SCHEMA_SHA256,
|
||||
PUBLIC_DURABLE_OVERLAP_SHA256
|
||||
@@ -735,6 +735,7 @@ mod tests {
|
||||
PolicyRevisionPayload::decode(legacy_bytes(V0_0_116_POLICY_PAYLOAD).as_slice())
|
||||
.expect("legacy policy payload must decode");
|
||||
assert!(policy_payload.policy.is_some());
|
||||
assert!(policy_payload.policy.as_ref().unwrap().ui.is_none());
|
||||
assert_eq!(policy_payload.hash, "sha256");
|
||||
assert_eq!(policy_payload.load_error, "none");
|
||||
assert_eq!(policy_payload.loaded_at_ms, 300);
|
||||
|
||||
@@ -93,7 +93,7 @@ pub fn install(
|
||||
)
|
||||
},
|
||||
);
|
||||
let (jsonl_layer, jsonl_dir) = build_ocsf_jsonl_layer(gateway.compute_driver());
|
||||
let (jsonl_layer, jsonl_dir) = build_ocsf_jsonl_layer();
|
||||
|
||||
// Keep the audit sink independent from the operator's diagnostic log
|
||||
// level. An explicit JSONL opt-in must keep every OCSF event even when the
|
||||
@@ -161,8 +161,7 @@ pub fn install(
|
||||
|
||||
/// Build the OCSF JSONL audit layer for the gateway, plus the directory it
|
||||
/// writes into (for a one-line startup log). Returns `(None, None)` when
|
||||
/// the target is not Windows, the selected compute driver is not MXC, the sink
|
||||
/// was not explicitly enabled through `OPENSHELL_OCSF_JSON`, or the target
|
||||
/// the sink was not explicitly enabled through `OPENSHELL_OCSF_JSON`, or the target
|
||||
/// directory/appender cannot be opened.
|
||||
///
|
||||
/// The appender is *synchronous* (not wrapped in `tracing_appender::non_blocking`)
|
||||
@@ -171,31 +170,26 @@ pub fn install(
|
||||
/// its non-blocking guard on graceful shutdown), the gateway's ETW capture path
|
||||
/// can be force-killed by the harness, and we do not want to lose the tail of the
|
||||
/// audit trail.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn build_ocsf_jsonl_layer(
|
||||
_compute_driver: Option<&str>,
|
||||
) -> (
|
||||
Option<OcsfJsonlLayer<tracing_appender::rolling::RollingFileAppender>>,
|
||||
Option<std::path::PathBuf>,
|
||||
) {
|
||||
// The gateway-local JSONL sink belongs to the Windows/MXC ETW path. A
|
||||
// cross-platform sink needs an explicit storage and configuration contract.
|
||||
(None, None)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn build_ocsf_jsonl_layer(
|
||||
compute_driver: Option<&str>,
|
||||
) -> (
|
||||
fn build_ocsf_jsonl_layer() -> (
|
||||
Option<OcsfJsonlLayer<tracing_appender::rolling::RollingFileAppender>>,
|
||||
Option<std::path::PathBuf>,
|
||||
) {
|
||||
let requested = std::env::var("OPENSHELL_OCSF_JSON").ok();
|
||||
if !mxc_ocsf_jsonl_requested(compute_driver, requested.as_deref()) {
|
||||
if !ocsf_jsonl_requested(requested.as_deref()) {
|
||||
return (None, None);
|
||||
}
|
||||
|
||||
let dir = ocsf_log_dir();
|
||||
open_ocsf_jsonl_layer(ocsf_log_dir())
|
||||
}
|
||||
|
||||
/// Portable sink construction; environment and platform defaults are resolved
|
||||
/// separately so operators and tests can supply an explicit directory.
|
||||
fn open_ocsf_jsonl_layer(
|
||||
dir: std::path::PathBuf,
|
||||
) -> (
|
||||
Option<OcsfJsonlLayer<tracing_appender::rolling::RollingFileAppender>>,
|
||||
Option<std::path::PathBuf>,
|
||||
) {
|
||||
if let Err(e) = std::fs::create_dir_all(&dir) {
|
||||
eprintln!(
|
||||
"openshell: could not create OCSF JSONL log dir {}: {e}",
|
||||
@@ -222,24 +216,21 @@ fn build_ocsf_jsonl_layer(
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this gateway explicitly requested the Windows/MXC JSONL sink.
|
||||
/// Whether this gateway explicitly requested the JSONL sink.
|
||||
/// Unknown values fail closed so a typo cannot unexpectedly retain audit data.
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn mxc_ocsf_jsonl_requested(compute_driver: Option<&str>, value: Option<&str>) -> bool {
|
||||
compute_driver == Some("mxc")
|
||||
&& value.is_some_and(|value| {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "on" | "yes"
|
||||
)
|
||||
})
|
||||
fn ocsf_jsonl_requested(value: Option<&str>) -> bool {
|
||||
value.is_some_and(|value| {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "on" | "yes"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve the directory for the OCSF JSONL audit file.
|
||||
///
|
||||
/// Precedence: `OPENSHELL_OCSF_LOG_DIR` (harness / operator override) then
|
||||
/// `%PROGRAMDATA%\OpenShell\logs`.
|
||||
#[cfg(target_os = "windows")]
|
||||
/// the platform's gateway audit directory.
|
||||
fn ocsf_log_dir() -> std::path::PathBuf {
|
||||
if let Ok(dir) = std::env::var("OPENSHELL_OCSF_LOG_DIR") {
|
||||
let trimmed = dir.trim();
|
||||
@@ -247,10 +238,7 @@ fn ocsf_log_dir() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(trimmed);
|
||||
}
|
||||
}
|
||||
if let Ok(pd) = std::env::var("ProgramData") {
|
||||
return std::path::PathBuf::from(pd).join("OpenShell").join("logs");
|
||||
}
|
||||
std::env::temp_dir().join("openshell").join("logs")
|
||||
openshell_core::paths::gateway_audit_log_dir()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -264,7 +252,45 @@ mod tests {
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
use super::mxc_ocsf_jsonl_requested;
|
||||
use super::ocsf_jsonl_requested;
|
||||
|
||||
#[test]
|
||||
fn portable_gateway_audit_sink_writes_jsonl() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let directory = root.path().join("audit");
|
||||
let (layer, resolved) = super::open_ocsf_jsonl_layer(directory.clone());
|
||||
assert_eq!(resolved, Some(directory.clone()));
|
||||
let subscriber = tracing_subscriber::registry().with(layer.expect("open audit sink"));
|
||||
let ctx = EventContext {
|
||||
sandbox_id: "portable-audit".into(),
|
||||
sandbox_name: "portable-audit".into(),
|
||||
container_image: String::new(),
|
||||
hostname: "gateway-host".into(),
|
||||
product_version: openshell_core::VERSION.into(),
|
||||
proxy_ip: "127.0.0.1".parse().unwrap(),
|
||||
proxy_port: 0,
|
||||
origin: openshell_ocsf::EventOrigin::Supervisor,
|
||||
};
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
emit_ocsf_event_routed("portable-audit", AppLifecycleBuilder::new(&ctx).build());
|
||||
});
|
||||
let files: Vec<_> = std::fs::read_dir(directory).unwrap().collect();
|
||||
assert_eq!(files.len(), 1);
|
||||
let contents = std::fs::read_to_string(files[0].as_ref().unwrap().path()).unwrap();
|
||||
let records: Vec<_> = contents.lines().collect();
|
||||
assert_eq!(records.len(), 1);
|
||||
assert!(serde_json::from_str::<serde_json::Value>(records[0]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portable_gateway_audit_sink_rejects_a_file_as_directory() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let path = root.path().join("file");
|
||||
std::fs::write(&path, b"not a directory").unwrap();
|
||||
let (layer, directory) = super::open_ocsf_jsonl_layer(path);
|
||||
assert!(layer.is_none());
|
||||
assert!(directory.is_none());
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SharedWriter(Arc<Mutex<Vec<u8>>>);
|
||||
@@ -316,33 +342,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn gateway_ocsf_jsonl_requires_explicit_opt_in() {
|
||||
assert!(!mxc_ocsf_jsonl_requested(Some("mxc"), None));
|
||||
assert!(!mxc_ocsf_jsonl_requested(Some("mxc"), Some("")));
|
||||
assert!(!mxc_ocsf_jsonl_requested(Some("mxc"), Some("enabled")));
|
||||
assert!(!ocsf_jsonl_requested(None));
|
||||
assert!(!ocsf_jsonl_requested(Some("")));
|
||||
assert!(!ocsf_jsonl_requested(Some("enabled")));
|
||||
for value in ["0", "false", "FALSE", " off ", "no"] {
|
||||
assert!(!mxc_ocsf_jsonl_requested(Some("mxc"), Some(value)));
|
||||
assert!(!ocsf_jsonl_requested(Some(value)));
|
||||
}
|
||||
|
||||
for value in ["1", "true", "TRUE", " on ", "yes"] {
|
||||
assert!(
|
||||
mxc_ocsf_jsonl_requested(Some("mxc"), Some(value)),
|
||||
ocsf_jsonl_requested(Some(value)),
|
||||
"expected {value:?} to opt in"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_ocsf_jsonl_rejects_non_mxc_drivers() {
|
||||
for driver in [
|
||||
None,
|
||||
Some("docker"),
|
||||
Some("kubernetes"),
|
||||
Some("podman"),
|
||||
Some("vm"),
|
||||
] {
|
||||
assert!(!mxc_ocsf_jsonl_requested(driver, Some("1")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -222,6 +222,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result<HostProxyHandle
|
||||
None,
|
||||
None,
|
||||
Some(direct_listener_identity),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -293,6 +293,14 @@ mod tests {
|
||||
value.repeat(32).parse().unwrap()
|
||||
}
|
||||
|
||||
fn native_identity_path(path: &str) -> PathBuf {
|
||||
if cfg!(target_os = "windows") && path.starts_with('/') {
|
||||
PathBuf::from(format!("C:{path}"))
|
||||
} else {
|
||||
PathBuf::from(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn supplied_identity(
|
||||
executable_path: &str,
|
||||
executable_digest: Option<&str>,
|
||||
@@ -300,13 +308,13 @@ mod tests {
|
||||
) -> BinaryIdentity {
|
||||
BinaryIdentity {
|
||||
executable: ExecutableIdentity {
|
||||
path: PathBuf::from(executable_path),
|
||||
path: native_identity_path(executable_path),
|
||||
digest: executable_digest.map(digest),
|
||||
},
|
||||
ancestors: ancestors
|
||||
.iter()
|
||||
.map(|(path, digest_value)| ExecutableIdentity {
|
||||
path: PathBuf::from(path),
|
||||
path: native_identity_path(path),
|
||||
digest: digest_value.map(digest),
|
||||
})
|
||||
.collect(),
|
||||
@@ -450,7 +458,7 @@ mod tests {
|
||||
.hashes
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(Path::new("/sandbox/other"))
|
||||
.contains_key(&native_identity_path("/sandbox/other"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -480,8 +488,8 @@ mod tests {
|
||||
assert!(error.contains("capacity"));
|
||||
let hashes = cache.hashes.lock().unwrap();
|
||||
assert_eq!(hashes.len(), 4095);
|
||||
assert!(!hashes.contains_key(Path::new("/sandbox/new-leaf")));
|
||||
assert!(!hashes.contains_key(Path::new("/sandbox/new-ancestor")));
|
||||
assert!(!hashes.contains_key(&native_identity_path("/sandbox/new-leaf")));
|
||||
assert!(!hashes.contains_key(&native_identity_path("/sandbox/new-ancestor")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1538,6 +1538,7 @@ fn validate_opa_static_settings(data: &mut serde_json::Value) -> Result<()> {
|
||||
fn redacted_policy_violation_category(violation: &PolicyViolation) -> &'static str {
|
||||
match violation {
|
||||
PolicyViolation::InvalidProcessIdentity { .. } => "invalid process identity",
|
||||
PolicyViolation::InvalidUiClipboardAccess { .. } => "invalid UI clipboard access",
|
||||
PolicyViolation::InvalidLandlockCompatibility { .. } => "invalid Landlock compatibility",
|
||||
PolicyViolation::PathTraversal { .. }
|
||||
| PolicyViolation::RelativePath { .. }
|
||||
@@ -3343,6 +3344,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(),
|
||||
}
|
||||
@@ -5513,6 +5515,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");
|
||||
@@ -6051,6 +6054,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
|
||||
@@ -6122,6 +6126,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
|
||||
@@ -6198,6 +6203,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
|
||||
@@ -7941,6 +7947,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
|
||||
@@ -7998,6 +8005,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
|
||||
@@ -8056,6 +8064,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
|
||||
@@ -8116,6 +8125,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
|
||||
@@ -8175,6 +8185,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
|
||||
@@ -9686,6 +9697,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 {
|
||||
@@ -9756,6 +9768,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");
|
||||
|
||||
@@ -9986,6 +9999,7 @@ network_policies:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
let engine = OpaEngine::from_proto(&proto).unwrap();
|
||||
// Port 443
|
||||
@@ -10951,6 +10965,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
|
||||
@@ -11634,6 +11649,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/)
|
||||
@@ -11797,6 +11813,7 @@ process:
|
||||
}),
|
||||
network_policies,
|
||||
network_middlewares: std::collections::HashMap::default(),
|
||||
ui: None,
|
||||
};
|
||||
|
||||
// Initial load at pid=0 — no symlink expansion
|
||||
|
||||
@@ -273,6 +273,7 @@ impl ProxyHandle {
|
||||
network_mediation_source: Option<Arc<dyn NetworkMediationSource>>,
|
||||
policy_dns_store: Option<Arc<ResolvedEndpointStore>>,
|
||||
direct_listener_identity: Option<ContractBinaryIdentity>,
|
||||
required_proxy_authorization: Option<Arc<str>>,
|
||||
) -> Result<Self> {
|
||||
// Use override bind_addr, fall back to policy http_addr, then default
|
||||
// to loopback:3128. The default allows the proxy to function when no
|
||||
@@ -289,6 +290,11 @@ impl ProxyHandle {
|
||||
}
|
||||
|
||||
let source_backed = network_mediation_source.is_some();
|
||||
if source_backed && required_proxy_authorization.is_some() {
|
||||
return Err(miette::miette!(
|
||||
"proxy authorization cannot be required for a network mediation source"
|
||||
));
|
||||
}
|
||||
let listener = if source_backed {
|
||||
None
|
||||
} else {
|
||||
@@ -511,6 +517,7 @@ impl ProxyHandle {
|
||||
let dtx = denial_tx.clone();
|
||||
let atx = activity_tx.clone();
|
||||
let endpoint_observations = endpoint_observation_tx.clone();
|
||||
let required_authorization = required_proxy_authorization.clone();
|
||||
tokio::spawn(async move {
|
||||
#[allow(clippy::large_futures)]
|
||||
if let Err(err) = handle_mediated_connection(
|
||||
@@ -534,6 +541,7 @@ impl ProxyHandle {
|
||||
dtx,
|
||||
atx,
|
||||
endpoint_observations,
|
||||
required_authorization,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1555,7 +1563,6 @@ fn build_accept_error_event(
|
||||
.message(message)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn classify_accept_error(
|
||||
err: &std::io::Error,
|
||||
consecutive_resource_errors: &mut u32,
|
||||
@@ -1563,7 +1570,6 @@ fn classify_accept_error(
|
||||
) -> AcceptAction {
|
||||
#[cfg(not(unix))]
|
||||
let _ = (err, &mut *consecutive_resource_errors);
|
||||
|
||||
#[cfg(unix)]
|
||||
if matches!(
|
||||
err.raw_os_error(),
|
||||
@@ -2315,6 +2321,40 @@ where
|
||||
.await
|
||||
}
|
||||
|
||||
fn constant_time_bytes_eq(left: &[u8], right: &[u8]) -> bool {
|
||||
let max_len = left.len().max(right.len());
|
||||
let mut difference = left.len() ^ right.len();
|
||||
for index in 0..max_len {
|
||||
let left_byte = left.get(index).copied().unwrap_or_default();
|
||||
let right_byte = right.get(index).copied().unwrap_or_default();
|
||||
difference |= usize::from(left_byte ^ right_byte);
|
||||
}
|
||||
difference == 0
|
||||
}
|
||||
|
||||
fn has_valid_proxy_authorization(request: &str, expected: &str) -> bool {
|
||||
let mut provided = None;
|
||||
for line in request.split("\r\n").skip(1) {
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
let Some((name, value)) = line.split_once(':') else {
|
||||
return false;
|
||||
};
|
||||
if name.eq_ignore_ascii_case("proxy-authorization") {
|
||||
// Reject duplicates even when both values are correct. Accepting
|
||||
// ambiguous credentials can produce parser differentials between
|
||||
// this proxy and downstream HTTP implementations.
|
||||
if provided.is_some() {
|
||||
return false;
|
||||
}
|
||||
provided = Some(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
provided.is_some_and(|value| constant_time_bytes_eq(value.as_bytes(), expected.as_bytes()))
|
||||
}
|
||||
|
||||
// Many distinct, non-related context parameters are required for a CONNECT
|
||||
// dispatch; bundling them into a struct would just shift the noise into call
|
||||
// sites.
|
||||
@@ -2367,6 +2407,7 @@ async fn handle_tcp_connection(
|
||||
denial_tx,
|
||||
activity_tx,
|
||||
endpoint_observation_tx,
|
||||
None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -2438,6 +2479,7 @@ async fn handle_mediated_connection(
|
||||
denial_tx: Option<mpsc::UnboundedSender<DenialEvent>>,
|
||||
activity_tx: Option<ActivitySender>,
|
||||
endpoint_observation_tx: Option<EndpointObservationSender>,
|
||||
required_proxy_authorization: Option<Arc<str>>,
|
||||
) -> Result<()> {
|
||||
// Bind observations to the policy/provider inventory active when this
|
||||
// connection was accepted, even if configuration changes while it runs.
|
||||
@@ -2536,6 +2578,20 @@ async fn handle_mediated_connection(
|
||||
respond(&mut client, b"HTTP/1.1 400 Bad Request\r\n\r\n").await?;
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(expected) = required_proxy_authorization.as_deref()
|
||||
&& !has_valid_proxy_authorization(request, expected)
|
||||
{
|
||||
warn!("Rejected host proxy request with missing or invalid per-sandbox credentials");
|
||||
respond(
|
||||
&mut client,
|
||||
b"HTTP/1.1 407 Proxy Authentication Required\r\n\
|
||||
Proxy-Authenticate: Basic realm=\"OpenShell\"\r\n\
|
||||
Content-Length: 0\r\n\
|
||||
Connection: close\r\n\r\n",
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut lines = request.split("\r\n");
|
||||
let request_line = lines.next().unwrap_or("");
|
||||
let mut parts = request_line.split_whitespace();
|
||||
@@ -6934,17 +6990,60 @@ fn is_benign_relay_error(err: &miette::Report) -> bool {
|
||||
reason = "Test code: test fixtures and explicit control-flow markers are idiomatic in tests."
|
||||
)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn proxy_authorization_rejects_missing_wrong_cross_and_duplicate_credentials() {
|
||||
let expected = "Basic sandbox-a-generation";
|
||||
let request = |headers: &str| format!("CONNECT example.com:443 HTTP/1.1\r\n{headers}\r\n");
|
||||
for headers in [
|
||||
"",
|
||||
"Proxy-Authorization: Basic wrong\r\n",
|
||||
"Proxy-Authorization: Basic sandbox-b-generation\r\n",
|
||||
"Proxy-Authorization: Basic sandbox-a-generation\r\nProxy-Authorization: Basic sandbox-a-generation\r\n",
|
||||
"Malformed-header\r\nProxy-Authorization: Basic sandbox-a-generation\r\n",
|
||||
] {
|
||||
assert!(!has_valid_proxy_authorization(&request(headers), expected));
|
||||
}
|
||||
assert!(has_valid_proxy_authorization(
|
||||
&request("proxy-authorization: Basic sandbox-a-generation\r\n"),
|
||||
expected,
|
||||
));
|
||||
}
|
||||
|
||||
use super::*;
|
||||
use openshell_core::proposals::AgentProposals;
|
||||
use std::collections::HashMap as TestHashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
// These contract tests use host-native absolute identities and matching
|
||||
// policy declarations. Relative-path rejection remains independently tested.
|
||||
fn native_identity_path(path: impl AsRef<Path>) -> PathBuf {
|
||||
let path = path.as_ref();
|
||||
if cfg!(target_os = "windows") && path.to_string_lossy().starts_with('/') {
|
||||
PathBuf::from(format!("C:{}", path.display()))
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
fn native_identity_policy(source: &str) -> std::borrow::Cow<'_, str> {
|
||||
if cfg!(target_os = "windows") {
|
||||
std::borrow::Cow::Owned(source.replace("path: /", "path: C:/"))
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(source)
|
||||
}
|
||||
}
|
||||
|
||||
fn native_identity_engine(rego: &str, source: &str) -> Result<OpaEngine> {
|
||||
OpaEngine::from_strings(rego, &native_identity_policy(source))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supplied_identity_preserves_authorized_endpoint_metadata() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
r#"
|
||||
network_policies:
|
||||
@@ -6975,7 +7074,7 @@ process:
|
||||
.expect("load policy");
|
||||
let identity = Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/usr/bin/python3"),
|
||||
path: native_identity_path("/usr/bin/python3"),
|
||||
digest: Some("00".repeat(32).parse().expect("digest")),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -7001,7 +7100,7 @@ process:
|
||||
|
||||
#[test]
|
||||
fn supplied_identity_rejects_same_path_replacement() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
r#"
|
||||
network_policies:
|
||||
@@ -7031,7 +7130,7 @@ process:
|
||||
let identity = |digest_byte: &str| {
|
||||
Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/sandbox/bin/client"),
|
||||
path: native_identity_path("/sandbox/bin/client"),
|
||||
digest: Some(digest_byte.repeat(32).parse().expect("digest")),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -7058,7 +7157,7 @@ process:
|
||||
|
||||
#[test]
|
||||
fn supplied_identity_rejects_replaced_authorizing_ancestor() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
r#"
|
||||
network_policies:
|
||||
@@ -7084,11 +7183,11 @@ process:
|
||||
let identity = |ancestor_digest: &str| {
|
||||
Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/sandbox/bin/client"),
|
||||
path: native_identity_path("/sandbox/bin/client"),
|
||||
digest: Some("11".repeat(32).parse().expect("digest")),
|
||||
},
|
||||
ancestors: vec![ContractExecutableIdentity {
|
||||
path: PathBuf::from("/sandbox/bin/launcher"),
|
||||
path: native_identity_path("/sandbox/bin/launcher"),
|
||||
digest: Some(ancestor_digest.repeat(32).parse().expect("digest")),
|
||||
}],
|
||||
cmdline_paths: Vec::new(),
|
||||
@@ -7131,11 +7230,11 @@ process:
|
||||
run_as_group: sandbox
|
||||
"#;
|
||||
let rego = include_str!("../data/sandbox-policy.rego");
|
||||
let engine = OpaEngine::from_strings(rego, POLICY_DATA).expect("load policy");
|
||||
let engine = native_identity_engine(rego, POLICY_DATA).expect("load policy");
|
||||
let identity = |digest_byte: &str| {
|
||||
Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/sandbox/bin/client"),
|
||||
path: native_identity_path("/sandbox/bin/client"),
|
||||
digest: Some(digest_byte.repeat(32).parse().expect("digest")),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -7149,7 +7248,9 @@ process:
|
||||
authorize_supplied_identity(&engine, &identity_cache, intent(), &identity("11"));
|
||||
assert!(matches!(original.action, NetworkAction::Allow { .. }));
|
||||
|
||||
engine.reload(rego, POLICY_DATA).expect("reload policy");
|
||||
engine
|
||||
.reload(rego, &native_identity_policy(POLICY_DATA))
|
||||
.expect("reload policy");
|
||||
let replacement =
|
||||
authorize_supplied_identity(&engine, &identity_cache, intent(), &identity("22"));
|
||||
|
||||
@@ -7163,7 +7264,7 @@ process:
|
||||
|
||||
#[test]
|
||||
fn supplied_identity_rejects_missing_ancestor_digest_before_policy() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
r#"
|
||||
network_policies:
|
||||
@@ -7188,11 +7289,11 @@ process:
|
||||
.expect("load policy");
|
||||
let identity = Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/sandbox/bin/client"),
|
||||
path: native_identity_path("/sandbox/bin/client"),
|
||||
digest: Some("11".repeat(32).parse().expect("digest")),
|
||||
},
|
||||
ancestors: vec![ContractExecutableIdentity {
|
||||
path: PathBuf::from("/sandbox/bin/launcher"),
|
||||
path: native_identity_path("/sandbox/bin/launcher"),
|
||||
digest: None,
|
||||
}],
|
||||
cmdline_paths: Vec::new(),
|
||||
@@ -7214,7 +7315,7 @@ process:
|
||||
|
||||
#[tokio::test]
|
||||
async fn staged_transparent_open_waits_for_l4_policy() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
r#"
|
||||
network_policies:
|
||||
@@ -7244,7 +7345,7 @@ process:
|
||||
let identity = || {
|
||||
Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/usr/bin/curl"),
|
||||
path: native_identity_path("/usr/bin/curl"),
|
||||
digest: Some("00".repeat(32).parse().unwrap()),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -7337,7 +7438,10 @@ process:
|
||||
.expect("policy denial is sent to mapper");
|
||||
assert_eq!(event.host, "203.0.113.8");
|
||||
assert_eq!(event.port, 443);
|
||||
assert_eq!(event.binary, "/usr/bin/curl");
|
||||
assert_eq!(
|
||||
event.binary,
|
||||
native_identity_path("/usr/bin/curl").display().to_string()
|
||||
);
|
||||
assert_eq!(event.denial_stage, "transparent_tcp_connect");
|
||||
assert!(denial_rx.try_recv().is_err(), "exactly one mapper event");
|
||||
}
|
||||
@@ -7439,7 +7543,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
stream: Box::new(stream),
|
||||
binary_identity: Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/usr/bin/curl"),
|
||||
path: native_identity_path("/usr/bin/curl"),
|
||||
digest: Some("00".repeat(32).parse().unwrap()),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -7514,6 +7618,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -7622,7 +7727,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
|
||||
#[tokio::test]
|
||||
async fn staged_transparent_open_dials_only_pinned_policy_dns_addresses() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
POLICY_DNS_OPEN_POLICY,
|
||||
)
|
||||
@@ -7662,7 +7767,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
|
||||
#[tokio::test]
|
||||
async fn staged_transparent_open_proposes_a_denied_observation_hostname() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
POLICY_DNS_OPEN_POLICY,
|
||||
)
|
||||
@@ -7697,12 +7802,15 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
let event = denial_rx.try_recv().expect("denial is sent to the mapper");
|
||||
assert_eq!(event.host, "unknown.example");
|
||||
assert_eq!(event.port, 443);
|
||||
assert_eq!(event.binary, "/usr/bin/curl");
|
||||
assert_eq!(
|
||||
event.binary,
|
||||
native_identity_path("/usr/bin/curl").display().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn staged_transparent_open_never_relays_an_observation_that_policy_allows() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
POLICY_DNS_OPEN_POLICY,
|
||||
)
|
||||
@@ -7768,7 +7876,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
|
||||
#[test]
|
||||
fn pinned_plan_requires_the_mapping_of_the_deciding_generation() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
POLICY_DNS_OPEN_POLICY,
|
||||
)
|
||||
@@ -7813,7 +7921,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
engine
|
||||
.reload(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
POLICY_DNS_OPEN_POLICY,
|
||||
&native_identity_policy(POLICY_DNS_OPEN_POLICY),
|
||||
)
|
||||
.unwrap();
|
||||
let reloaded = decide("db.example", 5432);
|
||||
@@ -7836,7 +7944,10 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
);
|
||||
assert_eq!(
|
||||
mapped.format_shorthand(),
|
||||
"NET:OPEN [MED] DENIED /usr/bin/curl(0) -> blocked.invalid:80 [reason:transparent_tcp_policy_denied]"
|
||||
format!(
|
||||
"NET:OPEN [MED] DENIED {}(0) -> blocked.invalid:80 [reason:transparent_tcp_policy_denied]",
|
||||
native_identity_path("/usr/bin/curl").display()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(mapped).unwrap()["dst_endpoint"],
|
||||
@@ -7859,7 +7970,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
#[tokio::test]
|
||||
async fn staged_policy_local_open_reaches_the_sandbox_scoped_api() {
|
||||
let engine = Arc::new(
|
||||
OpaEngine::from_strings(
|
||||
native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
"network_policies: {}\n",
|
||||
)
|
||||
@@ -7868,7 +7979,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
let cache = Arc::new(BinaryIdentityCache::new());
|
||||
let identity = ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/usr/bin/bash"),
|
||||
path: native_identity_path("/usr/bin/bash"),
|
||||
digest: Some("44".repeat(32).parse().unwrap()),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -7928,6 +8039,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
workload
|
||||
.write_all(b"GET /v1/policy/current HTTP/1.1\r\nHost: policy.local\r\nConnection: close\r\n\r\n")
|
||||
@@ -7978,7 +8090,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox }
|
||||
.unwrap();
|
||||
});
|
||||
let engine = Arc::new(
|
||||
OpaEngine::from_strings(
|
||||
native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
&format!(
|
||||
r#"
|
||||
@@ -8004,7 +8116,7 @@ network_policies:
|
||||
stream: Box::new(stream),
|
||||
binary_identity: Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/usr/bin/curl"),
|
||||
path: native_identity_path("/usr/bin/curl"),
|
||||
digest: Some("44".repeat(32).parse().unwrap()),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -8056,6 +8168,7 @@ network_policies:
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
let request = tokio::time::timeout(std::time::Duration::from_secs(5), request_rx)
|
||||
.await
|
||||
@@ -8182,7 +8295,7 @@ process:
|
||||
|
||||
#[tokio::test]
|
||||
async fn staged_transparent_open_reports_identity_cache_capacity_exhaustion() {
|
||||
let engine = OpaEngine::from_strings(
|
||||
let engine = native_identity_engine(
|
||||
include_str!("../data/sandbox-policy.rego"),
|
||||
r#"
|
||||
network_policies:
|
||||
@@ -8210,7 +8323,7 @@ process:
|
||||
identity_cache
|
||||
.verify_or_cache_supplied_identity(&ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from(format!("/sandbox/pinned-{index}")),
|
||||
path: native_identity_path(format!("/sandbox/pinned-{index}")),
|
||||
digest: Some("11".repeat(32).parse().unwrap()),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -8224,7 +8337,7 @@ process:
|
||||
stream: Box::new(stream),
|
||||
binary_identity: Ok(ContractBinaryIdentity {
|
||||
executable: ContractExecutableIdentity {
|
||||
path: PathBuf::from("/sandbox/overflow"),
|
||||
path: native_identity_path("/sandbox/overflow"),
|
||||
digest: Some("22".repeat(32).parse().unwrap()),
|
||||
},
|
||||
ancestors: Vec::new(),
|
||||
@@ -8662,6 +8775,7 @@ network_policies: {}
|
||||
Some(Arc::new(FailedMediationSource)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("proxy starts before source accept");
|
||||
|
||||
@@ -40,6 +40,25 @@ use crate::proxy::ProxyHandle;
|
||||
use openshell_core::endpoint_status::EndpointObservationSender;
|
||||
use openshell_isolation_interface::contract::NetworkMediationSource;
|
||||
|
||||
/// Concrete listener options supplied by trusted supervisor composition.
|
||||
/// This is not an isolation-backend capability or a serialized launch contract.
|
||||
#[derive(Clone)]
|
||||
pub struct ProxyListenerConfig {
|
||||
pub bind_addr: SocketAddr,
|
||||
pub authorization: Arc<str>,
|
||||
pub binary_identity: openshell_isolation_interface::contract::BinaryIdentity,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ProxyListenerConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ProxyListenerConfig")
|
||||
.field("bind_addr", &self.bind_addr)
|
||||
.field("authorization", &"<redacted>")
|
||||
.field("binary_identity", &self.binary_identity)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub struct TransparentRuntimeSetup {
|
||||
pub listeners: Vec<tokio::net::TcpListener>,
|
||||
@@ -201,6 +220,7 @@ pub async fn run_networking(
|
||||
host_gateway_ip: Option<IpAddr>,
|
||||
#[cfg(target_os = "linux")] transparent_runtime: Option<TransparentRuntimeSetup>,
|
||||
network_mediation_source: Option<Arc<dyn NetworkMediationSource>>,
|
||||
proxy_listener: Option<ProxyListenerConfig>,
|
||||
) -> Result<Networking> {
|
||||
// Build the policy-local route context. The orchestrator's policy poll
|
||||
// loop also holds an `Arc` clone (via `Networking::policy_local_ctx`) so
|
||||
@@ -467,10 +487,15 @@ pub async fn run_networking(
|
||||
// originating inside the namespace can reach the proxy. Otherwise the
|
||||
// proxy falls back to the policy-declared http_addr (loopback in
|
||||
// tests, etc.).
|
||||
let bind_addr = proxy_bind_ip.map(|ip| {
|
||||
let port = proxy_policy.http_addr.map_or(3128, |addr| addr.port());
|
||||
SocketAddr::new(ip, port)
|
||||
});
|
||||
let bind_addr = proxy_listener
|
||||
.as_ref()
|
||||
.map(|proxy| proxy.bind_addr)
|
||||
.or_else(|| {
|
||||
proxy_bind_ip.map(|ip| {
|
||||
let port = proxy_policy.http_addr.map_or(3128, |addr| addr.port());
|
||||
SocketAddr::new(ip, port)
|
||||
})
|
||||
});
|
||||
|
||||
let proxy_handle = ProxyHandle::start_with_bind_addr(
|
||||
proxy_policy,
|
||||
@@ -491,7 +516,12 @@ pub async fn run_networking(
|
||||
mediated_policy_dns
|
||||
.as_ref()
|
||||
.map(|runtime| runtime.store.clone()),
|
||||
None,
|
||||
proxy_listener
|
||||
.as_ref()
|
||||
.map(|proxy| proxy.binary_identity.clone()),
|
||||
proxy_listener
|
||||
.as_ref()
|
||||
.map(|proxy| proxy.authorization.clone()),
|
||||
)
|
||||
.await?;
|
||||
Some(proxy_handle)
|
||||
|
||||
@@ -16,6 +16,12 @@ The client receives the supervisor's live provider state, bearer-token slot, and
|
||||
|
||||
The setup interface stays private to the supervisor. It adds no runtime backend registration, endpoint configuration, or public factory API. The public `run_sandbox` signature and standard backend selection remain unchanged.
|
||||
|
||||
The private startup result separates the backend transport payload from optional
|
||||
authenticated CONNECT listener settings. Shared networking starts the listener
|
||||
only after boundary attachment and confirmation; the supervisor owns policy
|
||||
evaluation, live credentials, and listener lifetime. These settings do not add
|
||||
a proxy hook to the isolation interface or the shared Sandbox Protocol descriptor.
|
||||
|
||||
## Portable supervisor access
|
||||
|
||||
The supervisor consumes the shared isolation backend contract. Its gateway
|
||||
|
||||
@@ -36,6 +36,14 @@ pub struct BackendServices {
|
||||
pub sandbox_bearer: SessionBearerTokenSlot,
|
||||
}
|
||||
|
||||
/// Private startup result; native payload decoding never reaches shared runtime
|
||||
/// descriptors or the public isolation interface.
|
||||
pub struct BuiltBackend {
|
||||
pub backend: Arc<dyn IsolationBackend>,
|
||||
pub payload: Vec<u8>,
|
||||
pub proxy_listener: Option<openshell_supervisor_network::run::ProxyListenerConfig>,
|
||||
}
|
||||
|
||||
/// Selected by trusted composition, never by payload contents. Decoding and
|
||||
/// construction may prepare a client but must not launch workload code.
|
||||
pub trait BackendSetup: Sync {
|
||||
@@ -66,7 +74,7 @@ pub trait PreparedBackend: Send + Sync {
|
||||
fn build(
|
||||
self: Box<Self>,
|
||||
services: BackendServices,
|
||||
) -> std::result::Result<Arc<dyn IsolationBackend>, BackendError>;
|
||||
) -> std::result::Result<BuiltBackend, BackendError>;
|
||||
}
|
||||
|
||||
/// Created only after name and launch identity checks. Consuming attachment
|
||||
@@ -153,24 +161,28 @@ impl SelectedBackend {
|
||||
/// Construct and attach the selected client using the admitted policy and
|
||||
/// shared services. Registry verification rejects a differently named client.
|
||||
pub async fn attach(
|
||||
self,
|
||||
mut self,
|
||||
services: BackendServices,
|
||||
policy: SandboxPolicy,
|
||||
agent: AgentSpec,
|
||||
) -> Result<Box<dyn BoundBoundary>> {
|
||||
let backend = self
|
||||
) -> Result<(
|
||||
Box<dyn BoundBoundary>,
|
||||
Option<openshell_supervisor_network::run::ProxyListenerConfig>,
|
||||
)> {
|
||||
let built = self
|
||||
.prepared
|
||||
.build(services)
|
||||
.map_err(|error| miette::miette!(error.to_string()))?;
|
||||
self.descriptor.payload = built.payload;
|
||||
let mut registry = BackendRegistry::new();
|
||||
registry
|
||||
.register(backend)
|
||||
.register(built.backend)
|
||||
.map_err(|error| miette::miette!(error.to_string()))?;
|
||||
let admitted_backend = self.descriptor.backend_name.clone();
|
||||
let (backend, verified) = registry
|
||||
.resolve(self.descriptor, &admitted_backend)
|
||||
.map_err(|error| miette::miette!(error.to_string()))?;
|
||||
backend
|
||||
let bound = backend
|
||||
.attach(
|
||||
verified,
|
||||
SandboxContext {
|
||||
@@ -182,7 +194,8 @@ impl SelectedBackend {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| miette::miette!(error.to_string()))
|
||||
.map_err(|error| miette::miette!(error.to_string()))?;
|
||||
Ok((bound, built.proxy_listener))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,14 +248,17 @@ impl PreparedBackend for OpenShellLaunch {
|
||||
fn build(
|
||||
self: Box<Self>,
|
||||
services: BackendServices,
|
||||
) -> std::result::Result<Arc<dyn IsolationBackend>, BackendError> {
|
||||
Ok(Arc::new(
|
||||
openshell_sandbox_backend::OpenShellRuntimeBackend::new(
|
||||
) -> std::result::Result<BuiltBackend, BackendError> {
|
||||
let payload = self.0.backend_descriptor()?.payload;
|
||||
Ok(BuiltBackend {
|
||||
payload,
|
||||
proxy_listener: None,
|
||||
backend: Arc::new(openshell_sandbox_backend::OpenShellRuntimeBackend::new(
|
||||
services.ca_file_paths,
|
||||
services.provider_credentials,
|
||||
services.sandbox_bearer,
|
||||
),
|
||||
))
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use openshell_isolation_interface::contract::{
|
||||
NetworkMediationSource, OuterFenceGuarantee, OuterFenceGuarantees, PendingDnsQuery,
|
||||
PendingTcpOpen, ReadyBoundary, RunningBoundary, VerifiedBackendDescriptor,
|
||||
};
|
||||
#[cfg(unix)]
|
||||
use openshell_supervisor_network::upstream_proxy::UpstreamProxyArgs;
|
||||
|
||||
const TEST_BACKEND: &str = "in-process-test";
|
||||
@@ -184,17 +185,21 @@ impl PreparedBackend for TestLaunch {
|
||||
fn build(
|
||||
self: Box<Self>,
|
||||
services: BackendServices,
|
||||
) -> std::result::Result<Arc<dyn IsolationBackend>, BackendError> {
|
||||
) -> std::result::Result<BuiltBackend, BackendError> {
|
||||
self.observed.record("build");
|
||||
let services = Arc::new(services);
|
||||
*self.observed.services.lock().unwrap() = Arc::downgrade(&services);
|
||||
Ok(Arc::new(TestBackend {
|
||||
name: self.name,
|
||||
observed: self.observed,
|
||||
services,
|
||||
generation: self.generation,
|
||||
expected_session: self.expected_session,
|
||||
}))
|
||||
Ok(BuiltBackend {
|
||||
payload: TEST_PAYLOAD.to_vec(),
|
||||
proxy_listener: None,
|
||||
backend: Arc::new(TestBackend {
|
||||
name: self.name,
|
||||
observed: self.observed,
|
||||
services,
|
||||
generation: self.generation,
|
||||
expected_session: self.expected_session,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,7 +471,8 @@ async fn attachment_receives_live_supervisor_services() {
|
||||
.select()
|
||||
.attach(services, policy(), agent())
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
.0;
|
||||
let backend_services = setup.observed.services.lock().unwrap().upgrade().unwrap();
|
||||
assert!(Arc::ptr_eq(&ca_paths, &backend_services.ca_file_paths));
|
||||
assert!(Arc::ptr_eq(
|
||||
|
||||
@@ -600,6 +600,7 @@ pub async fn run_network_proxy(
|
||||
#[cfg(target_os = "linux")]
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -926,7 +927,7 @@ async fn run_sandbox_with_backend(
|
||||
// supervisor-owned handles shared with the backend.
|
||||
let admitted_backend_name = selected_backend.backend_name().to_string();
|
||||
let ca_file_paths = Arc::new(std::sync::Mutex::new(None));
|
||||
let bound = selected_backend
|
||||
let (bound, proxy_listener) = selected_backend
|
||||
.attach(
|
||||
backend_setup::BackendServices {
|
||||
ca_file_paths: ca_file_paths.clone(),
|
||||
@@ -1009,7 +1010,9 @@ async fn run_sandbox_with_backend(
|
||||
// API read the current value so proposals target the correct workspace.
|
||||
let (workspace_tx, workspace_rx) = tokio::sync::watch::channel(String::new());
|
||||
|
||||
let remote_network_source = remote_boundary.0.network_mediation_source();
|
||||
let remote_network_source = proxy_listener
|
||||
.is_none()
|
||||
.then(|| remote_boundary.0.network_mediation_source());
|
||||
let remote_host_gateway_ip = remote_boundary.0.host_gateway_ip();
|
||||
let (remote_ready, backend_name, ca_file_paths) = {
|
||||
let (bound, backend_name, ca_file_paths) = remote_boundary;
|
||||
@@ -1047,7 +1050,8 @@ async fn run_sandbox_with_backend(
|
||||
remote_host_gateway_ip,
|
||||
#[cfg(target_os = "linux")]
|
||||
None,
|
||||
Some(remote_network_source),
|
||||
remote_network_source,
|
||||
proxy_listener,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -920,6 +920,12 @@ administrator or belong to the Windows Performance Log Users group. Workload
|
||||
commands and working directories remain sandbox-scoped and must be supplied in
|
||||
the `mxc` driver configuration when creating a sandbox.
|
||||
|
||||
`OPENSHELL_OCSF_JSON=1` enables the gateway's JSONL audit
|
||||
sink independently of the selected compute driver. Set `OPENSHELL_OCSF_LOG_DIR`
|
||||
to override the default `%PROGRAMDATA%\OpenShell\logs` directory on Windows
|
||||
or `$XDG_STATE_HOME/openshell/logs` on Unix. Prefer the gateway's configured
|
||||
`ocsf_log` output for retention controls and loss metrics.
|
||||
|
||||
The driver records executable identity in process audit events and omits raw
|
||||
command arguments because they can contain credentials or personal data. See
|
||||
[OCSF JSON Export](/observability/ocsf-json-export) for durable Windows audit
|
||||
|
||||
@@ -20,7 +20,7 @@ Policies](/how-it-works/policies/manage-policies).
|
||||
|
||||
## What a Policy Controls
|
||||
|
||||
A policy file sets `version: 1` and has up to five other top-level sections.
|
||||
A policy file sets `version: 1` and has up to six other top-level sections.
|
||||
Each section controls a different part of the sandbox and takes effect at a
|
||||
specific time:
|
||||
|
||||
@@ -29,10 +29,11 @@ specific time:
|
||||
| `filesystem_policy` | Paths that sandbox processes can read, or read and write. | Landlock LSM in the kernel. | At sandbox startup. |
|
||||
| `landlock` | Whether the sandbox still starts, without your filesystem rules, if OpenShell cannot apply them. | Sandbox runtime. | At sandbox startup. |
|
||||
| `process` | User and group that sandbox processes run as. | Docker and Podman, when they create the sandbox. | At sandbox creation. |
|
||||
| `ui` | Graphical UI, clipboard, and synthetic input. | A compute driver advertising complete UI-policy support. | At sandbox creation. |
|
||||
| `network_policies` | Destinations each binary can reach, and the requests it can send. | Sandbox network proxy. | While the sandbox runs. |
|
||||
| `network_middlewares` | Additional inspection, transformation, or blocking of traffic that network rules allow. | Sandbox network proxy. | While the sandbox runs. |
|
||||
|
||||
Filesystem, Landlock, and process settings are fixed once the sandbox starts.
|
||||
Filesystem, Landlock, process, and UI settings are fixed once the sandbox starts.
|
||||
Network rules and middleware can change while it runs, as described in [How
|
||||
Changes Take
|
||||
Effect](/how-it-works/policies/manage-policies#how-changes-take-effect).
|
||||
|
||||
@@ -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 | Startup | Paths the workload can read and write. |
|
||||
| `landlock` | object | No | Startup | Landlock enforcement behavior. |
|
||||
| `process` | object | No | Startup | User and group that the workload runs as. |
|
||||
| `ui` | object | No | Startup | Graphical UI, clipboard, and input injection on a supporting runtime. |
|
||||
| `network_policies` | map | No | Live | Which binaries can reach which network endpoints. |
|
||||
| `network_middlewares` | map | No | Live | Middleware applied to allowed traffic. |
|
||||
|
||||
@@ -99,6 +101,32 @@ process:
|
||||
run_as_group: "1500"
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
UI controls require the selected compute driver to advertise
|
||||
`openshell.policy.ui.v1` in its extension capabilities. Unsupported drivers
|
||||
reject any explicit UI section, including
|
||||
`ui: {}`, before provisioning. Omit the section to preserve their existing
|
||||
behavior.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `allow_graphical_ui` | bool | `false` | Allows a graphical UI surface. |
|
||||
| `clipboard` | string | `none` | `none`, `read`, `write`, or `all`, from the sandbox's perspective. |
|
||||
| `allow_input_injection` | bool | `false` | Allows synthetic input. |
|
||||
|
||||
Clipboard access and input injection require `allow_graphical_ui: true`.
|
||||
Unknown fields and values are rejected. UI settings are startup-only; recreate
|
||||
the sandbox to change them. Global policies cannot set UI and preserve each
|
||||
sandbox's startup UI settings.
|
||||
|
||||
```yaml showLineNumbers={false}
|
||||
ui:
|
||||
allow_graphical_ui: true
|
||||
clipboard: read
|
||||
allow_input_injection: false
|
||||
```
|
||||
|
||||
## Network Policies
|
||||
|
||||
A map of named rules. The key is the rule's name. Each rule allows every listed
|
||||
|
||||
@@ -63,9 +63,13 @@ $env:OPENSHELL_OCSF_LOG_DIR = "D:\OpenShell\audit"
|
||||
openshell-gateway --drivers mxc --config gateway.toml
|
||||
```
|
||||
|
||||
The sink is available only when Windows selects the MXC driver. It rotates
|
||||
This environment-configured sink is available on all platforms independently of
|
||||
the selected compute driver. On Unix hosts its default directory is
|
||||
`$XDG_STATE_HOME/openshell/logs` (or `$HOME/.local/state/openshell/logs`). It rotates
|
||||
daily and retains the three most recent files. Diagnostic `--log-level` values
|
||||
do not suppress records in this explicitly enabled audit sink.
|
||||
Prefer the `gateway.toml` output above for configured retention and loss metrics;
|
||||
enabling both sinks writes to both independently.
|
||||
|
||||
## Output Location
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ message SandboxPolicy {
|
||||
// policy-local names. At most 10 configs are accepted, and at most 10 stages
|
||||
// can be selected per request.
|
||||
map<string, NetworkMiddlewareConfig> 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.
|
||||
@@ -59,6 +63,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.
|
||||
|
||||
@@ -79,6 +79,10 @@ func copySandboxPolicy(p *types.SandboxPolicy) *types.SandboxPolicy {
|
||||
pr := *p.Process
|
||||
cp.Process = &pr
|
||||
}
|
||||
if p.UI != nil {
|
||||
ui := *p.UI
|
||||
cp.UI = &ui
|
||||
}
|
||||
if p.NetworkPolicies != nil {
|
||||
np := make(map[string]types.NetworkPolicyRule, len(p.NetworkPolicies))
|
||||
for k, rule := range p.NetworkPolicies {
|
||||
|
||||
@@ -843,6 +843,11 @@ func TestFakeSandboxCreateWithPolicy(t *testing.T) {
|
||||
RunAsUser: "sandbox",
|
||||
RunAsGroup: "sandbox-group",
|
||||
},
|
||||
UI: &types.UIPolicy{
|
||||
AllowGraphicalUI: true,
|
||||
Clipboard: types.UIClipboardAccessRead,
|
||||
AllowInputInjection: true,
|
||||
},
|
||||
NetworkPolicies: map[string]types.NetworkPolicyRule{
|
||||
"web": {
|
||||
Name: "web",
|
||||
@@ -880,6 +885,10 @@ func TestFakeSandboxCreateWithPolicy(t *testing.T) {
|
||||
require.NotNil(t, p.Process)
|
||||
assert.Equal(t, "sandbox", p.Process.RunAsUser)
|
||||
assert.Equal(t, "sandbox-group", p.Process.RunAsGroup)
|
||||
require.NotNil(t, p.UI)
|
||||
assert.True(t, p.UI.AllowGraphicalUI)
|
||||
assert.Equal(t, types.UIClipboardAccessRead, p.UI.Clipboard)
|
||||
assert.True(t, p.UI.AllowInputInjection)
|
||||
|
||||
require.Len(t, p.NetworkPolicies, 1)
|
||||
webRule, ok := p.NetworkPolicies["web"]
|
||||
@@ -892,19 +901,23 @@ func TestFakeSandboxCreateWithPolicy(t *testing.T) {
|
||||
// Deep-copy isolation: mutate input spec, verify stored copy unchanged
|
||||
spec.Policy.Version = 99
|
||||
spec.Policy.Filesystem.ReadOnly[0] = "mutated"
|
||||
spec.Policy.UI.Clipboard = types.UIClipboardAccessAll
|
||||
spec.Policy.NetworkPolicies["web"] = types.NetworkPolicyRule{Name: "mutated"}
|
||||
|
||||
got2, err := sc.Get(ctx, "default", "policy-sb")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint32(3), got2.Spec.Policy.Version)
|
||||
assert.Equal(t, "/etc", got2.Spec.Policy.Filesystem.ReadOnly[0])
|
||||
assert.Equal(t, types.UIClipboardAccessRead, got2.Spec.Policy.UI.Clipboard)
|
||||
assert.Equal(t, "web", got2.Spec.Policy.NetworkPolicies["web"].Name)
|
||||
|
||||
// Deep-copy isolation: mutate returned object, verify store unchanged
|
||||
got.Spec.Policy.Filesystem.ReadWrite[0] = "mutated"
|
||||
got.Spec.Policy.UI.AllowInputInjection = false
|
||||
got3, err := sc.Get(ctx, "default", "policy-sb")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/tmp", got3.Spec.Policy.Filesystem.ReadWrite[0])
|
||||
assert.True(t, got3.Spec.Policy.UI.AllowInputInjection)
|
||||
}
|
||||
|
||||
func TestFakeSandboxCreateWithNilPolicy(t *testing.T) {
|
||||
|
||||
@@ -135,7 +135,7 @@ func ListGateways() ([]Info, error) {
|
||||
}
|
||||
}
|
||||
|
||||
sysNames, listErr := listGatewayDirs(systemConfigBase)
|
||||
sysNames, listErr := listGatewayDirs(systemConfigDir())
|
||||
if listErr != nil {
|
||||
return nil, listErr
|
||||
}
|
||||
|
||||
@@ -409,6 +409,7 @@ func TestLoadConfig_ActiveGateway(t *testing.T) {
|
||||
func TestListGateways_MultipleGateways(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", tmp)
|
||||
t.Setenv(systemGatewayDirEnv, tmp)
|
||||
|
||||
for _, name := range []string{"prod", "staging", "dev"} {
|
||||
gwDir := filepath.Join(tmp, "openshell", "gateways", name)
|
||||
@@ -432,6 +433,7 @@ func TestListGateways_MultipleGateways(t *testing.T) {
|
||||
func TestListGateways_EmptyDirs(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", tmp)
|
||||
t.Setenv(systemGatewayDirEnv, tmp)
|
||||
|
||||
gateways, err := ListGateways()
|
||||
require.NoError(t, err)
|
||||
@@ -441,6 +443,7 @@ func TestListGateways_EmptyDirs(t *testing.T) {
|
||||
func TestListGateways_ActiveStatus(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", tmp)
|
||||
t.Setenv(systemGatewayDirEnv, tmp)
|
||||
|
||||
for _, name := range []string{"alpha", "beta"} {
|
||||
gwDir := filepath.Join(tmp, "openshell", "gateways", name)
|
||||
|
||||
@@ -24,6 +24,10 @@ const (
|
||||
|
||||
// systemConfigBase is the system-wide config directory.
|
||||
systemConfigBase = "/etc/openshell"
|
||||
|
||||
// systemGatewayDirEnv overrides the system-wide config root. Keep this in
|
||||
// sync with the Rust CLI so SDK discovery sees the same gateway set.
|
||||
systemGatewayDirEnv = "OPENSHELL_SYSTEM_GATEWAY_DIR"
|
||||
)
|
||||
|
||||
// userConfigDir returns the user-specific configuration directory for
|
||||
@@ -49,7 +53,16 @@ func userConfigDir() (string, error) {
|
||||
|
||||
// systemGatewayDir returns the system-wide gateway config directory.
|
||||
func systemGatewayDir() string {
|
||||
return filepath.Join(systemConfigBase, gatewaySubdir)
|
||||
return filepath.Join(systemConfigDir(), gatewaySubdir)
|
||||
}
|
||||
|
||||
// systemConfigDir returns the system-wide configuration root. Empty and
|
||||
// relative overrides are ignored to match the CLI's fail-safe behavior.
|
||||
func systemConfigDir() string {
|
||||
if dir := os.Getenv(systemGatewayDirEnv); dir != "" && filepath.IsAbs(dir) {
|
||||
return dir
|
||||
}
|
||||
return systemConfigBase
|
||||
}
|
||||
|
||||
// resolveGatewayDir searches for a gateway directory by name, checking the
|
||||
|
||||
@@ -39,6 +39,22 @@ func TestSystemGatewayDir(t *testing.T) {
|
||||
assert.Equal(t, filepath.FromSlash("/etc/openshell/gateways"), dir)
|
||||
}
|
||||
|
||||
func TestSystemGatewayDirOverride(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv(systemGatewayDirEnv, tmp)
|
||||
|
||||
assert.Equal(t, filepath.Join(tmp, "gateways"), systemGatewayDir())
|
||||
}
|
||||
|
||||
func TestSystemGatewayDirIgnoresInvalidOverrides(t *testing.T) {
|
||||
for _, override := range []string{"", "relative/path"} {
|
||||
t.Run(override, func(t *testing.T) {
|
||||
t.Setenv(systemGatewayDirEnv, override)
|
||||
assert.Equal(t, filepath.FromSlash("/etc/openshell/gateways"), systemGatewayDir())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGatewayDir_UserDir(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", tmp)
|
||||
|
||||
@@ -171,6 +171,7 @@ func TestConverterCoversAllProtoFields_SandboxPolicy(t *testing.T) {
|
||||
"network_policies": true,
|
||||
"process": true,
|
||||
"landlock": true,
|
||||
"ui": true,
|
||||
"network_middlewares": true,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -112,7 +112,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
|
||||
@@ -125,6 +125,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
|
||||
@@ -178,6 +181,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).
|
||||
|
||||
@@ -27,6 +27,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}
|
||||
}
|
||||
|
||||
// TLS handling for a network endpoint.
|
||||
type NetworkTlsMode int32
|
||||
|
||||
@@ -72,11 +133,11 @@ func (x NetworkTlsMode) String() string {
|
||||
}
|
||||
|
||||
func (NetworkTlsMode) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_sandbox_proto_enumTypes[0].Descriptor()
|
||||
return file_sandbox_proto_enumTypes[1].Descriptor()
|
||||
}
|
||||
|
||||
func (NetworkTlsMode) Type() protoreflect.EnumType {
|
||||
return &file_sandbox_proto_enumTypes[0]
|
||||
return &file_sandbox_proto_enumTypes[1]
|
||||
}
|
||||
|
||||
func (x NetworkTlsMode) Number() protoreflect.EnumNumber {
|
||||
@@ -85,7 +146,7 @@ func (x NetworkTlsMode) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use NetworkTlsMode.Descriptor instead.
|
||||
func (NetworkTlsMode) EnumDescriptor() ([]byte, []int) {
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{0}
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
// Enforcement behavior for inspected network traffic.
|
||||
@@ -125,11 +186,11 @@ func (x NetworkEnforcementMode) String() string {
|
||||
}
|
||||
|
||||
func (NetworkEnforcementMode) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_sandbox_proto_enumTypes[1].Descriptor()
|
||||
return file_sandbox_proto_enumTypes[2].Descriptor()
|
||||
}
|
||||
|
||||
func (NetworkEnforcementMode) Type() protoreflect.EnumType {
|
||||
return &file_sandbox_proto_enumTypes[1]
|
||||
return &file_sandbox_proto_enumTypes[2]
|
||||
}
|
||||
|
||||
func (x NetworkEnforcementMode) Number() protoreflect.EnumNumber {
|
||||
@@ -138,7 +199,7 @@ func (x NetworkEnforcementMode) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use NetworkEnforcementMode.Descriptor instead.
|
||||
func (NetworkEnforcementMode) EnumDescriptor() ([]byte, []int) {
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{1}
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
// Shorthand access policy for an inspected network endpoint.
|
||||
@@ -182,11 +243,11 @@ func (x NetworkAccessPreset) String() string {
|
||||
}
|
||||
|
||||
func (NetworkAccessPreset) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_sandbox_proto_enumTypes[2].Descriptor()
|
||||
return file_sandbox_proto_enumTypes[3].Descriptor()
|
||||
}
|
||||
|
||||
func (NetworkAccessPreset) Type() protoreflect.EnumType {
|
||||
return &file_sandbox_proto_enumTypes[2]
|
||||
return &file_sandbox_proto_enumTypes[3]
|
||||
}
|
||||
|
||||
func (x NetworkAccessPreset) Number() protoreflect.EnumNumber {
|
||||
@@ -195,7 +256,7 @@ func (x NetworkAccessPreset) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use NetworkAccessPreset.Descriptor instead.
|
||||
func (NetworkAccessPreset) EnumDescriptor() ([]byte, []int) {
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{2}
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
// Scope that currently controls a setting.
|
||||
@@ -232,11 +293,11 @@ func (x SettingScope) String() string {
|
||||
}
|
||||
|
||||
func (SettingScope) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_sandbox_proto_enumTypes[3].Descriptor()
|
||||
return file_sandbox_proto_enumTypes[4].Descriptor()
|
||||
}
|
||||
|
||||
func (SettingScope) Type() protoreflect.EnumType {
|
||||
return &file_sandbox_proto_enumTypes[3]
|
||||
return &file_sandbox_proto_enumTypes[4]
|
||||
}
|
||||
|
||||
func (x SettingScope) Number() protoreflect.EnumNumber {
|
||||
@@ -245,7 +306,7 @@ func (x SettingScope) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use SettingScope.Descriptor instead.
|
||||
func (SettingScope) EnumDescriptor() ([]byte, []int) {
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{3}
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
// Source used for the policy payload in GetSandboxConfigResponse.
|
||||
@@ -282,11 +343,11 @@ func (x PolicySource) String() string {
|
||||
}
|
||||
|
||||
func (PolicySource) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_sandbox_proto_enumTypes[4].Descriptor()
|
||||
return file_sandbox_proto_enumTypes[5].Descriptor()
|
||||
}
|
||||
|
||||
func (PolicySource) Type() protoreflect.EnumType {
|
||||
return &file_sandbox_proto_enumTypes[4]
|
||||
return &file_sandbox_proto_enumTypes[5]
|
||||
}
|
||||
|
||||
func (x PolicySource) Number() protoreflect.EnumNumber {
|
||||
@@ -295,7 +356,7 @@ func (x PolicySource) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use PolicySource.Descriptor instead.
|
||||
func (PolicySource) EnumDescriptor() ([]byte, []int) {
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{4}
|
||||
return file_sandbox_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
// Sandbox security policy configuration.
|
||||
@@ -315,8 +376,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() {
|
||||
@@ -391,6 +456,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"`
|
||||
@@ -556,6 +628,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"`
|
||||
@@ -571,7 +709,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)
|
||||
}
|
||||
@@ -583,7 +721,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 {
|
||||
@@ -596,7 +734,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 {
|
||||
@@ -641,7 +779,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)
|
||||
}
|
||||
@@ -653,7 +791,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 {
|
||||
@@ -666,7 +804,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 {
|
||||
@@ -726,7 +864,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)
|
||||
}
|
||||
@@ -738,7 +876,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 {
|
||||
@@ -751,7 +889,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 {
|
||||
@@ -780,7 +918,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)
|
||||
}
|
||||
@@ -792,7 +930,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 {
|
||||
@@ -805,7 +943,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 {
|
||||
@@ -841,7 +979,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.
|
||||
@@ -917,7 +1054,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)
|
||||
}
|
||||
@@ -929,7 +1066,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 {
|
||||
@@ -942,7 +1079,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 {
|
||||
@@ -1168,7 +1305,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)
|
||||
}
|
||||
@@ -1180,7 +1317,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 {
|
||||
@@ -1193,7 +1330,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 {
|
||||
@@ -1232,7 +1369,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)
|
||||
}
|
||||
@@ -1244,7 +1381,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 {
|
||||
@@ -1257,7 +1394,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 {
|
||||
@@ -1312,7 +1449,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)
|
||||
}
|
||||
@@ -1324,7 +1461,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 {
|
||||
@@ -1337,7 +1474,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 {
|
||||
@@ -1406,7 +1543,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)
|
||||
}
|
||||
@@ -1418,7 +1555,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 {
|
||||
@@ -1431,7 +1568,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 {
|
||||
@@ -1471,7 +1608,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)
|
||||
}
|
||||
@@ -1483,7 +1620,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 {
|
||||
@@ -1496,7 +1633,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 {
|
||||
@@ -1568,7 +1705,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)
|
||||
}
|
||||
@@ -1580,7 +1717,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 {
|
||||
@@ -1593,7 +1730,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 {
|
||||
@@ -1620,7 +1757,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)
|
||||
}
|
||||
@@ -1632,7 +1769,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 {
|
||||
@@ -1645,7 +1782,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 {
|
||||
@@ -1667,7 +1804,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)
|
||||
}
|
||||
@@ -1679,7 +1816,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 {
|
||||
@@ -1692,7 +1829,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) GetWorkspaceScope() *datamodelv1.WorkspaceSelector {
|
||||
@@ -1718,7 +1855,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)
|
||||
}
|
||||
@@ -1730,7 +1867,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 {
|
||||
@@ -1743,7 +1880,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.
|
||||
@@ -1760,7 +1897,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)
|
||||
}
|
||||
@@ -1772,7 +1909,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 {
|
||||
@@ -1785,7 +1922,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 {
|
||||
@@ -1818,7 +1955,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)
|
||||
}
|
||||
@@ -1830,7 +1967,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 {
|
||||
@@ -1843,7 +1980,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 {
|
||||
@@ -1928,7 +2065,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)
|
||||
}
|
||||
@@ -1940,7 +2077,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 {
|
||||
@@ -1953,7 +2090,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 {
|
||||
@@ -2022,7 +2159,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)
|
||||
}
|
||||
@@ -2034,7 +2171,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 {
|
||||
@@ -2047,7 +2184,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 {
|
||||
@@ -2194,7 +2331,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)
|
||||
}
|
||||
@@ -2206,7 +2343,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 {
|
||||
@@ -2219,7 +2356,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 {
|
||||
@@ -2275,7 +2412,7 @@ var File_sandbox_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_sandbox_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x0fdatamodel.proto\"\xa8\x05\n" +
|
||||
"\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x0fdatamodel.proto\"\xd8\x05\n" +
|
||||
"\rSandboxPolicy\x12\x18\n" +
|
||||
"\aversion\x18\x01 \x01(\rR\aversion\x12F\n" +
|
||||
"\n" +
|
||||
@@ -2284,7 +2421,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" +
|
||||
@@ -2301,7 +2439,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" +
|
||||
@@ -2454,7 +2596,13 @@ const file_sandbox_proto_rawDesc = "" +
|
||||
"\x0frequest_timeout\x18h \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeout\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\x16allowInsecureTransportJ\x04\b\x04\x10\x05R\atimeout*\x97\x01\n" +
|
||||
"\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransportJ\x04\b\x04\x10\x05R\atimeout*\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*\x97\x01\n" +
|
||||
"\x0eNetworkTlsMode\x12 \n" +
|
||||
"\x1cNETWORK_TLS_MODE_UNSPECIFIED\x10\x00\x12\x19\n" +
|
||||
"\x15NETWORK_TLS_MODE_SKIP\x10\x01\x12\"\n" +
|
||||
@@ -2490,96 +2638,100 @@ func file_sandbox_proto_rawDescGZIP() []byte {
|
||||
return file_sandbox_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||
var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
|
||||
var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 6)
|
||||
var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 33)
|
||||
var file_sandbox_proto_goTypes = []any{
|
||||
(NetworkTlsMode)(0), // 0: openshell.sandbox.v1.NetworkTlsMode
|
||||
(NetworkEnforcementMode)(0), // 1: openshell.sandbox.v1.NetworkEnforcementMode
|
||||
(NetworkAccessPreset)(0), // 2: openshell.sandbox.v1.NetworkAccessPreset
|
||||
(SettingScope)(0), // 3: openshell.sandbox.v1.SettingScope
|
||||
(PolicySource)(0), // 4: openshell.sandbox.v1.PolicySource
|
||||
(*SandboxPolicy)(nil), // 5: openshell.sandbox.v1.SandboxPolicy
|
||||
(*FilesystemPolicy)(nil), // 6: openshell.sandbox.v1.FilesystemPolicy
|
||||
(*LandlockPolicy)(nil), // 7: openshell.sandbox.v1.LandlockPolicy
|
||||
(*ProcessPolicy)(nil), // 8: openshell.sandbox.v1.ProcessPolicy
|
||||
(*NetworkPolicyRule)(nil), // 9: openshell.sandbox.v1.NetworkPolicyRule
|
||||
(*NetworkMiddlewareConfig)(nil), // 10: openshell.sandbox.v1.NetworkMiddlewareConfig
|
||||
(*MiddlewareEndpointSelector)(nil), // 11: openshell.sandbox.v1.MiddlewareEndpointSelector
|
||||
(*NetworkCredentialBinding)(nil), // 12: openshell.sandbox.v1.NetworkCredentialBinding
|
||||
(*NetworkEndpoint)(nil), // 13: openshell.sandbox.v1.NetworkEndpoint
|
||||
(*McpOptions)(nil), // 14: openshell.sandbox.v1.McpOptions
|
||||
(*GraphqlOperation)(nil), // 15: openshell.sandbox.v1.GraphqlOperation
|
||||
(*L7DenyRule)(nil), // 16: openshell.sandbox.v1.L7DenyRule
|
||||
(*L7Rule)(nil), // 17: openshell.sandbox.v1.L7Rule
|
||||
(*L7Allow)(nil), // 18: openshell.sandbox.v1.L7Allow
|
||||
(*L7QueryMatcher)(nil), // 19: openshell.sandbox.v1.L7QueryMatcher
|
||||
(*NetworkBinary)(nil), // 20: openshell.sandbox.v1.NetworkBinary
|
||||
(*GetSandboxConfigRequest)(nil), // 21: openshell.sandbox.v1.GetSandboxConfigRequest
|
||||
(*GetGatewayConfigRequest)(nil), // 22: openshell.sandbox.v1.GetGatewayConfigRequest
|
||||
(*GetGatewayConfigResponse)(nil), // 23: openshell.sandbox.v1.GetGatewayConfigResponse
|
||||
(*SettingValue)(nil), // 24: openshell.sandbox.v1.SettingValue
|
||||
(*EffectiveSetting)(nil), // 25: openshell.sandbox.v1.EffectiveSetting
|
||||
(*GetSandboxConfigResponse)(nil), // 26: openshell.sandbox.v1.GetSandboxConfigResponse
|
||||
(*SupervisorMiddlewareService)(nil), // 27: openshell.sandbox.v1.SupervisorMiddlewareService
|
||||
nil, // 28: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry
|
||||
nil, // 29: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry
|
||||
nil, // 30: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry
|
||||
nil, // 31: openshell.sandbox.v1.L7DenyRule.QueryEntry
|
||||
nil, // 32: openshell.sandbox.v1.L7DenyRule.ParamsEntry
|
||||
nil, // 33: openshell.sandbox.v1.L7Allow.QueryEntry
|
||||
nil, // 34: openshell.sandbox.v1.L7Allow.ParamsEntry
|
||||
nil, // 35: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry
|
||||
nil, // 36: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry
|
||||
(*structpb.Struct)(nil), // 37: google.protobuf.Struct
|
||||
(*datamodelv1.WorkspaceSelector)(nil), // 38: openshell.datamodel.v1.WorkspaceSelector
|
||||
(*durationpb.Duration)(nil), // 39: google.protobuf.Duration
|
||||
(UiClipboardAccess)(0), // 0: openshell.sandbox.v1.UiClipboardAccess
|
||||
(NetworkTlsMode)(0), // 1: openshell.sandbox.v1.NetworkTlsMode
|
||||
(NetworkEnforcementMode)(0), // 2: openshell.sandbox.v1.NetworkEnforcementMode
|
||||
(NetworkAccessPreset)(0), // 3: openshell.sandbox.v1.NetworkAccessPreset
|
||||
(SettingScope)(0), // 4: openshell.sandbox.v1.SettingScope
|
||||
(PolicySource)(0), // 5: openshell.sandbox.v1.PolicySource
|
||||
(*SandboxPolicy)(nil), // 6: openshell.sandbox.v1.SandboxPolicy
|
||||
(*FilesystemPolicy)(nil), // 7: openshell.sandbox.v1.FilesystemPolicy
|
||||
(*LandlockPolicy)(nil), // 8: openshell.sandbox.v1.LandlockPolicy
|
||||
(*ProcessPolicy)(nil), // 9: openshell.sandbox.v1.ProcessPolicy
|
||||
(*UiPolicy)(nil), // 10: openshell.sandbox.v1.UiPolicy
|
||||
(*NetworkPolicyRule)(nil), // 11: openshell.sandbox.v1.NetworkPolicyRule
|
||||
(*NetworkMiddlewareConfig)(nil), // 12: openshell.sandbox.v1.NetworkMiddlewareConfig
|
||||
(*MiddlewareEndpointSelector)(nil), // 13: openshell.sandbox.v1.MiddlewareEndpointSelector
|
||||
(*NetworkCredentialBinding)(nil), // 14: openshell.sandbox.v1.NetworkCredentialBinding
|
||||
(*NetworkEndpoint)(nil), // 15: openshell.sandbox.v1.NetworkEndpoint
|
||||
(*McpOptions)(nil), // 16: openshell.sandbox.v1.McpOptions
|
||||
(*GraphqlOperation)(nil), // 17: openshell.sandbox.v1.GraphqlOperation
|
||||
(*L7DenyRule)(nil), // 18: openshell.sandbox.v1.L7DenyRule
|
||||
(*L7Rule)(nil), // 19: openshell.sandbox.v1.L7Rule
|
||||
(*L7Allow)(nil), // 20: openshell.sandbox.v1.L7Allow
|
||||
(*L7QueryMatcher)(nil), // 21: openshell.sandbox.v1.L7QueryMatcher
|
||||
(*NetworkBinary)(nil), // 22: openshell.sandbox.v1.NetworkBinary
|
||||
(*GetSandboxConfigRequest)(nil), // 23: openshell.sandbox.v1.GetSandboxConfigRequest
|
||||
(*GetGatewayConfigRequest)(nil), // 24: openshell.sandbox.v1.GetGatewayConfigRequest
|
||||
(*GetGatewayConfigResponse)(nil), // 25: openshell.sandbox.v1.GetGatewayConfigResponse
|
||||
(*SettingValue)(nil), // 26: openshell.sandbox.v1.SettingValue
|
||||
(*EffectiveSetting)(nil), // 27: openshell.sandbox.v1.EffectiveSetting
|
||||
(*GetSandboxConfigResponse)(nil), // 28: openshell.sandbox.v1.GetSandboxConfigResponse
|
||||
(*SupervisorMiddlewareService)(nil), // 29: openshell.sandbox.v1.SupervisorMiddlewareService
|
||||
nil, // 30: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry
|
||||
nil, // 31: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry
|
||||
nil, // 32: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry
|
||||
nil, // 33: openshell.sandbox.v1.L7DenyRule.QueryEntry
|
||||
nil, // 34: openshell.sandbox.v1.L7DenyRule.ParamsEntry
|
||||
nil, // 35: openshell.sandbox.v1.L7Allow.QueryEntry
|
||||
nil, // 36: openshell.sandbox.v1.L7Allow.ParamsEntry
|
||||
nil, // 37: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry
|
||||
nil, // 38: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry
|
||||
(*structpb.Struct)(nil), // 39: google.protobuf.Struct
|
||||
(*datamodelv1.WorkspaceSelector)(nil), // 40: openshell.datamodel.v1.WorkspaceSelector
|
||||
(*durationpb.Duration)(nil), // 41: google.protobuf.Duration
|
||||
}
|
||||
var file_sandbox_proto_depIdxs = []int32{
|
||||
6, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy
|
||||
7, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy
|
||||
8, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy
|
||||
28, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry
|
||||
29, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry
|
||||
13, // 5: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint
|
||||
20, // 6: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary
|
||||
37, // 7: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct
|
||||
11, // 8: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector
|
||||
0, // 9: openshell.sandbox.v1.NetworkEndpoint.tls:type_name -> openshell.sandbox.v1.NetworkTlsMode
|
||||
1, // 10: openshell.sandbox.v1.NetworkEndpoint.enforcement:type_name -> openshell.sandbox.v1.NetworkEnforcementMode
|
||||
2, // 11: openshell.sandbox.v1.NetworkEndpoint.access:type_name -> openshell.sandbox.v1.NetworkAccessPreset
|
||||
17, // 12: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule
|
||||
16, // 13: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule
|
||||
30, // 14: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry
|
||||
14, // 15: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions
|
||||
12, // 16: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding
|
||||
31, // 17: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry
|
||||
32, // 18: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry
|
||||
18, // 19: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow
|
||||
33, // 20: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry
|
||||
34, // 21: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry
|
||||
38, // 22: openshell.sandbox.v1.GetSandboxConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector
|
||||
35, // 23: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry
|
||||
24, // 24: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue
|
||||
3, // 25: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope
|
||||
5, // 26: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy
|
||||
36, // 27: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry
|
||||
4, // 28: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource
|
||||
27, // 29: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService
|
||||
39, // 30: openshell.sandbox.v1.SupervisorMiddlewareService.request_timeout:type_name -> google.protobuf.Duration
|
||||
9, // 31: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule
|
||||
10, // 32: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig
|
||||
15, // 33: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation
|
||||
19, // 34: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
|
||||
19, // 35: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
|
||||
19, // 36: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
|
||||
19, // 37: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
|
||||
24, // 38: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue
|
||||
25, // 39: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting
|
||||
40, // [40:40] is the sub-list for method output_type
|
||||
40, // [40:40] is the sub-list for method input_type
|
||||
40, // [40:40] is the sub-list for extension type_name
|
||||
40, // [40:40] is the sub-list for extension extendee
|
||||
0, // [0:40] is the sub-list for field type_name
|
||||
7, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy
|
||||
8, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy
|
||||
9, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy
|
||||
30, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry
|
||||
31, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry
|
||||
10, // 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
|
||||
15, // 7: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint
|
||||
22, // 8: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary
|
||||
39, // 9: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct
|
||||
13, // 10: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector
|
||||
1, // 11: openshell.sandbox.v1.NetworkEndpoint.tls:type_name -> openshell.sandbox.v1.NetworkTlsMode
|
||||
2, // 12: openshell.sandbox.v1.NetworkEndpoint.enforcement:type_name -> openshell.sandbox.v1.NetworkEnforcementMode
|
||||
3, // 13: openshell.sandbox.v1.NetworkEndpoint.access:type_name -> openshell.sandbox.v1.NetworkAccessPreset
|
||||
19, // 14: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule
|
||||
18, // 15: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule
|
||||
32, // 16: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry
|
||||
16, // 17: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions
|
||||
14, // 18: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding
|
||||
33, // 19: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry
|
||||
34, // 20: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry
|
||||
20, // 21: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow
|
||||
35, // 22: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry
|
||||
36, // 23: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry
|
||||
40, // 24: openshell.sandbox.v1.GetSandboxConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector
|
||||
37, // 25: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry
|
||||
26, // 26: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue
|
||||
4, // 27: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope
|
||||
6, // 28: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy
|
||||
38, // 29: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry
|
||||
5, // 30: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource
|
||||
29, // 31: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService
|
||||
41, // 32: openshell.sandbox.v1.SupervisorMiddlewareService.request_timeout:type_name -> google.protobuf.Duration
|
||||
11, // 33: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule
|
||||
12, // 34: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig
|
||||
17, // 35: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation
|
||||
21, // 36: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
|
||||
21, // 37: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
|
||||
21, // 38: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
|
||||
21, // 39: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
|
||||
26, // 40: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue
|
||||
27, // 41: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting
|
||||
42, // [42:42] is the sub-list for method output_type
|
||||
42, // [42:42] is the sub-list for method input_type
|
||||
42, // [42:42] is the sub-list for extension type_name
|
||||
42, // [42:42] is the sub-list for extension extendee
|
||||
0, // [0:42] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_sandbox_proto_init() }
|
||||
@@ -2587,8 +2739,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),
|
||||
@@ -2599,8 +2751,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: 5,
|
||||
NumMessages: 32,
|
||||
NumEnums: 6,
|
||||
NumMessages: 33,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -258,6 +259,27 @@ 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 requires the selected compute driver to
|
||||
advertise `openshell.policy.ui.v1` through existing extension metadata. A driver
|
||||
without that capability 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:
|
||||
@@ -389,6 +411,7 @@ Before presenting the policy to the user, verify correctness **and** flag breadt
|
||||
- [ ] Any required WebSocket control advertises `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`, and the user understands that V1 does not inspect binary messages
|
||||
- [ ] Any required response control advertises `HTTP_RESPONSE/PRE_RETURN`
|
||||
- [ ] 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)
|
||||
|
||||
@@ -451,7 +474,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**:
|
||||
@@ -470,7 +493,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
|
||||
|
||||
|
||||
@@ -747,7 +747,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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -499,7 +499,13 @@ 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 activation: `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 activation: `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. Drivers without the standard supervisor fetch revisions through the sandbox configuration API and report whether they loaded them.
|
||||
|
||||
Explicit `ui` requires the selected driver to advertise
|
||||
`openshell.policy.ui.v1` in its existing supported extension capabilities,
|
||||
visible through `openshell gateway info`. Unsupported drivers reject even
|
||||
`ui: {}` before provisioning. Omit UI unless requested. UI changes require
|
||||
sandbox recreation.
|
||||
|
||||
If startup reports `ConfigurationInvalid`, inspect `openshell sandbox get` and
|
||||
repair the complete policy or provider set through the gateway. The workload
|
||||
|
||||
Reference in New Issue
Block a user