Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/openshell-core/src/driver_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub const LABEL_SANDBOX_NAME: &str = "openshell.ai/sandbox-name";
/// Container/pod label carrying the sandbox namespace.
pub const LABEL_SANDBOX_NAMESPACE: &str = "openshell.ai/sandbox-namespace";

/// Container/pod label carrying the sandbox workspace.
pub const LABEL_SANDBOX_WORKSPACE: &str = "openshell.ai/sandbox-workspace";

// ---------------------------------------------------------------------------

/// Path to the sandbox supervisor binary inside the container image.
Expand Down
80 changes: 73 additions & 7 deletions crates/openshell-core/src/grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,36 @@ mod auth_tests {
}
}

#[cfg(test)]
mod workspace_tests {
use super::*;

#[test]
fn cached_client_workspace_defaults_to_empty_before_poll() {
let client_ws: Arc<tokio::sync::OnceCell<String>> = Arc::new(tokio::sync::OnceCell::new());
assert_eq!(client_ws.get().cloned().unwrap_or_default(), "");
}

#[test]
fn cached_client_workspace_returns_learned_value() {
let client_ws: Arc<tokio::sync::OnceCell<String>> = Arc::new(tokio::sync::OnceCell::new());
let _ = client_ws.set("beta".to_string());
assert_eq!(client_ws.get().cloned().unwrap_or_default(), "beta");
}

#[test]
fn cached_client_workspace_is_set_once() {
let client_ws: Arc<tokio::sync::OnceCell<String>> = Arc::new(tokio::sync::OnceCell::new());
let _ = client_ws.set("beta".to_string());
let _ = client_ws.set("gamma".to_string());
assert_eq!(
client_ws.get().cloned().unwrap_or_default(),
"beta",
"workspace should not change after first poll"
);
}
}

/// Connect to the `OpenShell` server.
async fn connect(endpoint: &str) -> Result<OpenShellClient<AuthedChannel>> {
let channel = connect_channel(endpoint).await?;
Expand Down Expand Up @@ -618,12 +648,19 @@ async fn sync_policy_with_client(
client: &mut OpenShellClient<AuthedChannel>,
sandbox: &str,
policy: &ProtoSandboxPolicy,
workspace: &str,
) -> Result<()> {
client
.update_config(UpdateConfigRequest {
name: sandbox.to_string(),
policy: Some(policy.clone()),
..Default::default()
setting_key: String::new(),
setting_value: None,
delete_setting: false,
global: false,
merge_operations: vec![],
expected_resource_version: 0,
workspace: workspace.to_string(),
})
.await
.into_diagnostic()
Expand All @@ -641,6 +678,7 @@ pub async fn discover_and_sync_policy(
sandbox_id: &str,
sandbox: &str,
discovered_policy: &ProtoSandboxPolicy,
workspace: &str,
) -> Result<ProtoSandboxPolicy> {
debug!(
endpoint = %endpoint,
Expand All @@ -652,7 +690,7 @@ pub async fn discover_and_sync_policy(
let mut client = connect(endpoint).await?;

// Sync the discovered policy to the gateway.
sync_policy_with_client(&mut client, sandbox, discovered_policy).await?;
sync_policy_with_client(&mut client, sandbox, discovered_policy, workspace).await?;

// Re-fetch from the gateway to get the canonical version/hash.
fetch_policy_with_client(&mut client, sandbox_id)
Expand All @@ -666,10 +704,15 @@ pub async fn discover_and_sync_policy(
///
/// Used by the supervisor to push baseline-path-enriched policies so the
/// gateway stores the effective policy users see via `openshell sandbox get`.
pub async fn sync_policy(endpoint: &str, sandbox: &str, policy: &ProtoSandboxPolicy) -> Result<()> {
pub async fn sync_policy(
endpoint: &str,
sandbox: &str,
policy: &ProtoSandboxPolicy,
workspace: &str,
) -> Result<()> {
debug!(endpoint = %endpoint, sandbox = %sandbox, "Syncing enriched policy to gateway");
let mut client = connect(endpoint).await?;
sync_policy_with_client(&mut client, sandbox, policy).await
sync_policy_with_client(&mut client, sandbox, policy, workspace).await
}

/// Sync an enriched policy and return the authoritative revision snapshot.
Expand All @@ -678,9 +721,10 @@ pub async fn sync_policy_and_fetch_snapshot(
sandbox_id: &str,
sandbox: &str,
policy: &ProtoSandboxPolicy,
workspace: &str,
) -> Result<SettingsPollResult> {
let mut client = connect(endpoint).await?;
sync_policy_with_client(&mut client, sandbox, policy).await?;
sync_policy_with_client(&mut client, sandbox, policy, workspace).await?;
fetch_settings_snapshot_with_client(&mut client, sandbox_id).await
}

Expand Down Expand Up @@ -720,6 +764,7 @@ pub async fn fetch_provider_environment(
#[derive(Clone)]
pub struct CachedOpenShellClient {
client: OpenShellClient<AuthedChannel>,
workspace: Arc<tokio::sync::OnceCell<String>>,
}

/// Settings poll result returned by [`CachedOpenShellClient::poll_settings`].
Expand All @@ -735,6 +780,8 @@ pub struct SettingsPollResult {
/// When `policy_source` is `Global`, the version of the global policy revision.
pub global_policy_version: u32,
pub provider_env_revision: u64,
/// Workspace the sandbox belongs to.
pub workspace: String,
}

fn settings_poll_result(inner: crate::proto::GetSandboxConfigResponse) -> SettingsPollResult {
Expand All @@ -748,6 +795,7 @@ fn settings_poll_result(inner: crate::proto::GetSandboxConfigResponse) -> Settin
settings: inner.settings,
global_policy_version: inner.global_policy_version,
provider_env_revision: inner.provider_env_revision,
workspace: inner.workspace,
}
}

Expand All @@ -762,7 +810,10 @@ impl CachedOpenShellClient {
pub async fn connect(endpoint: &str) -> Result<Self> {
debug!(endpoint = %endpoint, "Connecting openshell gRPC client for policy polling");
let client = connect(endpoint).await?;
Ok(Self { client })
Ok(Self {
client,
workspace: Arc::new(tokio::sync::OnceCell::new()),
})
}

/// Get a clone of the underlying tonic client for direct RPC calls.
Expand All @@ -781,7 +832,20 @@ impl CachedOpenShellClient {
.await
.into_diagnostic()?;

Ok(settings_poll_result(response.into_inner()))
let result = settings_poll_result(response.into_inner());
let _ = self.workspace.set(result.workspace.clone());
Ok(result)
}

/// Returns the workspace learned from the server, or empty if not yet polled.
pub fn workspace(&self) -> String {
self.workspace.get().cloned().unwrap_or_default()
}

/// Pre-seed the workspace without polling. The value is ignored if the
/// workspace was already learned from `poll_settings`.
pub fn set_workspace(&self, workspace: String) {
let _ = self.workspace.set(workspace);
}

/// Submit denial summaries and/or agent-authored proposals for policy analysis.
Expand All @@ -807,6 +871,7 @@ impl CachedOpenShellClient {
proposed_chunks,
network_activity_summaries,
analysis_mode: analysis_mode.to_string(),
workspace: self.workspace(),
})
.await
.into_diagnostic()?;
Expand All @@ -829,6 +894,7 @@ impl CachedOpenShellClient {
.get_draft_policy(GetDraftPolicyRequest {
name: sandbox_name.to_string(),
status_filter: status_filter.to_string(),
workspace: self.workspace(),
})
.await
.into_diagnostic()?;
Expand Down
4 changes: 3 additions & 1 deletion crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ pub use config::{
MtlsAuthConfig, OidcConfig, TlsConfig,
};
pub use error::{ComputeDriverError, Error, Result};
pub use metadata::{GetResourceVersion, ObjectId, ObjectLabels, ObjectName, SetResourceVersion};
pub use metadata::{
GetResourceVersion, ObjectId, ObjectLabels, ObjectName, ObjectWorkspace, SetResourceVersion,
};

/// Build version string derived from git metadata.
///
Expand Down
Loading
Loading