diff --git a/architecture/gateway.md b/architecture/gateway.md index 8771fd4c38..9a6c49b28a 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -56,13 +56,18 @@ cross-origin or sibling-subdomain request. Public workspace-scoped RPCs carry a typed `WorkspaceSelector`. A request must select one non-empty workspace explicitly; `default` is an ordinary explicit -name, not an omitted-value fallback. Sandbox, sandbox template, provider, and -service list RPCs also accept an all-workspaces marker after Platform Admin -authorization. Single-workspace handlers reject that marker. Platform-global -policy operations require the selector to be absent, while workspace policy -operations require it. The gateway authorizes the selected scope before -performing resource lookup so malformed, unsupported, and unauthorized scopes -have consistent behavior across resource types. +name, not an omitted-value fallback. Every public sandbox-scoped RPC identifies +the sandbox with a string `sandbox` and carries its workspace selector as +a separate request field. Canonical sandbox IDs remain internal metadata used +at authentication, persistence, and compute-driver boundaries; public callers +do not use them as sandbox references. The gateway resolves the name to the +persisted sandbox record and authorizes that record's workspace. Missing and +unauthorized references use the same response within each principal class so +the resolver does not expose an object-existence oracle. Sandbox, sandbox +template, provider, and service list RPCs also accept an all-workspaces marker +after Platform Admin authorization. Single-workspace handlers reject that +marker. Platform-global policy operations require `sandbox` and +`workspace_scope` to be absent, while sandbox policy operations require both. Docker and Podman report the local address through which their sandboxes can reach the gateway. When the primary listener covers that address, the gateway @@ -329,7 +334,7 @@ Compute-driver, credential-driver, gateway-interceptor, and supervisor-middleware services are compiled contracts for internal extension boundaries, not public gateway RPCs. The current public inventory has 74 methods, 278 messages, and 12 enums -(`0f14943574349d02bdc61076c8c5a59a98b627325564ef1a6d21d7941825dc46`). +(`6c803d61db1b667d78a6fd7e781316e681bf9093eaef7010f07cb3b9aa672ccb`). Storage-only messages live in the private, versioned `openshell.storage.v1` package under `crates/openshell-server/proto`. The server diff --git a/crates/openshell-cli/src/commands/provider.rs b/crates/openshell-cli/src/commands/provider.rs index b85fef0013..7a8bc20789 100644 --- a/crates/openshell-cli/src/commands/provider.rs +++ b/crates/openshell-cli/src/commands/provider.rs @@ -58,7 +58,7 @@ pub async fn sandbox_provider_list( let mut client = grpc_client(server, tls).await?; let response = client .list_sandbox_providers(ListSandboxProvidersRequest { - sandbox_name: name.to_string(), + sandbox: (name).to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -90,7 +90,7 @@ pub async fn sandbox_provider_attach( // Fetch current sandbox to get resource_version for CAS let sandbox = client .get_sandbox(GetSandboxRequest { - name: name.to_string(), + sandbox: (name).to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -103,10 +103,10 @@ pub async fn sandbox_provider_attach( let response = match client .attach_sandbox_provider(AttachSandboxProviderRequest { - sandbox_name: name.to_string(), + sandbox: (name).to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), provider_name: provider.to_string(), expected_resource_version: resource_version, - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -146,7 +146,7 @@ pub async fn sandbox_provider_detach( // Fetch current sandbox to get resource_version for CAS let sandbox = client .get_sandbox(GetSandboxRequest { - name: name.to_string(), + sandbox: (name).to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -159,10 +159,10 @@ pub async fn sandbox_provider_detach( let response = match client .detach_sandbox_provider(DetachSandboxProviderRequest { - sandbox_name: name.to_string(), + sandbox: (name).to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), provider_name: provider.to_string(), expected_resource_version: resource_version, - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index bffd137c09..d715acb80f 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -644,7 +644,7 @@ enum Commands { /// Two mutually exclusive modes: /// /// **Token mode** (used internally by `sandbox connect`): - /// `openshell ssh-proxy --gateway --sandbox-id --token ` + /// `openshell ssh-proxy --gateway --sandbox --token ` /// /// **Name mode** (for use in `~/.ssh/config`): /// `openshell ssh-proxy --gateway --name ` @@ -655,9 +655,9 @@ enum Commands { #[arg(long, short = 'g')] gateway: Option, - /// Sandbox id. Required in token mode. + /// Sandbox name. Required in token mode. #[arg(long)] - sandbox_id: Option, + sandbox: Option, /// SSH session token. Required in token mode. #[arg(long)] @@ -2141,7 +2141,7 @@ enum ServiceCommands { page_token: String, /// List services across all workspaces (overrides --workspace). - #[arg(long)] + #[arg(long, conflicts_with = "sandbox")] all_workspaces: bool, /// Output format. @@ -3785,15 +3785,15 @@ async fn run_async() -> Result<()> { } Some(Commands::SshProxy { gateway, - sandbox_id, + sandbox, token, server, gateway_name, name, }) => { - match (gateway, sandbox_id, token, server, gateway_name, name) { + match (gateway, sandbox, token, server, gateway_name, name) { // Token mode (existing behavior): pre-created session credentials. - (Some(gw), Some(sid), Some(tok), _, gateway_name_opt, _) => { + (Some(gw), Some(sandbox), Some(tok), _, gateway_name_opt, _) => { let mut effective_tls = match gateway_name_opt { Some(ref g) => tls.with_gateway_name(g), None => tls, @@ -3801,7 +3801,7 @@ async fn run_async() -> Result<()> { if let Some(ref g) = gateway_name_opt { apply_auth(&mut effective_tls, g)?; } - run::sandbox_ssh_proxy(&gw, &sid, &tok, &effective_tls).await?; + run::sandbox_ssh_proxy(&gw, &sandbox, &tok, &effective_tls).await?; } // Name mode with --gateway-name: resolve endpoint from metadata. (_, _, _, server_override, Some(g), Some(n)) => { @@ -3827,7 +3827,7 @@ async fn run_async() -> Result<()> { } _ => { return Err(miette::miette!( - "provide either --gateway/--sandbox-id/--token or --gateway-name/--name (or --server/--name)" + "provide either --gateway/--sandbox/--token or --gateway-name/--name (or --server/--name)" )); } } @@ -4477,8 +4477,8 @@ mod tests { "ssh-proxy", "--gateway", "https://gw.example.com:8080/proxy/connect", - "--sandbox-id", - "sbx-123", + "--sandbox", + "my-box", "--token", "tok-abc", "--gateway-name", @@ -4489,7 +4489,7 @@ mod tests { match cli.command { Some(Commands::SshProxy { gateway, - sandbox_id, + sandbox, token, gateway_name, .. @@ -4499,7 +4499,7 @@ mod tests { Some("https://gw.example.com:8080/proxy/connect"), "gateway URL must land in SshProxy.gateway, not the global flag" ); - assert_eq!(sandbox_id.as_deref(), Some("sbx-123")); + assert_eq!(sandbox.as_deref(), Some("my-box")); assert_eq!(token.as_deref(), Some("tok-abc")); assert_eq!(gateway_name.as_deref(), Some("my-gateway")); } @@ -6066,6 +6066,19 @@ mod tests { } } + #[test] + fn service_list_rejects_sandbox_with_all_workspaces() { + let result = Cli::try_parse_from([ + "openshell", + "service", + "list", + "my-sandbox", + "--all-workspaces", + ]); + + assert!(result.is_err()); + } + #[test] fn service_get_accepts_optional_service_name() { let cli = Cli::try_parse_from(["openshell", "service", "get", "my-sandbox", "api"]) diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index a89e3c30b3..2547cf103a 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -686,10 +686,10 @@ pub async fn sandbox_create( let setting = parse_cli_setting_value(settings::PROPOSAL_APPROVAL_MODE_KEY, approval_mode)?; match client .update_config(UpdateConfigRequest { - name: sandbox_name.clone(), + sandbox: sandbox_name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), setting_key: settings::PROPOSAL_APPROVAL_MODE_KEY.to_string(), setting_value: Some(setting), - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), ..Default::default() }) .await @@ -745,14 +745,12 @@ pub async fn sandbox_create( // a newly created sandbox. Instead we handle termination client-side: // we wait until we have observed at least one non-Ready phase followed // by Ready (a genuine Provisioning → Ready transition). - let sandbox_id = if sandbox.object_id().is_empty() { - "unknown".to_string() - } else { - sandbox.object_id().to_string() - }; + let sandbox_name = sandbox.object_name().to_string(); + let sandbox_workspace = sandbox.object_workspace().to_string(); let mut stream = client .watch_sandbox(WatchSandboxRequest { - id: sandbox_id.clone(), + sandbox: sandbox_name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(sandbox_workspace)), follow_status: true, follow_logs: true, follow_events: true, @@ -1494,7 +1492,7 @@ pub async fn sandbox_get( let response = client .get_sandbox(GetSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -1504,14 +1502,11 @@ pub async fn sandbox_get( .sandbox .ok_or_else(|| miette::miette!("sandbox missing from response"))?; - let sandbox_id = if sandbox.object_id().is_empty() { - return Err(miette::miette!("sandbox missing metadata")); - } else { - sandbox.object_id().to_string() - }; - let config = client - .get_sandbox_config(GetSandboxConfigRequest { sandbox_id }) + .get_sandbox_config(GetSandboxConfigRequest { + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + }) .await .into_diagnostic()? .into_inner(); @@ -1656,7 +1651,7 @@ pub async fn sandbox_exec_grpc( // Resolve sandbox name to id. let sandbox = client .get_sandbox(GetSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -1725,7 +1720,8 @@ pub async fn sandbox_exec_grpc( // Make the streaming gRPC call. let mut stream = client .exec_sandbox(ExecSandboxRequest { - sandbox_id: sandbox.object_id().to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), command: command.to_vec(), workdir: workdir.unwrap_or_default().to_string(), environment: environment.clone(), @@ -1791,7 +1787,7 @@ pub async fn service_forward_tcp( let (bind_addr, bind_port) = parse_tcp_forward_spec(local, target_port)?; let mut client = grpc_client(server, tls).await?; - let sandbox = fetch_ready_sandbox_for_forward(&mut client, name, workspace).await?; + fetch_ready_sandbox_for_forward(&mut client, name, workspace).await?; let listener = tokio::net::TcpListener::bind((bind_addr.as_str(), bind_port)) .await @@ -1810,7 +1806,8 @@ pub async fn service_forward_tcp( name, ); - let sandbox_id = sandbox.object_id().to_string(); + let sandbox_name = name.to_string(); + let sandbox_workspace = workspace.to_string(); let (fatal_tx, mut fatal_rx) = tokio::sync::mpsc::channel::(1); let mut health_check = tokio::time::interval(Duration::from_secs(2)); health_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); @@ -1830,12 +1827,17 @@ pub async fn service_forward_tcp( .wrap_err("failed to accept local forward connection")?; set_tcp_nodelay_best_effort(&socket); let mut client = client.clone(); - let sandbox_id = sandbox_id.clone(); + let sandbox_name = sandbox_name.clone(); + let sandbox_workspace = sandbox_workspace.clone(); let target_host = target_host.to_string(); let service_id = format!("service-forward:{name}:{target_host}:{target_port}"); let fatal_tx = fatal_tx.clone(); tokio::spawn(async move { - let token = match create_forward_session_token(&mut client, &sandbox_id).await { + let token = match create_forward_session_token( + &mut client, + &sandbox_name, + &sandbox_workspace, + ).await { Ok(token) => token, Err(err) => { tracing::warn!(peer = %peer, error = %err, "service forward session creation failed"); @@ -1848,7 +1850,8 @@ pub async fn service_forward_tcp( if let Err(err) = forward_one_tcp_connection( &mut client, socket, - sandbox_id, + sandbox_name, + sandbox_workspace, target_host, target_port, service_id, @@ -1872,11 +1875,13 @@ pub async fn service_forward_tcp( async fn create_forward_session_token( client: &mut crate::tls::GrpcClient, - sandbox_id: &str, + sandbox_name: &str, + workspace: &str, ) -> std::result::Result { let response = client .create_ssh_session(CreateSshSessionRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .map_err(ForwardTcpConnectionError::from_status)?; @@ -1890,7 +1895,7 @@ async fn fetch_ready_sandbox_for_forward( ) -> Result { let response = match client .get_sandbox(GetSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -1973,10 +1978,12 @@ fn parse_tcp_forward_spec(local: Option<&str>, default_port: u16) -> Result<(Str Ok(("127.0.0.1".to_string(), port)) } +#[allow(clippy::too_many_arguments)] // one connection's sandbox, target, and authorization context async fn forward_one_tcp_connection( client: &mut crate::tls::GrpcClient, socket: tokio::net::TcpStream, - sandbox_id: String, + sandbox_name: String, + workspace: String, target_host: String, target_port: u16, service_id: String, @@ -1989,7 +1996,8 @@ async fn forward_one_tcp_connection( tx.send(TcpForwardFrame { payload: Some(openshell_core::proto::tcp_forward_frame::Payload::Init( TcpForwardInit { - sandbox_id, + sandbox: sandbox_name, + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), service_id, target: Some(tcp_forward_init::Target::Tcp(TcpRelayTarget { host: target_host, @@ -2109,7 +2117,10 @@ async fn sandbox_exec_interactive_grpc( input_tx .send(ExecSandboxInput { payload: Some(exec_sandbox_input::Payload::Start(ExecSandboxRequest { - sandbox_id: sandbox.object_id().to_string(), + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + sandbox.object_workspace(), + )), command: command.to_vec(), workdir: workdir.unwrap_or_default().to_string(), environment: environment.clone(), @@ -3091,7 +3102,7 @@ pub async fn sandbox_delete( let response = match client .delete_sandbox(DeleteSandboxRequest { - name: name.clone(), + sandbox: name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -3143,7 +3154,7 @@ pub async fn sandbox_stop( let mut client = grpc_client(server, tls).await?; let sandbox = client .stop_sandbox(StopSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -3166,7 +3177,7 @@ pub async fn sandbox_start( let mut client = grpc_client(server, tls).await?; let sandbox = client .start_sandbox(StartSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -3200,10 +3211,12 @@ async fn wait_for_lifecycle_phase( .and_then(|value| value.parse().ok()) .unwrap_or(300), ); - let sandbox_id = sandbox.object_id().to_string(); + let sandbox_name = sandbox.object_name().to_string(); + let workspace = sandbox.object_workspace().to_string(); let mut stream = client .watch_sandbox(WatchSandboxRequest { - id: sandbox_id, + sandbox: sandbox_name, + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), follow_status: true, follow_logs: false, follow_events: false, @@ -3264,11 +3277,11 @@ pub async fn service_expose( let mut client = grpc_client(server, tls).await?; let response = client .expose_service(ExposeServiceRequest { - sandbox: sandbox.to_string(), + sandbox: (sandbox).to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), service: service.to_string(), target_port: u32::from(target_port), domain: true, - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .map_err(service_expose_status_error)? @@ -3368,9 +3381,9 @@ pub async fn service_get( let mut client = grpc_client(server, tls).await?; let response = client .get_service(GetServiceRequest { - sandbox: sandbox.to_string(), - service: service.to_string(), + sandbox: (sandbox).to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + service: service.to_string(), }) .await .map_err(|status| service_status_error("get service", "sandbox:read", status))? @@ -3390,9 +3403,9 @@ pub async fn service_delete( let mut client = grpc_client(server, tls).await?; let response = client .delete_service(DeleteServiceRequest { - sandbox: sandbox.to_string(), - service: service.to_string(), + sandbox: (sandbox).to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + service: service.to_string(), }) .await .map_err(|status| service_status_error("delete service", "sandbox:write", status))? @@ -4204,7 +4217,6 @@ pub async fn sandbox_policy_set_global( let mut client = grpc_client(server, tls).await?; let response = client .update_config(UpdateConfigRequest { - name: String::new(), policy: Some(policy), global: true, ..Default::default() @@ -4234,20 +4246,10 @@ pub async fn sandbox_settings_get( tls: &TlsOptions, ) -> Result<()> { let mut client = grpc_client(server, tls).await?; - let sandbox = client - .get_sandbox(GetSandboxRequest { - name: name.to_string(), - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), - }) - .await - .into_diagnostic()? - .into_inner() - .sandbox - .ok_or_else(|| miette::miette!("sandbox not found"))?; - let response = client .get_sandbox_config(GetSandboxConfigRequest { - sandbox_id: sandbox.object_id().to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -4404,7 +4406,6 @@ pub async fn gateway_setting_set( let mut client = grpc_client(server, tls).await?; let response = client .update_config(UpdateConfigRequest { - name: String::new(), setting_key: key.to_string(), setting_value: Some(setting_value), global: true, @@ -4437,10 +4438,10 @@ pub async fn sandbox_setting_set( let mut client = grpc_client(server, tls).await?; let response = client .update_config(UpdateConfigRequest { - name: name.to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), setting_key: key.to_string(), setting_value: Some(setting_value), - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), ..Default::default() }) .await @@ -4470,7 +4471,6 @@ pub async fn gateway_setting_delete( let mut client = grpc_client(server, tls).await?; let response = client .update_config(UpdateConfigRequest { - name: String::new(), setting_key: key.to_string(), delete_setting: true, global: true, @@ -4503,10 +4503,10 @@ pub async fn sandbox_setting_delete( let mut client = grpc_client(server, tls).await?; let response = client .update_config(UpdateConfigRequest { - name: name.to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), setting_key: key.to_string(), delete_setting: true, - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), ..Default::default() }) .await @@ -4549,10 +4549,10 @@ pub async fn sandbox_policy_set( // Get current version so we can detect no-ops. let current_version = client .get_sandbox_policy_status(GetSandboxPolicyStatusRequest { - name: name.to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), version: 0, global: false, - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .ok() @@ -4561,9 +4561,9 @@ pub async fn sandbox_policy_set( let response = client .update_config(UpdateConfigRequest { - name: name.to_string(), - policy: Some(policy), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + policy: Some(policy), ..Default::default() }) .await @@ -4608,10 +4608,10 @@ pub async fn sandbox_policy_set( let status_resp = client .get_sandbox_policy_status(GetSandboxPolicyStatusRequest { - name: name.to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), version: resp.version, global: false, - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -4685,27 +4685,13 @@ pub async fn sandbox_policy_update( )?; let mut client = grpc_client(server, tls).await?; - let sandbox = client - .get_sandbox(GetSandboxRequest { - name: name.to_string(), + let current = client + .get_sandbox_config(GetSandboxConfigRequest { + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? - .into_inner() - .sandbox - .ok_or_else(|| miette!("sandbox not found"))?; - - let sandbox_id = if sandbox.object_id().is_empty() { - return Err(miette!("sandbox missing metadata")); - } else { - sandbox.object_id().to_string() - }; - - let current = client - .get_sandbox_config(GetSandboxConfigRequest { sandbox_id }) - .await - .into_diagnostic()? .into_inner(); if current.policy_source == PolicySource::Global as i32 { @@ -4735,9 +4721,9 @@ pub async fn sandbox_policy_update( let current_hash = current.policy_hash.clone(); let response = client .update_config(UpdateConfigRequest { - name: name.to_string(), - merge_operations: plan.merge_operations, + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + merge_operations: plan.merge_operations, ..Default::default() }) .await @@ -4782,10 +4768,10 @@ pub async fn sandbox_policy_update( let status_resp = client .get_sandbox_policy_status(GetSandboxPolicyStatusRequest { - name: name.to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), version: response.version, global: false, - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -4890,10 +4876,10 @@ where let status_resp = client .get_sandbox_policy_status(GetSandboxPolicyStatusRequest { - name: name.to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), version, global: false, - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -4972,24 +4958,10 @@ where let (stdout, _stderr) = writers; let mut client = grpc_client(server, tls).await?; - let sandbox = client - .get_sandbox(GetSandboxRequest { - name: name.to_string(), - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), - }) - .await - .into_diagnostic()? - .into_inner() - .sandbox - .ok_or_else(|| miette!("sandbox missing from response"))?; - let sandbox_id = sandbox.object_id(); - if sandbox_id.is_empty() { - return Err(miette!("sandbox missing metadata")); - } - let config = client .get_sandbox_config(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -5084,9 +5056,9 @@ pub async fn sandbox_policy_get_global( let status_resp = client .get_sandbox_policy_status(GetSandboxPolicyStatusRequest { - name: String::new(), version, global: true, + sandbox: String::new(), workspace_scope: None, }) .await @@ -5225,11 +5197,11 @@ pub async fn sandbox_policy_list( let resp = client .list_sandbox_policies(ListSandboxPoliciesRequest { - name: name.to_string(), + sandbox: name.to_string(), page_size, page_token: page_token.to_string(), - global: false, workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + global: false, }) .await .into_diagnostic()?; @@ -5270,10 +5242,10 @@ pub async fn sandbox_policy_list_global( let resp = client .list_sandbox_policies(ListSandboxPoliciesRequest { - name: String::new(), page_size, page_token: page_token.to_string(), global: true, + sandbox: String::new(), workspace_scope: None, }) .await @@ -5371,18 +5343,6 @@ pub async fn sandbox_logs( ) -> Result<()> { let mut client = grpc_client(server, tls).await?; - // Resolve sandbox name to id. - let sandbox = client - .get_sandbox(GetSandboxRequest { - name: name.to_string(), - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), - }) - .await - .into_diagnostic()? - .into_inner() - .sandbox - .ok_or_else(|| miette::miette!("sandbox not found"))?; - // Normalize "all" to empty list (server treats empty as "no filter"). let source_filter: Vec = sources .iter() @@ -5408,7 +5368,8 @@ pub async fn sandbox_logs( // Streaming mode: use WatchSandbox. let mut stream = client .watch_sandbox(WatchSandboxRequest { - id: sandbox.object_id().to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), follow_status: false, follow_logs: true, follow_events: false, @@ -5435,12 +5396,12 @@ pub async fn sandbox_logs( // One-shot mode: use GetSandboxLogs. let resp = client .get_sandbox_logs(GetSandboxLogsRequest { - sandbox_id: sandbox.object_id().to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), lines, since_ms, sources: source_filter, min_level: level.to_uppercase(), - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -5514,9 +5475,9 @@ pub async fn sandbox_draft_get( let response = client .get_draft_policy(GetDraftPolicyRequest { - name: name.to_string(), - status_filter: status_filter.unwrap_or("").to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + status_filter: status_filter.unwrap_or("").to_string(), }) .await .into_diagnostic()?; @@ -5622,9 +5583,9 @@ pub async fn sandbox_draft_approve( let mut client = grpc_client(server, tls).await?; let review_token = client .get_draft_policy(GetDraftPolicyRequest { - name: name.to_string(), - status_filter: String::new(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + status_filter: String::new(), }) .await .into_diagnostic()? @@ -5637,9 +5598,9 @@ pub async fn sandbox_draft_approve( let response = client .approve_draft_chunk(ApproveDraftChunkRequest { - name: name.to_string(), - chunk_id: chunk_id.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + chunk_id: chunk_id.to_string(), review_token, }) .await @@ -5669,10 +5630,10 @@ pub async fn sandbox_draft_reject( client .reject_draft_chunk(RejectDraftChunkRequest { - name: name.to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), chunk_id: chunk_id.to_string(), reason: reason.to_string(), - workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -5693,9 +5654,9 @@ pub async fn sandbox_draft_approve_all( let mut client = grpc_client(server, tls).await?; let approvals = client .get_draft_policy(GetDraftPolicyRequest { - name: name.to_string(), - status_filter: "pending".to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + status_filter: "pending".to_string(), }) .await .into_diagnostic()? @@ -5710,9 +5671,9 @@ pub async fn sandbox_draft_approve_all( let response = client .approve_all_draft_chunks(ApproveAllDraftChunksRequest { - name: name.to_string(), - include_security_flagged, + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + include_security_flagged, approvals, }) .await @@ -5741,7 +5702,7 @@ pub async fn sandbox_draft_clear( let response = client .clear_draft_chunks(ClearDraftChunksRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -5768,7 +5729,7 @@ pub async fn sandbox_draft_history( let response = client .get_draft_history(GetDraftHistoryRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await diff --git a/crates/openshell-cli/src/ssh.rs b/crates/openshell-cli/src/ssh.rs index c0937db484..b5a247c339 100644 --- a/crates/openshell-cli/src/ssh.rs +++ b/crates/openshell-cli/src/ssh.rs @@ -8,6 +8,7 @@ use crate::tls::{TlsOptions, grpc_client}; use miette::{IntoDiagnostic, Result, WrapErr}; #[cfg(unix)] use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction}; +use openshell_core::driver_mounts; use openshell_core::forward::{ ForwardSpec, build_proxy_command, format_gateway_url, resolve_ssh_gateway, shell_escape, validate_ssh_session_response, write_forward_pid, @@ -16,7 +17,6 @@ use openshell_core::proto::{ CreateSshSessionRequest, GetSandboxRequest, SshRelayTarget, TcpForwardFrame, TcpForwardInit, tcp_forward_init, }; -use openshell_core::{ObjectId, driver_mounts}; use std::fs; use std::future::Future; use std::io::{IsTerminal, Write}; @@ -74,6 +74,7 @@ impl Editor { struct SshSessionConfig { proxy_command: String, sandbox_id: String, + sandbox_name: String, gateway_url: String, token: String, main_terminal: bool, @@ -88,10 +89,10 @@ async fn ssh_session_config( ) -> Result { let mut client = grpc_client(server, tls).await?; - // Resolve sandbox name to id. + // Resolve the sandbox and retain its ID for local lifecycle tracking. let sandbox = client .get_sandbox(GetSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await @@ -105,7 +106,8 @@ async fn ssh_session_config( let response = loop { match client .create_ssh_session(CreateSshSessionRequest { - sandbox_id: sandbox.object_id().to_string(), + sandbox: name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -150,7 +152,7 @@ async fn ssh_session_config( let proxy_command = build_proxy_command( &exe_command, &gateway_url, - &session.sandbox_id, + name, &session.token, gateway_name, ); @@ -158,6 +160,7 @@ async fn ssh_session_config( Ok(SshSessionConfig { proxy_command, sandbox_id: session.sandbox_id.clone(), + sandbox_name: name.to_string(), gateway_url, token: session.token, main_terminal: sandbox.spec.as_ref().is_none_or(|spec| spec.tty), @@ -1425,7 +1428,7 @@ async fn sandbox_sync_down_directory( /// Run the SSH proxy, connecting stdin/stdout to the gateway. pub async fn sandbox_ssh_proxy( gateway_url: &str, - sandbox_id: &str, + sandbox_name: &str, token: &str, tls: &TlsOptions, ) -> Result<()> { @@ -1436,8 +1439,9 @@ pub async fn sandbox_ssh_proxy( tx.send(TcpForwardFrame { payload: Some(openshell_core::proto::tcp_forward_frame::Payload::Init( TcpForwardInit { - sandbox_id: sandbox_id.to_string(), - service_id: format!("ssh-proxy:{sandbox_id}"), + sandbox: sandbox_name.to_string(), + workspace_scope: None, + service_id: format!("ssh-proxy:{sandbox_name}"), target: Some(tcp_forward_init::Target::Ssh(SshRelayTarget {})), authorization_token: token.to_string(), }, @@ -1530,7 +1534,7 @@ pub async fn sandbox_ssh_proxy_by_name( let session = ssh_session_config(server, name, tls, workspace, None).await?; sandbox_ssh_proxy( &session.gateway_url, - &session.sandbox_id, + &session.sandbox_name, &session.token, tls, ) diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 5b8ae54333..b4d91c8b7f 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -174,7 +174,8 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { - let name = request.into_inner().name; + let request = request.into_inner(); + let name = request.sandbox; // Return a minimal sandbox with metadata for CAS operations Ok(Response::new(SandboxResponse { sandbox: Some(Sandbox { @@ -208,7 +209,8 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { - let sandbox_name = request.into_inner().sandbox_name; + let request = request.into_inner(); + let sandbox_name = request.sandbox.clone(); self.state .sandbox_provider_requests .lock() @@ -237,12 +239,13 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let request = request.into_inner(); + let sandbox_name = request.sandbox.clone(); self.state .sandbox_provider_requests .lock() .await .push(SandboxProviderRequestLog::Attach { - sandbox_name: request.sandbox_name.clone(), + sandbox_name: sandbox_name.clone(), provider_name: request.provider_name.clone(), }); if !self @@ -255,9 +258,7 @@ impl OpenShell for TestOpenShell { return Err(Status::failed_precondition("provider not found")); } let mut sandbox_providers = self.state.sandbox_providers.lock().await; - let providers = sandbox_providers - .entry(request.sandbox_name.clone()) - .or_default(); + let providers = sandbox_providers.entry(sandbox_name.clone()).or_default(); let attached = if providers.contains(&request.provider_name) { false } else { @@ -266,7 +267,7 @@ impl OpenShell for TestOpenShell { }; let sandbox = Sandbox { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { - name: request.sandbox_name, + name: sandbox_name, ..Default::default() }), spec: Some(openshell_core::proto::SandboxSpec { @@ -286,24 +287,23 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let request = request.into_inner(); + let sandbox_name = request.sandbox.clone(); self.state .sandbox_provider_requests .lock() .await .push(SandboxProviderRequestLog::Detach { - sandbox_name: request.sandbox_name.clone(), + sandbox_name: sandbox_name.clone(), provider_name: request.provider_name.clone(), }); let mut sandbox_providers = self.state.sandbox_providers.lock().await; - let providers = sandbox_providers - .entry(request.sandbox_name.clone()) - .or_default(); + let providers = sandbox_providers.entry(sandbox_name.clone()).or_default(); let before_len = providers.len(); providers.retain(|name| name != &request.provider_name); let detached = providers.len() != before_len; let sandbox = Sandbox { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { - name: request.sandbox_name, + name: sandbox_name, ..Default::default() }), spec: Some(openshell_core::proto::SandboxSpec { diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 5c24fc6695..b33f2e8515 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -182,7 +182,8 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { - let name = request.into_inner().name; + let request = request.into_inner(); + let name = request.sandbox; let mut sandbox = Sandbox { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("id-{name}"), @@ -333,7 +334,7 @@ impl OpenShell for TestOpenShell { .deleted_names .lock() .await - .push(vec![request.name.clone()]); + .push(vec![request.sandbox.clone()]); let delete_failure = self.state.fail_delete_sandbox_message.lock().await.take(); if let Some(message) = delete_failure { return Err(Status::internal(message)); @@ -387,7 +388,8 @@ impl OpenShell for TestOpenShell { { return Err(Status::failed_precondition("sandbox is not ready")); } - let sandbox_id = request.into_inner().sandbox_id; + let request = request.into_inner(); + let sandbox_id = format!("id-{}", request.sandbox); Ok(Response::new(CreateSshSessionResponse { sandbox_id, token: "test-token".to_string(), @@ -579,7 +581,8 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { - let sandbox_id = request.into_inner().id; + let request = request.into_inner(); + let sandbox_id = format!("id-{}", request.sandbox); let (tx, rx) = mpsc::channel(4); let vm_error_after_started = self.state.vm_error_after_started.load(Ordering::SeqCst); let vm_error_with_observed_exit = self @@ -1213,7 +1216,7 @@ for arg in "$@"; do if [ "$previous" = "-o" ]; then case "$arg" in ProxyCommand=*) - sandbox_id="$(printf '%s\n' "$arg" | sed -n 's/.*--sandbox-id \([^ ]*\).*/\1/p')" + sandbox_id="$(printf '%s\n' "$arg" | sed -n 's/.*--sandbox \([^ ]*\).*/\1/p')" ;; esac previous="" @@ -1257,8 +1260,8 @@ fi helper='@HELPER_PATH@' echo "$$" > '@PID_PATH@' -printf '%s\n' "ssh -N -o ProxyCommand=/tmp/openshell ssh-proxy --gateway https://127.0.0.1:9443 --sandbox-id $sandbox_id --token test-token --gateway-name test-gateway -o ExitOnForwardFailure=yes -L $forward sandbox" > '@COMMAND_PATH@' -exec env OPENSHELL_FAKE_FORWARD_MODE=listen "$helper" -N -o "ProxyCommand=/tmp/openshell ssh-proxy --gateway https://127.0.0.1:9443 --sandbox-id $sandbox_id --token test-token --gateway-name test-gateway" -o ExitOnForwardFailure=yes -L "$forward" sandbox +printf '%s\n' "ssh -N -o ProxyCommand=/tmp/openshell ssh-proxy --gateway https://127.0.0.1:9443 --sandbox $sandbox_id --token test-token --gateway-name test-gateway -o ExitOnForwardFailure=yes -L $forward sandbox" > '@COMMAND_PATH@' +exec env OPENSHELL_FAKE_FORWARD_MODE=listen "$helper" -N -o "ProxyCommand=/tmp/openshell ssh-proxy --gateway https://127.0.0.1:9443 --sandbox $sandbox_id --token test-token --gateway-name test-gateway" -o ExitOnForwardFailure=yes -L "$forward" sandbox "# .replace("@PID_PATH@", &pid_path.display().to_string()) .replace("@COMMAND_PATH@", &command_path.display().to_string()) @@ -1302,7 +1305,7 @@ for arg in "$@"; do if [ "$previous" = "-o" ]; then case "$arg" in ProxyCommand=*) - sandbox_id="$(printf '%s\n' "$arg" | sed -n 's/.*--sandbox-id \([^ ]*\).*/\1/p')" + sandbox_id="$(printf '%s\n' "$arg" | sed -n 's/.*--sandbox \([^ ]*\).*/\1/p')" ;; esac previous="" @@ -1331,7 +1334,7 @@ fi helper='@HELPER_PATH@' echo "$$" > '@PID_PATH@' -exec env OPENSHELL_FAKE_FORWARD_MODE=sleep "$helper" -N -o "ProxyCommand=/tmp/openshell ssh-proxy --gateway https://127.0.0.1:9443 --sandbox-id $sandbox_id --token test-token --gateway-name test-gateway" -o ExitOnForwardFailure=yes -L "$forward" sandbox >'@LOG_PATH@' 2>&1 +exec env OPENSHELL_FAKE_FORWARD_MODE=sleep "$helper" -N -o "ProxyCommand=/tmp/openshell ssh-proxy --gateway https://127.0.0.1:9443 --sandbox $sandbox_id --token test-token --gateway-name test-gateway" -o ExitOnForwardFailure=yes -L "$forward" sandbox >'@LOG_PATH@' 2>&1 "# .replace("@LOG_PATH@", &log_path.display().to_string()) .replace("@PID_PATH@", &pid_path.display().to_string()) diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index fdb0ebbd0d..3a38f4d731 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -117,7 +117,8 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { - let name = request.into_inner().name; + let request = request.into_inner(); + let name = request.sandbox.clone(); *self.state.last_get_name.lock().await = Some(name.clone()); Ok(Response::new(SandboxResponse { sandbox: Some(Sandbox { @@ -178,10 +179,7 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let req = request.into_inner(); - assert_eq!( - req.sandbox_id, "test-id", - "GetSandboxConfig should pass the id from GetSandbox" - ); + assert!(!req.sandbox.is_empty()); Ok(Response::new(GetSandboxConfigResponse { policy: Some(SandboxPolicy { version: 9, @@ -450,7 +448,7 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let req = request.into_inner(); - assert_eq!(req.name, "my-sandbox"); + assert_eq!(req.sandbox, "my-sandbox"); assert_eq!(req.version, 3); assert!(!req.global); diff --git a/crates/openshell-core/src/forward.rs b/crates/openshell-core/src/forward.rs index a17edbdee2..b30a21a71e 100644 --- a/crates/openshell-core/src/forward.rs +++ b/crates/openshell-core/src/forward.rs @@ -49,7 +49,7 @@ pub fn write_forward_pid( /// Find the PID of a backgrounded SSH forward by searching for the matching /// SSH process. Falls back to `pgrep` since SSH `-f` forks a new process /// whose PID we cannot capture directly. -pub fn find_ssh_forward_pid(sandbox_id: &str, port: u16) -> Option { +pub fn find_ssh_forward_pid(sandbox_name: &str, port: u16) -> Option { // Use pgrep only as a broad process source. The command line still needs a // second exact check before the PID can be tracked or signaled, otherwise a // requested port such as 80 can substring-match an existing 8080 forward. @@ -62,7 +62,7 @@ pub fn find_ssh_forward_pid(sandbox_id: &str, port: u16) -> Option { .lines() .rev() .filter_map(|l| l.trim().parse::().ok()) - .find(|pid| pid_matches_openshell_ssh_forward(*pid, port, Some(sandbox_id))) + .find(|pid| pid_matches_openshell_ssh_forward(*pid, port, Some(sandbox_name))) } /// Record read from a forward PID file. @@ -103,12 +103,12 @@ pub fn pid_is_alive(pid: u32) -> bool { } /// Validate that a PID belongs to the expected `OpenShell` SSH forward. -pub fn pid_matches_openshell_ssh_forward(pid: u32, port: u16, sandbox_id: Option<&str>) -> bool { +pub fn pid_matches_openshell_ssh_forward(pid: u32, port: u16, sandbox_name: Option<&str>) -> bool { let Some(argv) = process_forward_match_tokens(pid) else { return false; }; let tokens: Vec<&str> = argv.iter().map(String::as_str).collect(); - args_match_ssh_forward(&tokens, port, sandbox_id) + args_match_ssh_forward(&tokens, port, sandbox_name) } /// Read a process command line as matcher tokens. @@ -210,14 +210,14 @@ struct ProxyCommandMatch { } /// Match an `OpenShell` SSH forward by proxy ownership and outer SSH args. -fn args_match_ssh_forward(args: &[&str], port: u16, sandbox_id: Option<&str>) -> bool { +fn args_match_ssh_forward(args: &[&str], port: u16, sandbox_name: Option<&str>) -> bool { if args.first().and_then(|arg| arg.rsplit('/').next()) != Some("ssh") { return false; } - let Some(proxy) = find_proxy_command_match(args, sandbox_id) else { + let Some(proxy) = find_proxy_command_match(args, sandbox_name) else { return false; }; - if sandbox_id.is_some() && !proxy.sandbox_id_requirement_met { + if sandbox_name.is_some() && !proxy.sandbox_id_requirement_met { return false; } outer_ssh_forward_matches( @@ -229,12 +229,15 @@ fn args_match_ssh_forward(args: &[&str], port: u16, sandbox_id: Option<&str>) -> /// Test-only wrapper for flat command lines. #[cfg(test)] -fn command_matches_ssh_forward(command: &str, port: u16, sandbox_id: Option<&str>) -> bool { +fn command_matches_ssh_forward(command: &str, port: u16, sandbox_name: Option<&str>) -> bool { let args = command.split_whitespace().collect::>(); - args_match_ssh_forward(&args, port, sandbox_id) + args_match_ssh_forward(&args, port, sandbox_name) } -fn find_proxy_command_match(args: &[&str], sandbox_id: Option<&str>) -> Option { +fn find_proxy_command_match( + args: &[&str], + sandbox_name: Option<&str>, +) -> Option { for (index, arg) in args.iter().enumerate().skip(1) { let Some(prefix_has_no_command) = parse_ssh_prefix_before_proxy(args, index) else { continue; @@ -244,25 +247,25 @@ fn find_proxy_command_match(args: &[&str], sandbox_id: Option<&str>) -> Option

Result { return Ok(false); }; let pid = record.pid; - let Some(sandbox_id) = expected_sandbox_id_from_record(&record) else { + let Some(_) = expected_sandbox_id_from_record(&record) else { // Legacy PID records do not prove process ownership. let _ = std::fs::remove_file(&pid_path); return Ok(false); }; if pid_is_alive(pid) { - if !pid_matches_openshell_ssh_forward(pid, port, Some(sandbox_id)) { + if !pid_matches_openshell_ssh_forward(pid, port, Some(name)) { let _ = std::fs::remove_file(&pid_path); return Ok(false); } @@ -496,11 +499,10 @@ pub fn list_forwards() -> Result> { && let Some(record) = read_forward_pid(&stem[..dash_pos], port) { // Revalidate ownership so PID reuse does not look like a live forward. - let validated_alive = - expected_sandbox_id_from_record(&record).is_some_and(|sandbox_id| { - pid_is_alive(record.pid) - && pid_matches_openshell_ssh_forward(record.pid, port, Some(sandbox_id)) - }); + let validated_alive = expected_sandbox_id_from_record(&record).is_some_and(|_| { + pid_is_alive(record.pid) + && pid_matches_openshell_ssh_forward(record.pid, port, Some(&stem[..dash_pos])) + }); forwards.push(ForwardInfo { sandbox_name: stem[..dash_pos].to_string(), port, @@ -798,20 +800,20 @@ pub fn shell_escape(value: &str) -> String { /// Build the SSH `ProxyCommand` string used to tunnel to a sandbox. /// /// Every interpolated argument is shell-escaped so that server-supplied values -/// (gateway URL, sandbox id, token, gateway name) cannot inject shell +/// (gateway URL, sandbox name, token, gateway name) cannot inject shell /// metacharacters into the command that OpenSSH executes via `/bin/sh -c`. pub fn build_proxy_command( exe: &str, gateway_url: &str, - sandbox_id: &str, + sandbox_name: &str, token: &str, gateway_name: &str, ) -> String { format!( - "{} ssh-proxy --gateway {} --sandbox-id {} --token {} --gateway-name {}", + "{} ssh-proxy --gateway {} --sandbox {} --token {} --gateway-name {}", shell_escape(exe), shell_escape(gateway_url), - shell_escape(sandbox_id), + shell_escape(sandbox_name), shell_escape(token), shell_escape(gateway_name), ) @@ -1218,7 +1220,7 @@ mod tests { // An empty value must become `''` rather than disappearing — otherwise // downstream argv splitting would misalign. let cmd = build_proxy_command("exe", "gw", "", "tok", "name"); - assert!(cmd.contains("--sandbox-id ''")); + assert!(cmd.contains("--sandbox ''")); } #[test] @@ -1232,7 +1234,7 @@ mod tests { ); assert_eq!( cmd, - "/usr/local/bin/openshell ssh-proxy --gateway gw --sandbox-id sb-123 --token tok.456 --gateway-name name_1" + "/usr/local/bin/openshell ssh-proxy --gateway gw --sandbox sb-123 --token tok.456 --gateway-name name_1" ); } @@ -1451,8 +1453,10 @@ mod tests { #[test] fn ssh_forward_command_matches_exact_l_argument() { - let command = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N -L 80:127.0.0.1:80 sandbox"; - let compact = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N -L80:127.0.0.1:80 sandbox"; + let command = + "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N -L 80:127.0.0.1:80 sandbox"; + let compact = + "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N -L80:127.0.0.1:80 sandbox"; assert!(command_matches_ssh_forward(command, 80, Some("sbx-1"))); assert!(command_matches_ssh_forward(compact, 80, Some("sbx-1"))); @@ -1460,8 +1464,8 @@ mod tests { #[test] fn ssh_forward_command_matches_bind_prefixed_l_argument() { - let command = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N -L 127.0.0.1:80:127.0.0.1:80 sandbox"; - let compact = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N -L[::1]:80:127.0.0.1:80 sandbox"; + let command = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N -L 127.0.0.1:80:127.0.0.1:80 sandbox"; + let compact = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N -L[::1]:80:127.0.0.1:80 sandbox"; assert!(command_matches_ssh_forward(command, 80, Some("sbx-1"))); assert!(command_matches_ssh_forward(compact, 80, Some("sbx-1"))); @@ -1469,15 +1473,17 @@ mod tests { #[test] fn ssh_forward_command_rejects_substring_port_collision() { - let command = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N -L 127.0.0.1:8080:127.0.0.1:8080 sandbox"; + let command = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N -L 127.0.0.1:8080:127.0.0.1:8080 sandbox"; assert!(!command_matches_ssh_forward(command, 80, Some("sbx-1"))); } #[test] fn ssh_forward_command_requires_matching_sandbox_id() { - let command = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-2 -N -L 80:127.0.0.1:80 sandbox"; - let equals = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id=sbx-1 -N -L 80:127.0.0.1:80 sandbox"; + let command = + "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-2 -N -L 80:127.0.0.1:80 sandbox"; + let equals = + "ssh -o ProxyCommand=openshell ssh-proxy --sandbox=sbx-1 -N -L 80:127.0.0.1:80 sandbox"; assert!(!command_matches_ssh_forward(command, 80, Some("sbx-1"))); assert!(command_matches_ssh_forward(equals, 80, Some("sbx-1"))); @@ -1486,8 +1492,8 @@ mod tests { #[test] fn ssh_forward_command_rejects_sandbox_id_prefix_collision() { - let split = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-10 -N -L 80:127.0.0.1:80 sandbox"; - let equals = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id=sbx-10 -N -L 80:127.0.0.1:80 sandbox"; + let split = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-10 -N -L 80:127.0.0.1:80 sandbox"; + let equals = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox=sbx-10 -N -L 80:127.0.0.1:80 sandbox"; assert!(!command_matches_ssh_forward(split, 80, Some("sbx-1"))); assert!(!command_matches_ssh_forward(equals, 80, Some("sbx-1"))); @@ -1495,9 +1501,10 @@ mod tests { #[test] fn ssh_forward_command_rejects_host_port_ambiguity() { - let wrong_remote_port = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N -L 80:127.0.0.1:8080 sandbox"; - let wrong_local_port = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N -L 127.0.0.1:8080:127.0.0.1:80 sandbox"; - let wrong_remote_host = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N -L 80:localhost:80 sandbox"; + let wrong_remote_port = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N -L 80:127.0.0.1:8080 sandbox"; + let wrong_local_port = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N -L 127.0.0.1:8080:127.0.0.1:80 sandbox"; + let wrong_remote_host = + "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N -L 80:localhost:80 sandbox"; assert!(!command_matches_ssh_forward( wrong_remote_port, @@ -1518,14 +1525,14 @@ mod tests { #[test] fn ssh_forward_command_matches_path_basenames_and_bind_variants() { - let command = "/usr/bin/ssh -o ProxyCommand=/usr/local/bin/ssh-proxy --sandbox-id=sbx-1 -N -L localhost:80:127.0.0.1:80 sandbox"; + let command = "/usr/bin/ssh -o ProxyCommand=/usr/local/bin/ssh-proxy --sandbox=sbx-1 -N -L localhost:80:127.0.0.1:80 sandbox"; assert!(command_matches_ssh_forward(command, 80, Some("sbx-1"))); } #[test] fn ssh_forward_command_matches_generated_forward_shape() { - let command = "/usr/bin/ssh -N -o ProxyCommand=/path/openshell ssh-proxy --gateway https://127.0.0.1:9443 --sandbox-id sbx-1 --token tok_123 --gateway-name local -o ExitOnForwardFailure=yes -L 127.0.0.1:80:127.0.0.1:80 -f sandbox"; + let command = "/usr/bin/ssh -N -o ProxyCommand=/path/openshell ssh-proxy --gateway https://127.0.0.1:9443 --sandbox sbx-1 --token tok_123 --gateway-name local -o ExitOnForwardFailure=yes -L 127.0.0.1:80:127.0.0.1:80 -f sandbox"; assert!(command_matches_ssh_forward(command, 80, Some("sbx-1"))); } @@ -1548,7 +1555,7 @@ mod tests { fn expand_proxy_command_arg_splits_value_and_keeps_prefix() { let exe = "/Application Support/openshell"; let arg = format!( - "ProxyCommand={} ssh-proxy --sandbox-id sbx-1", + "ProxyCommand={} ssh-proxy --sandbox sbx-1", shell_escape(exe) ); assert_eq!( @@ -1556,7 +1563,7 @@ mod tests { vec![ format!("ProxyCommand={exe}"), "ssh-proxy".to_string(), - "--sandbox-id".to_string(), + "--sandbox".to_string(), "sbx-1".to_string(), ] ); @@ -1572,7 +1579,7 @@ mod tests { // ProxyCommand element and matches correctly. let exe = "/Application Support/openshell"; let proxy_arg = format!( - "ProxyCommand={} ssh-proxy --gateway https://127.0.0.1:9443 --sandbox-id sbx-1 --token tok_123 --gateway-name local", + "ProxyCommand={} ssh-proxy --gateway https://127.0.0.1:9443 --sandbox sbx-1 --token tok_123 --gateway-name local", shell_escape(exe) ); // Mirror process_forward_match_tokens: the ProxyCommand element is expanded. @@ -1600,8 +1607,8 @@ mod tests { #[test] fn ssh_forward_command_rejects_proxy_name_collisions() { - let wrong_ssh = "notssh ssh-proxy --sandbox-id sbx-1 -N -L 80:127.0.0.1:80 sandbox"; - let wrong_proxy = "ssh -o ProxyCommand=/usr/local/bin/not-ssh-proxy --sandbox-id=sbx-1 -N -L 80:127.0.0.1:80 sandbox"; + let wrong_ssh = "notssh ssh-proxy --sandbox sbx-1 -N -L 80:127.0.0.1:80 sandbox"; + let wrong_proxy = "ssh -o ProxyCommand=/usr/local/bin/not-ssh-proxy --sandbox=sbx-1 -N -L 80:127.0.0.1:80 sandbox"; assert!(!command_matches_ssh_forward(wrong_ssh, 80, Some("sbx-1"))); assert!(!command_matches_ssh_forward(wrong_proxy, 80, Some("sbx-1"))); @@ -1609,24 +1616,25 @@ mod tests { #[test] fn ssh_forward_command_rejects_non_ssh_process_with_matching_tokens() { - let command = "python3 /tmp/ssh ssh-proxy --sandbox-id sbx-1 -N -L 80:127.0.0.1:80 sandbox"; + let command = "python3 /tmp/ssh ssh-proxy --sandbox sbx-1 -N -L 80:127.0.0.1:80 sandbox"; assert!(!command_matches_ssh_forward(command, 80, Some("sbx-1"))); } #[test] fn ssh_forward_command_rejects_bare_ssh_proxy_destination() { - let command = "ssh ssh-proxy --sandbox-id sbx-1 -N -L80:127.0.0.1:80 sandbox"; + let command = "ssh ssh-proxy --sandbox sbx-1 -N -L80:127.0.0.1:80 sandbox"; assert!(!command_matches_ssh_forward(command, 80, Some("sbx-1"))); } #[test] fn ssh_forward_command_rejects_remote_command_l_argument() { - let remote_arg = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 -N sandbox -L 80:127.0.0.1:80"; - let missing_no_command = "ssh ssh-proxy --sandbox-id sbx-1 -L 80:127.0.0.1:80 sandbox"; - let remote_command_lookalike = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-1 real-host echo -N -L 80:127.0.0.1:80 sandbox"; - let sandbox_id_in_remote_command = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox-id sbx-2 real-host --sandbox-id sbx-1 -N -L 80:127.0.0.1:80 sandbox"; + let remote_arg = + "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 -N sandbox -L 80:127.0.0.1:80"; + let missing_no_command = "ssh ssh-proxy --sandbox sbx-1 -L 80:127.0.0.1:80 sandbox"; + let remote_command_lookalike = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-1 real-host echo -N -L 80:127.0.0.1:80 sandbox"; + let sandbox_id_in_remote_command = "ssh -o ProxyCommand=openshell ssh-proxy --sandbox sbx-2 real-host --sandbox sbx-1 -N -L 80:127.0.0.1:80 sandbox"; assert!(!command_matches_ssh_forward(remote_arg, 80, Some("sbx-1"))); assert!(!command_matches_ssh_forward( diff --git a/crates/openshell-core/src/grpc_client.rs b/crates/openshell-core/src/grpc_client.rs index 38b91e8501..0af88f2989 100644 --- a/crates/openshell-core/src/grpc_client.rs +++ b/crates/openshell-core/src/grpc_client.rs @@ -28,7 +28,6 @@ use crate::proto::{ NetworkActivitySummary, PolicyChunk, PolicySource, PolicyStatus, RefreshSandboxTokenRequest, ReportPolicyStatusRequest, SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, UpdateConfigRequest, open_shell_client::OpenShellClient, - workspace_selector, }; use crate::sandbox_env; use miette::{IntoDiagnostic, Result, WrapErr}; @@ -686,14 +685,17 @@ async fn connect(endpoint: &str) -> Result> { /// Returns `Ok(Some(policy))` when the server has a policy configured, /// or `Ok(None)` when the sandbox was created without a policy (the sandbox /// should discover one from disk or use the restrictive default). -pub async fn fetch_policy(endpoint: &str, sandbox_id: &str) -> Result> { - debug!(endpoint = %endpoint, sandbox_id = %sandbox_id, "Connecting to OpenShell server"); +pub async fn fetch_policy( + endpoint: &str, + sandbox_name: &str, +) -> Result> { + debug!(endpoint = %endpoint, sandbox_name = %sandbox_name, "Connecting to OpenShell server"); let mut client = connect(endpoint).await?; debug!("Connected, fetching sandbox policy"); - fetch_policy_with_client(&mut client, sandbox_id).await + fetch_policy_with_client(&mut client, sandbox_name).await } /// Fetch the authoritative policy and revision metadata in one response. @@ -704,20 +706,22 @@ pub async fn fetch_policy(endpoint: &str, sandbox_id: &str) -> Result Result { - debug!(endpoint = %endpoint, sandbox_id = %sandbox_id, "Connecting to fetch OpenShell settings snapshot"); + debug!(endpoint = %endpoint, sandbox_name = %sandbox_name, "Connecting to fetch OpenShell settings snapshot"); let mut client = connect(endpoint).await?; - fetch_settings_snapshot_with_client(&mut client, sandbox_id).await + fetch_settings_snapshot_with_client(&mut client, sandbox_name, None).await } async fn fetch_settings_snapshot_with_client( client: &mut OpenShellClient, - sandbox_id: &str, + sandbox_name: &str, + workspace: Option<&str>, ) -> Result { let response = client .get_sandbox_config(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_name.to_string(), + workspace_scope: workspace.map(crate::proto::workspace_selector), }) .await .into_diagnostic()?; @@ -728,9 +732,9 @@ async fn fetch_settings_snapshot_with_client( /// Fetch sandbox policy using an existing client connection. async fn fetch_policy_with_client( client: &mut OpenShellClient, - sandbox_id: &str, + sandbox_name: &str, ) -> Result> { - let snapshot = fetch_settings_snapshot_with_client(client, sandbox_id).await?; + let snapshot = fetch_settings_snapshot_with_client(client, sandbox_name, None).await?; // version 0 with no policy means the sandbox was created without one. if snapshot.version == 0 && snapshot.policy.is_none() { @@ -751,9 +755,9 @@ async fn sync_policy_with_client( ) -> Result<()> { client .update_config(UpdateConfigRequest { - name: sandbox.to_string(), + sandbox: sandbox.to_string(), + workspace_scope: Some(crate::proto::workspace_selector(workspace)), policy: Some(policy.clone()), - workspace_scope: Some(workspace_selector(workspace)), ..Default::default() }) .await @@ -769,14 +773,12 @@ async fn sync_policy_with_client( /// channel instead of establishing three separate connections. pub async fn discover_and_sync_policy( endpoint: &str, - sandbox_id: &str, sandbox: &str, discovered_policy: &ProtoSandboxPolicy, workspace: &str, ) -> Result { debug!( endpoint = %endpoint, - sandbox_id = %sandbox_id, sandbox = %sandbox, "Syncing discovered policy and re-fetching canonical version" ); @@ -787,8 +789,9 @@ pub async fn discover_and_sync_policy( 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) + fetch_settings_snapshot_with_client(&mut client, sandbox, Some(workspace)) .await? + .policy .ok_or_else(|| { miette::miette!("Server still returned no policy after sync — this is a bug") }) @@ -812,14 +815,13 @@ pub async fn sync_policy( /// Sync an enriched policy and return the authoritative revision snapshot. pub async fn sync_policy_and_fetch_snapshot( endpoint: &str, - sandbox_id: &str, sandbox: &str, policy: &ProtoSandboxPolicy, workspace: &str, ) -> Result { let mut client = connect(endpoint).await?; sync_policy_with_client(&mut client, sandbox, policy, workspace).await?; - fetch_settings_snapshot_with_client(&mut client, sandbox_id).await + fetch_settings_snapshot_with_client(&mut client, sandbox, Some(workspace)).await } /// Fetch provider environment variables for a sandbox from `OpenShell` server via gRPC. @@ -1048,12 +1050,15 @@ impl CachedOpenShellClient { } /// Poll for current effective sandbox settings and policy metadata. - pub async fn poll_settings(&self, sandbox_id: &str) -> Result { + pub async fn poll_settings(&self, sandbox_name: &str) -> Result { + let workspace = self.workspace(); let response = self .client .clone() .get_sandbox_config(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_name.to_string(), + workspace_scope: (!workspace.is_empty()) + .then(|| crate::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -1179,9 +1184,9 @@ impl CachedOpenShellClient { .client .clone() .get_draft_policy(GetDraftPolicyRequest { - name: sandbox_name.to_string(), status_filter: status_filter.to_string(), - workspace_scope: Some(workspace_selector(self.workspace())), + sandbox: sandbox_name.to_string(), + workspace_scope: Some(crate::proto::workspace_selector(self.workspace())), }) .await .into_diagnostic()?; diff --git a/crates/openshell-gateway-interceptors/src/proto_json.rs b/crates/openshell-gateway-interceptors/src/proto_json.rs index dde37190e6..5c01046dda 100644 --- a/crates/openshell-gateway-interceptors/src/proto_json.rs +++ b/crates/openshell-gateway-interceptors/src/proto_json.rs @@ -450,12 +450,12 @@ mod tests { let codec = ProtoJsonCodec::from_descriptor_set(openshell_core::FILE_DESCRIPTOR_SET).unwrap(); let request = UpdateConfigRequest { - name: "demo".to_string(), + sandbox: "demo".to_string(), + workspace_scope: Some(workspace_selector("default")), annotations: HashMap::from([( "openshell.nvidia.com/policy-signature".to_string(), "signed".to_string(), )]), - workspace_scope: Some(workspace_selector("default")), ..Default::default() }; let bytes = request.encode_to_vec(); diff --git a/crates/openshell-gateway-interceptors/src/runtime.rs b/crates/openshell-gateway-interceptors/src/runtime.rs index 0bd1ba2b47..c2dbfe90f8 100644 --- a/crates/openshell-gateway-interceptors/src/runtime.rs +++ b/crates/openshell-gateway-interceptors/src/runtime.rs @@ -1009,7 +1009,8 @@ mod tests { codec: codec.clone(), }; let request = UpdateConfigRequest { - name: "demo".to_string(), + sandbox: "demo".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), expected_resource_version: u64::MAX - 1, annotations: HashMap::from([ ("policy-hash".to_string(), "sha256:v2:abc".to_string()), @@ -1017,7 +1018,6 @@ mod tests { ("policy-signature-kid".to_string(), "kid".to_string()), ("correlation-id".to_string(), "reload-1".to_string()), ]), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..UpdateConfigRequest::default() }; let body = GrpcFrame { @@ -1186,7 +1186,10 @@ mod tests { let operation = ValidatedOperation::new( &codec, "openshell.v1.UpdateConfigRequest", - json!({"name": "demo", "expectedResourceVersion": "7"}), + json!({ + "sandbox": "demo", "workspaceScope": {"workspace": "default"}, + "expectedResourceVersion": "7" + }), ) .unwrap(); let prior = operation.clone(); @@ -1224,7 +1227,10 @@ mod tests { let operation = ValidatedOperation::new( &codec, "openshell.v1.UpdateConfigRequest", - json!({"name": "demo", "expectedResourceVersion": "7"}), + json!({ + "sandbox": "demo", "workspaceScope": {"workspace": "default"}, + "expectedResourceVersion": "7" + }), ) .unwrap(); let plan = test_modify_plan(FailurePolicy::FailClosed); @@ -1262,12 +1268,15 @@ mod tests { let operation = ValidatedOperation::new( &codec, "openshell.v1.UpdateConfigRequest", - json!({"name": "demo", "expectedResourceVersion": "7"}), + json!({ + "sandbox": "demo", "workspaceScope": {"workspace": "default"}, + "expectedResourceVersion": "7" + }), ) .unwrap(); let prior = operation.clone(); let result = allowed_result(vec![ - patch("replace", "/name", json!("partially-mutated")), + patch("replace", "/sandbox", json!("partially-mutated")), patch("replace", "/expectedResourceVersion", json!("not-a-number")), ]); @@ -1281,7 +1290,7 @@ mod tests { .unwrap(); assert_eq!(outcome, prior); - assert_eq!(outcome.json["name"], "demo"); + assert_eq!(outcome.json["sandbox"], "demo"); } #[tokio::test] @@ -1292,17 +1301,20 @@ mod tests { let operation = ValidatedOperation::new( &codec, "openshell.v1.UpdateConfigRequest", - json!({"name": "demo", "expectedResourceVersion": "7"}), + json!({ + "sandbox": "demo", "workspaceScope": {"workspace": "default"}, + "expectedResourceVersion": "7" + }), ) .unwrap(); let plan = test_modify_plan(FailurePolicy::FailOpen); let invalid_first = allowed_result(vec![ - patch("replace", "/name", json!("rejected-candidate")), + patch("replace", "/sandbox", json!("rejected-candidate")), patch("replace", "/expectedResourceVersion", json!("not-a-number")), ]); let second = allowed_result(vec![ - patch("test", "/name", json!("demo")), - patch("replace", "/name", json!("accepted-candidate")), + patch("test", "/sandbox", json!("demo")), + patch("replace", "/sandbox", json!("accepted-candidate")), ]); let operation = apply_evaluation_result( @@ -1322,9 +1334,9 @@ mod tests { ) .unwrap(); - assert_eq!(operation.json["name"], "accepted-candidate"); + assert_eq!(operation.json["sandbox"], "accepted-candidate"); let decoded = UpdateConfigRequest::decode(operation.encoded.as_slice()).unwrap(); - assert_eq!(decoded.name, "accepted-candidate"); + assert_eq!(decoded.sandbox, "accepted-candidate"); assert_eq!(decoded.expected_resource_version, 7); } @@ -1371,10 +1383,13 @@ mod tests { let operation = ValidatedOperation::new( &codec, "openshell.v1.UpdateConfigRequest", - json!({"name": "demo", "expectedResourceVersion": "7"}), + json!({ + "sandbox": "demo", "workspaceScope": {"workspace": "default"}, + "expectedResourceVersion": "7" + }), ) .unwrap(); - let result = allowed_result(vec![patch("replace", "/name", json!("accepted"))]); + let result = allowed_result(vec![patch("replace", "/sandbox", json!("accepted"))]); let recorder = TestRecorder::default(); let operation = metrics::with_local_recorder(&recorder, || { @@ -1389,7 +1404,7 @@ mod tests { .unwrap(); let decoded = UpdateConfigRequest::decode(operation.encoded.as_slice()).unwrap(); - assert_eq!(decoded.name, "accepted"); + assert_eq!(decoded.sandbox, "accepted"); assert_eq!(TestRecorder::count(&recorder.evaluations), 1); assert_eq!(TestRecorder::count(&recorder.patches), 1); assert_eq!(TestRecorder::count(&recorder.fail_open), 0); diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index b5b9358ac0..2d804c5434 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -209,7 +209,7 @@ pub async fn run_sandbox( } else { load_policy( sandbox_id.clone(), - sandbox, + sandbox.clone(), openshell_endpoint.clone(), policy_rules, policy_data, @@ -733,8 +733,9 @@ pub async fn run_sandbox( // Spawn background policy poll task (gRPC mode only). if !process_uses_sidecar_control - && let (Some(id), Some(endpoint), Some(engine)) = ( + && let (Some(id), Some(sandbox_name), Some(endpoint), Some(engine)) = ( sandbox_id.as_deref(), + sandbox.as_deref(), openshell_endpoint.as_deref(), opa_engine.as_ref(), ) @@ -754,6 +755,7 @@ pub async fn run_sandbox( let poll_ctx = PolicyPollLoopContext { endpoint: poll_endpoint, sandbox_id: poll_id, + sandbox_name: sandbox_name.to_string(), opa_engine: poll_engine, loaded_policy_origin, entrypoint_pid: poll_pid, @@ -2368,14 +2370,16 @@ async fn load_policy( } // gRPC mode: fetch typed proto policy, construct OPA engine from baked rules + proto data - if let (Some(id), Some(endpoint)) = (&sandbox_id, &openshell_endpoint) { + if let (Some(id), Some(sandbox_name), Some(endpoint)) = + (&sandbox_id, &sandbox, &openshell_endpoint) + { info!( sandbox_id = %id, endpoint = %endpoint, "Fetching sandbox policy via gRPC" ); let mut snapshot = grpc_retry("Policy fetch", || { - openshell_core::grpc_client::fetch_settings_snapshot(endpoint, id) + openshell_core::grpc_client::fetch_settings_snapshot(endpoint, sandbox_name) }) .await?; @@ -2411,7 +2415,6 @@ async fn load_policy( snapshot = grpc_retry("Policy discovery sync", || { openshell_core::grpc_client::sync_policy_and_fetch_snapshot( endpoint, - id, sandbox, &discovered, &ws, @@ -2438,7 +2441,6 @@ async fn load_policy( if let Some(sandbox_name) = sandbox.as_deref() { match openshell_core::grpc_client::sync_policy_and_fetch_snapshot( endpoint, - id, sandbox_name, &sync_policy, &snapshot.workspace, @@ -3332,6 +3334,7 @@ async fn report_initial_policy_failure( struct PolicyPollLoopContext { endpoint: String, sandbox_id: String, + sandbox_name: String, opa_engine: Arc, /// Source of the policy currently loaded into OPA. This distinguishes an /// explicit local-file override from an unbound gateway revision so the @@ -3777,7 +3780,7 @@ async fn run_policy_poll_loop_with_client( // Initialize revision from the first poll and acknowledge the initial // policy revision the supervisor actually loaded. A mismatched result is // reconciled below instead of being recorded as already applied. - match client.poll_settings(&ctx.sandbox_id).await { + match client.poll_settings(&ctx.sandbox_name).await { Ok(result) => { let _ = ctx.workspace_tx.send(client.workspace()); match initial_poll_disposition(&ctx.loaded_policy_origin, &result) { @@ -3844,7 +3847,7 @@ async fn run_policy_poll_loop_with_client( result } else { tokio::time::sleep(next_poll_delay(&ctx.extension_credentials, interval)).await; - match client.poll_settings(&ctx.sandbox_id).await { + match client.poll_settings(&ctx.sandbox_name).await { Ok(result) => { let _ = ctx.workspace_tx.send(client.workspace()); result @@ -5076,6 +5079,7 @@ network_policies: PolicyPollLoopContext { endpoint: String::new(), sandbox_id: "sandbox-test".to_string(), + sandbox_name: "sandbox-test".to_string(), opa_engine, loaded_policy_origin, entrypoint_pid: Arc::new(AtomicU32::new(0)), diff --git a/crates/openshell-sandbox/src/main.rs b/crates/openshell-sandbox/src/main.rs index 440676ad14..cf6050fc35 100644 --- a/crates/openshell-sandbox/src/main.rs +++ b/crates/openshell-sandbox/src/main.rs @@ -29,8 +29,8 @@ const COPY_SELF_SUBCOMMAND: &str = "copy-self"; /// Reads the same token sources as the supervisor (env, file, K8s SA /// bootstrap) and issues a single gRPC call against the gateway. Useful /// for end-to-end verification: e.g. `docker exec` into a sandbox, then -/// run `openshell-sandbox debug-rpc get-sandbox-config --sandbox-id ` -/// to confirm the cross-sandbox IDOR guard fires. +/// run `openshell-sandbox debug-rpc get-sandbox-config --sandbox ` +/// to confirm the cross-sandbox authorization guard fires. const DEBUG_RPC_SUBCOMMAND: &str = "debug-rpc"; const VALIDATE_WORKSPACE_SUBCOMMAND: &str = "validate-workspace"; diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index c789aded99..b419a15a7e 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -291,7 +291,7 @@ impl OpenShellClient { let response = self .unary(|mut grpc| { let request = proto::GetSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.get_sandbox(request).await } @@ -347,7 +347,7 @@ impl OpenShellClient { let response = self .unary(|mut grpc| { let request = proto::DeleteSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.delete_sandbox(request).await } @@ -361,7 +361,7 @@ impl OpenShellClient { let response = self .unary(|mut grpc| { let request = proto::StopSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.stop_sandbox(request).await } @@ -375,7 +375,7 @@ impl OpenShellClient { let response = self .unary(|mut grpc| { let request = proto::StartSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.start_sandbox(request).await } @@ -570,9 +570,9 @@ impl OpenShellClient { /// For streaming output, drop down to [`OpenShellClient::raw_grpc`] and /// call `exec_sandbox` directly. pub async fn exec(&self, name: &str, cmd: &[String], opts: ExecOptions) -> Result { - let sandbox = self.get_sandbox(name).await?; let request = proto::ExecSandboxRequest { - sandbox_id: sandbox.id, + sandbox: name.to_string(), + workspace_scope: Some(proto::workspace_selector("default")), command: cmd.to_vec(), workdir: opts.workdir.unwrap_or_default(), environment: opts.environment, @@ -851,7 +851,7 @@ impl WorkspaceScopedClient { .client .unary(|mut grpc| { let request = proto::GetSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.get_sandbox(request).await } @@ -905,7 +905,7 @@ impl WorkspaceScopedClient { .client .unary(|mut grpc| { let request = proto::DeleteSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.delete_sandbox(request).await } @@ -920,7 +920,7 @@ impl WorkspaceScopedClient { .client .unary(|mut grpc| { let request = proto::StopSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.stop_sandbox(request).await } @@ -935,7 +935,7 @@ impl WorkspaceScopedClient { .client .unary(|mut grpc| { let request = proto::StartSandboxRequest { - name: name.to_string(), + sandbox: name.to_string(), workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.start_sandbox(request).await } @@ -999,9 +999,9 @@ impl WorkspaceScopedClient { /// Run a command inside a sandbox and buffer stdout/stderr. pub async fn exec(&self, name: &str, cmd: &[String], opts: ExecOptions) -> Result { - let sandbox = self.get_sandbox(name).await?; let request = proto::ExecSandboxRequest { - sandbox_id: sandbox.id, + sandbox: name.to_string(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), command: cmd.to_vec(), workdir: opts.workdir.unwrap_or_default(), environment: opts.environment, diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 549f715d9b..d097b0a416 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -300,7 +300,7 @@ impl OpenShell for TestOpenShell { ) -> Result, Status> { let request = request.into_inner(); let sandbox = sandbox_with_phase_ws( - &request.name, + request.sandbox.as_str(), proto::SandboxPhase::Stopped, selected_workspace(&request.workspace_scope).unwrap_or("default"), ); @@ -316,7 +316,7 @@ impl OpenShell for TestOpenShell { ) -> Result, Status> { let request = request.into_inner(); let sandbox = sandbox_with_phase_ws( - &request.name, + request.sandbox.as_str(), proto::SandboxPhase::Starting, selected_workspace(&request.workspace_scope).unwrap_or("default"), ); @@ -331,7 +331,7 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let req = request.into_inner(); - let name = req.name; + let name = req.sandbox.clone(); *self.state.last_get_name.lock().await = Some(name.clone()); *self.state.last_get_workspace.lock().await = selected_workspace(&req.workspace_scope).map(str::to_string); @@ -426,7 +426,7 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let req = request.into_inner(); - *self.state.last_delete_name.lock().await = Some(req.name); + *self.state.last_delete_name.lock().await = Some(req.sandbox.clone()); *self.state.last_delete_workspace.lock().await = selected_workspace(&req.workspace_scope).map(str::to_string); Ok(Response::new(proto::DeleteSandboxResponse { @@ -1187,7 +1187,7 @@ async fn stop_and_start_map_requests_and_phases() { let stopped = client.stop_sandbox("sleepy").await.unwrap(); assert_eq!(stopped.phase, SandboxPhase::Stopped); let stop = state.last_stop.lock().await.clone().unwrap(); - assert_eq!(stop.name, "sleepy"); + assert_eq!(Some(stop.sandbox.as_str()), Some("sleepy")); assert_eq!(selected_workspace(&stop.workspace_scope), Some("default")); let started = client @@ -1197,7 +1197,7 @@ async fn stop_and_start_map_requests_and_phases() { .unwrap(); assert_eq!(started.phase, SandboxPhase::Starting); let start = state.last_start.lock().await.clone().unwrap(); - assert_eq!(start.name, "sleepy"); + assert_eq!(Some(start.sandbox.as_str()), Some("sleepy")); assert_eq!(selected_workspace(&start.workspace_scope), Some("team-a")); } @@ -1330,7 +1330,7 @@ async fn exec_buffers_stdout_stderr_and_exit() { assert_eq!(result.stderr, b"warn\n"); let observed = state.last_exec_request.lock().await.clone().unwrap(); - assert_eq!(observed.sandbox_id, "id-my-box"); + assert_eq!(observed.sandbox, "my-box"); assert_eq!( observed.command, vec!["echo".to_string(), "hello".to_string()] @@ -1652,7 +1652,7 @@ async fn delete_workspace_returns_ack() { } #[tokio::test] -async fn sandbox_ref_includes_workspace_field() { +async fn sandbox_result_includes_workspace_field() { let state = Arc::new(MockState { phase_sequence: vec![proto::SandboxPhase::Ready], ..Default::default() diff --git a/crates/openshell-server/src/grpc/auth_rpc.rs b/crates/openshell-server/src/grpc/auth_rpc.rs index 62547cc290..538bfd9164 100644 --- a/crates/openshell-server/src/grpc/auth_rpc.rs +++ b/crates/openshell-server/src/grpc/auth_rpc.rs @@ -91,7 +91,7 @@ pub async fn handle_issue_sandbox_token( Status::unavailable("sandbox JWT minting is not configured on this gateway") })?; - ensure_sandbox_exists(state, &sandbox.sandbox_id).await?; + let _ = ensure_sandbox_exists(state, &sandbox.sandbox_id).await?; let minted = issuer.mint(&sandbox.sandbox_id)?; info!( @@ -142,7 +142,7 @@ pub async fn handle_refresh_sandbox_token( Status::unavailable("sandbox JWT minting is not configured on this gateway") })?; - ensure_sandbox_exists(state, &sandbox.sandbox_id).await?; + let sandbox_record = ensure_sandbox_exists(state, &sandbox.sandbox_id).await?; let minted = issuer.mint(&sandbox.sandbox_id)?; let extension_credentials = if requested_extension_services.is_empty() { @@ -164,7 +164,11 @@ pub async fn handle_refresh_sandbox_token( )); } else { let mut config_request = Request::new(GetSandboxConfigRequest { - sandbox_id: sandbox.sandbox_id.clone(), + sandbox: sandbox_record + .metadata + .as_ref() + .map_or_else(String::new, |metadata| metadata.name.clone()), + workspace_scope: None, }); config_request .extensions_mut() @@ -263,7 +267,10 @@ fn mint_extension_credentials( .collect() } -async fn ensure_sandbox_exists(state: &Arc, sandbox_id: &str) -> Result<(), Status> { +async fn ensure_sandbox_exists( + state: &Arc, + sandbox_id: &str, +) -> Result { if sandbox_id.is_empty() { return Err(Status::invalid_argument("sandbox_id is required")); } @@ -273,9 +280,7 @@ async fn ensure_sandbox_exists(state: &Arc, sandbox_id: &str) -> Re .get_message::(sandbox_id) .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; - - Ok(()) + .ok_or_else(|| Status::not_found("sandbox not found")) } #[cfg(test)] diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 814ee1b567..6c58688775 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -12,10 +12,7 @@ use crate::ServerState; use crate::auth::principal::Principal; -use crate::auth::workspace_authz::{ - MinWorkspaceRole, authorize_sandbox_workspace, authorize_workspace_selector, - require_platform_admin, selected_workspace_name, -}; +use crate::auth::workspace_authz::{MinWorkspaceRole, require_platform_admin}; use crate::pagination::Pagination; use crate::persistence::{ DraftChunkRecord, ObjectId, ObjectListQuery, ObjectName, ObjectType, ObjectWorkspace, @@ -2219,7 +2216,7 @@ fn validate_sandbox_caller_update(req: &UpdateConfigRequest) -> Result<(), Statu "sandbox callers cannot delete settings", )); } - if req.name.trim().is_empty() { + if req.sandbox.is_empty() { return Err(Status::permission_denied( "sandbox callers may only perform sandbox policy sync", )); @@ -2368,12 +2365,16 @@ pub(super) async fn handle_get_sandbox_config( request: Request, ) -> Result, Status> { let principal = super::extract_principal(&request)?; - let sandbox_id = request.get_ref().sandbox_id.clone(); - crate::auth::guard::enforce_sandbox_scope(&request, &sandbox_id)?; - drop(request); - - let sandbox = - super::sandbox::fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?; + let req = request.into_inner(); + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, + &principal, + &req.sandbox, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; + let sandbox_id = sandbox.object_id().to_string(); let workspace = sandbox.object_workspace().to_string(); let sandbox_provider_names = sandbox .spec @@ -3243,42 +3244,36 @@ async fn handle_update_config_inner( ) -> Result, Status> { let req = request.into_inner(); validate_annotations(&req.annotations, "annotations")?; - let workspace = if req.global { - if req.workspace_scope.is_some() { + let sandbox = if req.global { + if !req.sandbox.is_empty() || req.workspace_scope.is_some() { return Err(Status::invalid_argument( - "workspace_scope must be omitted when global is true", + "sandbox_name and workspace_scope must be omitted when global is true", )); } require_platform_admin(&state.admin_role, principal)?; - String::new() + None } else { let min_role = if sandbox_caller { MinWorkspaceRole::User } else { MinWorkspaceRole::Admin }; - let workspace = selected_workspace_name(req.workspace_scope.as_ref())?; - authorize_sandbox_workspace( - &state.store, - &state.admin_role, - principal, - workspace, - min_role, + Some( + super::sandbox::resolve_and_authorize_sandbox_name( + state, + principal, + &req.sandbox, + req.workspace_scope.as_ref(), + min_role, + ) + .await?, ) - .await?; - super::workspace::resolve_workspace(state.store.as_ref(), workspace) - .await? - .name }; + let workspace = sandbox.as_ref().map_or_else(String::new, |sandbox| { + sandbox.object_workspace().to_string() + }); if sandbox_caller { validate_sandbox_caller_update(&req)?; - resolve_sandbox_by_name_for_principal( - state.store.as_ref(), - &workspace, - principal, - &req.name, - ) - .await?; } let key = req.setting_key.trim(); let has_policy = req.policy.is_some(); @@ -3482,19 +3477,7 @@ async fn handle_update_config_inner( )); } - if req.name.is_empty() { - return Err(Status::invalid_argument( - "name is required for sandbox-scoped updates", - )); - } - - // Resolve sandbox by name. - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; + let sandbox = sandbox.expect("non-global config update resolves a sandbox"); let sandbox_id = sandbox.object_id().to_string(); let mut response_annotations = sandbox_metadata_annotations(&sandbox); @@ -3931,40 +3914,31 @@ pub(super) async fn handle_get_sandbox_policy_status( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let workspace = if req.global { - if req.workspace_scope.is_some() { + let sandbox = if req.global { + if !req.sandbox.is_empty() || req.workspace_scope.is_some() { return Err(Status::invalid_argument( - "workspace_scope must be omitted when global is true", + "sandbox_name and workspace_scope must be omitted when global is true", )); } require_platform_admin(&state.admin_role, &principal)?; - String::new() + None } else { - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, - &principal, - req.workspace_scope.as_ref(), - MinWorkspaceRole::User, + Some( + super::sandbox::resolve_and_authorize_sandbox_name( + state, + &principal, + &req.sandbox, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?, ) - .await?; - super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name }; let (policy_id, active_version) = if req.global { (GLOBAL_POLICY_SANDBOX_ID.to_string(), 0_u32) } else { - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; + let sandbox = sandbox.as_ref().expect("sandbox query resolved a sandbox"); ( sandbox.object_id().to_string(), sandbox.current_policy_version(), @@ -4004,40 +3978,31 @@ pub(super) async fn handle_list_sandbox_policies( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let workspace = if req.global { - if req.workspace_scope.is_some() { + let sandbox = if req.global { + if !req.sandbox.is_empty() || req.workspace_scope.is_some() { return Err(Status::invalid_argument( - "workspace_scope must be omitted when global is true", + "sandbox_name and workspace_scope must be omitted when global is true", )); } require_platform_admin(&state.admin_role, &principal)?; - String::new() + None } else { - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, - &principal, - req.workspace_scope.as_ref(), - MinWorkspaceRole::User, + Some( + super::sandbox::resolve_and_authorize_sandbox_name( + state, + &principal, + &req.sandbox, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?, ) - .await?; - super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name }; let policy_id = if req.global { GLOBAL_POLICY_SANDBOX_ID.to_string() } else { - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; + let sandbox = sandbox.as_ref().expect("sandbox query resolved a sandbox"); sandbox.object_id().to_string() }; @@ -4046,9 +4011,11 @@ pub(super) async fn handle_list_sandbox_policies( &req.page_token, "ListSandboxPolicies", &[ - &req.name, + &req.sandbox, if req.global { "true" } else { "false" }, - &workspace, + sandbox + .as_ref() + .map_or("", |sandbox| sandbox.object_workspace()), ], )?; let mut records = state @@ -4171,28 +4138,18 @@ pub(super) async fn handle_get_sandbox_logs( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - if req.sandbox_id.is_empty() { - return Err(Status::invalid_argument("sandbox_id is required")); - } - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - let sandbox = - super::sandbox::fetch_and_authorize_sandbox(state, &principal, &req.sandbox_id).await?; - if sandbox.object_workspace() != workspace { - return Err(Status::not_found("sandbox not found")); - } + let sandbox_id = sandbox.object_id(); let lines = if req.lines == 0 { 2000 } else { req.lines }; - let tail = state.tracing_log_bus.tail(&req.sandbox_id, lines as usize); + let tail = state.tracing_log_bus.tail(sandbox_id, lines as usize); let buffer_total = tail.len() as u32; @@ -4720,29 +4677,14 @@ pub(super) async fn handle_get_draft_policy( .cloned() .ok_or_else(|| Status::unauthenticated("missing principal"))?; let req = request.into_inner(); - let workspace_name = selected_workspace_name(req.workspace_scope.as_ref())?; - authorize_sandbox_workspace( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, - workspace_name, + &req.sandbox, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), workspace_name) - .await? - .name; - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - - let sandbox = resolve_sandbox_by_name_for_principal( - state.store.as_ref(), - &workspace, - &principal, - &req.name, - ) - .await?; let sandbox_id = sandbox.object_id().to_string(); let status_filter = if req.status_filter.is_empty() { @@ -4802,32 +4744,21 @@ async fn handle_approve_draft_chunk_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } + let workspace = sandbox.object_workspace().to_string(); if req.chunk_id.is_empty() { return Err(Status::invalid_argument("chunk_id is required")); } require_no_global_policy(state).await?; - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; let sandbox_id = sandbox.object_id().to_string(); let chunk = state @@ -4957,30 +4888,19 @@ async fn handle_reject_draft_chunk_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } + let workspace = sandbox.object_workspace().to_string(); if req.chunk_id.is_empty() { return Err(Status::invalid_argument("chunk_id is required")); } - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; let sandbox_id = sandbox.object_id().to_string(); let chunk = state @@ -5067,29 +4987,18 @@ async fn handle_approve_all_draft_chunks_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } + let workspace = sandbox.object_workspace().to_string(); require_no_global_policy(state).await?; - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; let sandbox_id = sandbox.object_id().to_string(); let pending_chunks = state @@ -5375,20 +5284,15 @@ pub(super) async fn handle_edit_draft_chunk( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } + let workspace = sandbox.object_workspace().to_string(); if req.chunk_id.is_empty() { return Err(Status::invalid_argument("chunk_id is required")); } @@ -5396,12 +5300,6 @@ pub(super) async fn handle_edit_draft_chunk( .proposed_rule .ok_or_else(|| Status::invalid_argument("proposed_rule is required"))?; - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; let sandbox_id = sandbox.object_id().to_string(); let chunk = state @@ -5456,30 +5354,19 @@ async fn handle_undo_draft_chunk_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } + let workspace = sandbox.object_workspace().to_string(); if req.chunk_id.is_empty() { return Err(Status::invalid_argument("chunk_id is required")); } - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; let sandbox_id = sandbox.object_id().to_string(); let chunk = state @@ -5553,27 +5440,14 @@ pub(super) async fn handle_clear_draft_chunks( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; let sandbox_id = sandbox.object_id().to_string(); let deleted = state @@ -5601,27 +5475,14 @@ pub(super) async fn handle_get_draft_history( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; let sandbox_id = sandbox.object_id().to_string(); let all_chunks = state @@ -7337,7 +7198,7 @@ mod tests { } #[tokio::test] - async fn list_sandbox_policies_rejects_token_from_different_filter() { + async fn list_global_policies_rejects_sandbox_name() { let state = test_server_state().await; let policy = ProtoSandboxPolicy::default(); let payload = policy.encode_to_vec(); @@ -7374,7 +7235,7 @@ mod tests { page_size: 1, page_token: first.next_page_token, global: true, - name: "different".to_string(), + sandbox: "different".to_string(), ..Default::default() }), ) @@ -7412,7 +7273,8 @@ mod tests { &state, with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: sandbox_id.clone(), + sandbox: sandbox_id.clone(), + workspace_scope: None, }), &sandbox_id, ), @@ -7459,7 +7321,8 @@ mod tests { &state, with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_id.to_string(), + workspace_scope: None, }), sandbox_id, ), @@ -7513,7 +7376,8 @@ mod tests { &state, with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: sandbox_id.clone(), + sandbox: sandbox_id.clone(), + workspace_scope: None, }), &sandbox_id, ), @@ -7659,7 +7523,8 @@ mod tests { &state, with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_id.to_string(), + workspace_scope: None, }), sandbox_id, ), @@ -7691,9 +7556,9 @@ mod tests { let detail_error = handle_get_sandbox_policy_status( &state, with_user(Request::new(GetSandboxPolicyStatusRequest { - name: "stored-invalid-history".to_string(), - version: 2, + sandbox: "stored-invalid-history".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + version: 2, ..Default::default() })), ) @@ -7709,7 +7574,7 @@ mod tests { let listed = handle_list_sandbox_policies( &state, with_user(Request::new(ListSandboxPoliciesRequest { - name: "stored-invalid-history".to_string(), + sandbox: "stored-invalid-history".to_string(), page_size: 10, workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() @@ -7763,7 +7628,8 @@ mod tests { &state, with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_id.to_string(), + workspace_scope: None, }), sandbox_id, ), @@ -7819,7 +7685,8 @@ mod tests { &state, with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_id.to_string(), + workspace_scope: None, }), sandbox_id, ), @@ -8072,9 +7939,9 @@ mod tests { let response = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: sandbox_name, - policy: Some(mcp_policy_with_versions(&["2025-11-25"])), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(mcp_policy_with_versions(&["2025-11-25"])), ..Default::default() })), ) @@ -8179,9 +8046,9 @@ mod tests { let response = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: sandbox_name.to_string(), - policy: Some(candidate.clone()), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(candidate.clone()), ..Default::default() })), ) @@ -8238,9 +8105,9 @@ mod tests { let response = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: sandbox_name.to_string(), - policy: Some(candidate.clone()), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(candidate.clone()), ..Default::default() })), ) @@ -8567,9 +8434,9 @@ mod tests { #[test] fn sandbox_caller_update_validation_allows_sandbox_policy_sync() { let req = UpdateConfigRequest { - name: "sandbox-1".to_string(), - policy: Some(ProtoSandboxPolicy::default()), + sandbox: "sandbox-1".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(ProtoSandboxPolicy::default()), ..Default::default() }; assert!(validate_sandbox_caller_update(&req).is_ok()); @@ -8589,10 +8456,10 @@ mod tests { #[test] fn sandbox_caller_update_validation_rejects_setting_mutation() { let req = UpdateConfigRequest { - name: "sandbox-1".to_string(), + sandbox: "sandbox-1".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), setting_key: "inference.model".to_string(), setting_value: Some(SettingValue { value: None }), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }; let err = validate_sandbox_caller_update(&req).unwrap_err(); @@ -8672,10 +8539,8 @@ mod tests { let error = handle_get_sandbox_logs( &state, with_user(Request::new(GetSandboxLogsRequest { - sandbox_id: "sandbox-b-id".to_string(), - workspace_scope: Some(openshell_core::proto::workspace_selector( - "default".to_string(), - )), + sandbox: "sandbox-b".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..GetSandboxLogsRequest::default() })), ) @@ -8769,6 +8634,7 @@ mod tests { &state, authed_request(UpdateConfigRequest { global: true, + sandbox: String::new(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), @@ -8781,6 +8647,7 @@ mod tests { &state, authed_request(GetSandboxPolicyStatusRequest { global: true, + sandbox: String::new(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), @@ -8793,6 +8660,7 @@ mod tests { &state, authed_request(ListSandboxPoliciesRequest { global: true, + sandbox: String::new(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), @@ -8982,7 +8850,8 @@ mod tests { } let req = with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-b".to_string(), + sandbox: "sb-b".to_string(), + workspace_scope: None, }), "sb-a", ); @@ -8992,6 +8861,24 @@ mod tests { assert_eq!(err.code(), Code::PermissionDenied); } + #[tokio::test] + async fn missing_sandbox_get_sandbox_config_matches_foreign_sandbox_denial() { + let state = test_server_state().await; + let req = with_sandbox( + Request::new(GetSandboxConfigRequest { + sandbox: "missing-sandbox".to_string(), + workspace_scope: None, + }), + "sb-a", + ); + + let err = handle_get_sandbox_config(&state, req) + .await + .expect_err("missing sandbox must not be distinguishable from a foreign sandbox"); + assert_eq!(err.code(), Code::PermissionDenied); + assert_eq!(err.message(), "sandbox not found or not owned by caller"); + } + #[tokio::test] async fn same_sandbox_get_sandbox_config_allowed() { use openshell_core::proto::{SandboxPhase, SandboxSpec}; @@ -9017,7 +8904,8 @@ mod tests { state.store.put_message(&sandbox).await.unwrap(); let req = with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-self".to_string(), + sandbox: "self".to_string(), + workspace_scope: None, }), "sb-self", ); @@ -9091,11 +8979,11 @@ mod tests { } let req = with_sandbox( Request::new(GetDraftPolicyRequest { - name: "sandbox-b".to_string(), - status_filter: String::new(), + sandbox: "sandbox-b".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), }), "sb-a", ); @@ -9106,13 +8994,13 @@ mod tests { } #[tokio::test] - async fn sandbox_update_config_missing_name_returns_permission_denied() { + async fn sandbox_update_config_missing_name_matches_foreign_sandbox_denial() { let state = test_server_state().await; let req = with_sandbox( Request::new(UpdateConfigRequest { - name: "missing-sandbox".to_string(), - policy: Some(ProtoSandboxPolicy::default()), + sandbox: "missing-sandbox".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(ProtoSandboxPolicy::default()), ..Default::default() }), "sb-a", @@ -9142,15 +9030,15 @@ mod tests { } #[tokio::test] - async fn sandbox_get_draft_policy_missing_name_returns_permission_denied() { + async fn sandbox_get_draft_policy_missing_name_matches_foreign_sandbox_denial() { let state = test_server_state().await; let req = with_sandbox( Request::new(GetDraftPolicyRequest { - name: "missing-sandbox".to_string(), - status_filter: String::new(), + sandbox: "missing-sandbox".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), }), "sb-a", ); @@ -9186,7 +9074,8 @@ mod tests { sandbox.set_phase(SandboxPhase::Provisioning as i32); state.store.put_message(&sandbox).await.unwrap(); let req = with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-x".to_string(), + sandbox: "x".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })); handle_get_sandbox_config(&state, req) .await @@ -9403,10 +9292,19 @@ mod tests { } async fn get_sandbox_policy(state: &Arc, sandbox_id: &str) -> ProtoSandboxPolicy { + let sandbox = state + .store + .get_message::(sandbox_id) + .await + .expect("sandbox lookup") + .expect("sandbox exists"); handle_get_sandbox_config( state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + sandbox.object_workspace(), + )), })), ) .await @@ -9520,7 +9418,8 @@ mod tests { let first = handle_get_sandbox_config( &state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-snapshot-consistency".to_string(), + sandbox: "snapshot-consistency".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })), ) .await @@ -9542,7 +9441,8 @@ mod tests { let second = handle_get_sandbox_config( &state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-snapshot-consistency".to_string(), + sandbox: "snapshot-consistency".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })), ) .await @@ -10142,7 +10042,8 @@ mod tests { let response = handle_get_sandbox_config( &state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-mcp-default-composed".to_string(), + sandbox: "mcp-default-composed".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })), ) .await @@ -10205,7 +10106,7 @@ mod tests { let error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "ambiguous-update".to_string(), + sandbox: "ambiguous-update".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10244,7 +10145,7 @@ mod tests { let error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "unattached-binding".to_string(), + sandbox: "unattached-binding".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10291,7 +10192,7 @@ mod tests { let error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "double-binding".to_string(), + sandbox: "double-binding".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10361,7 +10262,7 @@ mod tests { let l4_error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "endpointless-gating".to_string(), + sandbox: "endpointless-gating".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10386,7 +10287,7 @@ mod tests { let tls_error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "endpointless-gating".to_string(), + sandbox: "endpointless-gating".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10402,7 +10303,7 @@ mod tests { let merge_l4_error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "endpointless-gating".to_string(), + sandbox: "endpointless-gating".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10418,7 +10319,7 @@ mod tests { let merge_tls_error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "endpointless-gating".to_string(), + sandbox: "endpointless-gating".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10451,7 +10352,7 @@ mod tests { handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "endpointless-gating".to_string(), + sandbox: "endpointless-gating".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10486,7 +10387,7 @@ mod tests { let error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "signing-no-source".to_string(), + sandbox: "signing-no-source".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10534,7 +10435,7 @@ mod tests { let error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "signing-unbound-aws".to_string(), + sandbox: "signing-unbound-aws".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10573,7 +10474,7 @@ mod tests { handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "signing-bound-aws".to_string(), + sandbox: "signing-bound-aws".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10619,7 +10520,7 @@ mod tests { handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "signing-profile-endpoint".to_string(), + sandbox: "signing-profile-endpoint".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10651,7 +10552,7 @@ mod tests { let error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "signing-profile-mismatch".to_string(), + sandbox: "signing-profile-mismatch".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -10759,12 +10660,12 @@ mod tests { let error = super::super::sandbox::handle_attach_sandbox_provider( &state, authed_request(openshell_core::proto::AttachSandboxProviderRequest { - sandbox_name: "provider-ambiguity".to_string(), - provider_name: "candidate-provider".to_string(), - expected_resource_version: 0, + sandbox: "provider-ambiguity".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + provider_name: "candidate-provider".to_string(), + expected_resource_version: 0, }), ) .await @@ -10850,7 +10751,8 @@ mod tests { let error = handle_get_sandbox_config( &state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-invalid-composed-policy".to_string(), + sandbox: "invalid-composed-policy".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })), ) .await @@ -11341,7 +11243,8 @@ mod tests { let config = handle_get_sandbox_config( &state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-policy-binding".to_string(), + sandbox: "policy-binding".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })), ) .await @@ -11390,7 +11293,7 @@ mod tests { handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "policy-binding".to_string(), + sandbox: "policy-binding".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -11403,7 +11306,8 @@ mod tests { let next_config = handle_get_sandbox_config( &state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-policy-binding".to_string(), + sandbox: "policy-binding".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })), ) .await @@ -11447,7 +11351,7 @@ mod tests { handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "policy-binding".to_string(), + sandbox: "policy-binding".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -11460,7 +11364,8 @@ mod tests { let unbound_config = handle_get_sandbox_config( &state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-policy-binding".to_string(), + sandbox: "policy-binding".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })), ) .await @@ -12233,12 +12138,12 @@ mod tests { handle_attach_sandbox_provider( &state, with_user(Request::new(AttachSandboxProviderRequest { - sandbox_name: "attach-lifecycle".to_string(), - provider_name: "work-github".to_string(), - expected_resource_version: 0, + sandbox: "attach-lifecycle".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + provider_name: "work-github".to_string(), + expected_resource_version: 0, })), ) .await @@ -12273,12 +12178,12 @@ mod tests { handle_detach_sandbox_provider( &state, authed_request(DetachSandboxProviderRequest { - sandbox_name: "attach-lifecycle".to_string(), - provider_name: "work-github".to_string(), - expected_resource_version: 0, + sandbox: "attach-lifecycle".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + provider_name: "work-github".to_string(), + expected_resource_version: 0, }), ) .await @@ -12407,12 +12312,12 @@ mod tests { handle_attach_sandbox_provider( &state, with_user(Request::new(AttachSandboxProviderRequest { - sandbox_name: "attach-lifecycle".to_string(), - provider_name: "work-custom".to_string(), - expected_resource_version: 0, + sandbox: "attach-lifecycle".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + provider_name: "work-custom".to_string(), + expected_resource_version: 0, })), ) .await @@ -12450,12 +12355,12 @@ mod tests { handle_detach_sandbox_provider( &state, authed_request(DetachSandboxProviderRequest { - sandbox_name: "attach-lifecycle".to_string(), - provider_name: "work-custom".to_string(), - expected_resource_version: 0, + sandbox: "attach-lifecycle".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + provider_name: "work-custom".to_string(), + expected_resource_version: 0, }), ) .await @@ -12567,7 +12472,8 @@ mod tests { let response = handle_get_sandbox_config( &state, with_user(Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-global-profile".to_string(), + sandbox: "global-profile-sandbox".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), })), ) .await @@ -12761,7 +12667,7 @@ mod tests { let approved = handle_approve_all_draft_chunks( &state, with_user(Request::new(ApproveAllDraftChunksRequest { - name: sandbox_name.to_string(), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -12890,7 +12796,7 @@ mod tests { let approved = handle_approve_all_draft_chunks( &state, with_user(Request::new(ApproveAllDraftChunksRequest { - name: sandbox_name.to_string(), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -13093,11 +12999,11 @@ mod tests { let skipped = handle_approve_all_draft_chunks( &state, with_user(Request::new(ApproveAllDraftChunksRequest { - name: sandbox_name.to_string(), - include_security_flagged: false, + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + include_security_flagged: false, approvals: vec![openshell_core::proto::DraftChunkApproval { chunk_id: chunk_id.clone(), review_token: chunk.review_token.clone(), @@ -13123,11 +13029,11 @@ mod tests { let approved = handle_approve_all_draft_chunks( &state, with_user(Request::new(ApproveAllDraftChunksRequest { - name: sandbox_name.to_string(), - include_security_flagged: true, + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + include_security_flagged: true, approvals: vec![openshell_core::proto::DraftChunkApproval { chunk_id: chunk_id.clone(), review_token: chunk.review_token.clone(), @@ -13202,12 +13108,12 @@ mod tests { handle_edit_draft_chunk( &state, with_user(Request::new(EditDraftChunkRequest { - name: sandbox_name.to_string(), - chunk_id: chunk_id.clone(), - proposed_rule: Some(private_rule), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), + proposed_rule: Some(private_rule), })), ) .await @@ -13224,11 +13130,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.to_string(), - status_filter: String::new(), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -13243,11 +13149,11 @@ mod tests { let skipped = handle_approve_all_draft_chunks( &state, with_user(Request::new(ApproveAllDraftChunksRequest { - name: sandbox_name.to_string(), - include_security_flagged: false, + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + include_security_flagged: false, ..Default::default() })), ) @@ -13311,11 +13217,11 @@ mod tests { let skipped = handle_approve_all_draft_chunks( &state, with_user(Request::new(ApproveAllDraftChunksRequest { - name: sandbox_name.to_string(), - include_security_flagged: false, + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + include_security_flagged: false, ..Default::default() })), ) @@ -13383,11 +13289,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.to_string(), - status_filter: String::new(), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -13498,12 +13404,12 @@ mod tests { handle_edit_draft_chunk( &state, with_user(Request::new(EditDraftChunkRequest { - name: sandbox_name.to_string(), - chunk_id: chunk_id.clone(), - proposed_rule: Some(finding_rule), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), + proposed_rule: Some(finding_rule), })), ) .await @@ -13634,11 +13540,11 @@ mod tests { let draft_policy = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.clone(), - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -13656,11 +13562,11 @@ mod tests { let approve = handle_approve_draft_chunk( &state, authed_request(ApproveDraftChunkRequest { - name: sandbox_name.clone(), - chunk_id: chunk_id.clone(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), review_token, }), ) @@ -13673,7 +13579,7 @@ mod tests { let history_after_approve = handle_get_draft_history( &state, authed_request(GetDraftHistoryRequest { - name: sandbox_name.clone(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -13690,7 +13596,7 @@ mod tests { let policies_after_approve = handle_list_sandbox_policies( &state, authed_request(ListSandboxPoliciesRequest { - name: sandbox_name.clone(), + sandbox: sandbox_name.clone(), page_size: 10, page_token: String::new(), global: false, @@ -13708,11 +13614,11 @@ mod tests { let undo = handle_undo_draft_chunk( &state, authed_request(UndoDraftChunkRequest { - name: sandbox_name.clone(), - chunk_id: chunk_id.clone(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), }), ) .await @@ -13724,11 +13630,11 @@ mod tests { let draft_policy_after_undo = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.clone(), - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -13740,7 +13646,7 @@ mod tests { let history_after_undo = handle_get_draft_history( &state, authed_request(GetDraftHistoryRequest { - name: sandbox_name.clone(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -13755,7 +13661,7 @@ mod tests { let policies_after_undo = handle_list_sandbox_policies( &state, authed_request(ListSandboxPoliciesRequest { - name: sandbox_name.clone(), + sandbox: sandbox_name.clone(), page_size: 10, page_token: String::new(), global: false, @@ -13774,7 +13680,7 @@ mod tests { let cleared = handle_clear_draft_chunks( &state, authed_request(ClearDraftChunksRequest { - name: sandbox_name.clone(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -13788,11 +13694,11 @@ mod tests { let draft_policy_after_clear = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.clone(), - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -13803,7 +13709,7 @@ mod tests { let history_after_clear = handle_get_draft_history( &state, authed_request(GetDraftHistoryRequest { - name: sandbox_name, + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -13880,12 +13786,12 @@ mod tests { handle_reject_draft_chunk( &state, authed_request(RejectDraftChunkRequest { - name: sandbox_name.clone(), - chunk_id: chunk_id.clone(), - reason: guidance.to_string(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), + reason: guidance.to_string(), }), ) .await @@ -13894,11 +13800,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -14003,11 +13909,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -14118,11 +14024,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.clone(), - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -14196,11 +14102,11 @@ mod tests { let draft_after = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -14328,11 +14234,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -14435,7 +14341,7 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -14560,7 +14466,7 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -14692,11 +14598,11 @@ mod tests { let error = handle_approve_draft_chunk( &state, with_user(Request::new(ApproveDraftChunkRequest { - name: sandbox_name, - chunk_id: chunk_id.clone(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), review_token: before.review_token.clone(), })), ) @@ -14878,11 +14784,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -14984,11 +14890,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -15083,11 +14989,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -15173,11 +15079,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -15267,11 +15173,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -15364,11 +15270,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -15540,11 +15446,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.to_string(), - status_filter: String::new(), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -15615,11 +15521,11 @@ mod tests { let err = handle_approve_draft_chunk( &state, with_user(Request::new(ApproveDraftChunkRequest { - name: sandbox_name.to_string(), - chunk_id: chunk.id.clone(), + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk.id.clone(), ..Default::default() })), ) @@ -15728,11 +15634,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -15830,11 +15736,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -15921,11 +15827,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -16083,11 +15989,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.clone(), - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -16115,11 +16021,11 @@ mod tests { handle_approve_draft_chunk( &state, authed_request(ApproveDraftChunkRequest { - name: sandbox_name, - chunk_id, + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id, review_token: chunk.review_token.clone(), }), ) @@ -16305,11 +16211,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -16458,11 +16364,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.clone(), - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -16481,12 +16387,12 @@ mod tests { handle_reject_draft_chunk( &state, authed_request(RejectDraftChunkRequest { - name: sandbox_name, - chunk_id: second.accepted_chunk_ids[0].clone(), - reason: "redraft test".to_string(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: second.accepted_chunk_ids[0].clone(), + reason: "redraft test".to_string(), }), ) .await @@ -16568,11 +16474,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -16686,11 +16592,11 @@ mod tests { let after_first = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.clone(), - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -16709,11 +16615,11 @@ mod tests { let after_second = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name.clone(), - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -16953,12 +16859,12 @@ mod tests { handle_reject_draft_chunk( &state, authed_request(RejectDraftChunkRequest { - name: sandbox_name.clone(), - chunk_id: chunk_id.clone(), - reason: "scope too broad".to_string(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), + reason: "scope too broad".to_string(), }), ) .await @@ -16967,11 +16873,11 @@ mod tests { handle_approve_draft_chunk( &state, authed_request(ApproveDraftChunkRequest { - name: sandbox_name.clone(), - chunk_id: chunk_id.clone(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), review_token, }), ) @@ -16981,11 +16887,11 @@ mod tests { handle_undo_draft_chunk( &state, authed_request(UndoDraftChunkRequest { - name: sandbox_name.clone(), - chunk_id: chunk_id.clone(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), }), ) .await @@ -16994,11 +16900,11 @@ mod tests { let draft = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -17108,11 +17014,11 @@ mod tests { let draft_policy = handle_get_draft_policy( &state, with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_a.object_name().to_string(), - status_filter: String::new(), + sandbox: sandbox_a.object_name().to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + status_filter: String::new(), })), ) .await @@ -17125,11 +17031,11 @@ mod tests { let approve_err = handle_approve_draft_chunk( &state, authed_request(ApproveDraftChunkRequest { - name: other_name.clone(), - chunk_id: chunk_id.clone(), + sandbox: other_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), review_token: String::new(), }), ) @@ -17140,12 +17046,12 @@ mod tests { let reject_err = handle_reject_draft_chunk( &state, authed_request(RejectDraftChunkRequest { - name: other_name.clone(), - chunk_id: chunk_id.clone(), - reason: "wrong sandbox".to_string(), + sandbox: other_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), + reason: "wrong sandbox".to_string(), }), ) .await @@ -17155,12 +17061,12 @@ mod tests { let edit_err = handle_edit_draft_chunk( &state, authed_request(EditDraftChunkRequest { - name: other_name.clone(), - chunk_id: chunk_id.clone(), - proposed_rule: Some(proposed_rule.clone()), + sandbox: other_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), + proposed_rule: Some(proposed_rule.clone()), }), ) .await @@ -17170,11 +17076,11 @@ mod tests { handle_approve_draft_chunk( &state, authed_request(ApproveDraftChunkRequest { - name: sandbox_a.object_name().to_string(), - chunk_id: chunk_id.clone(), + sandbox: sandbox_a.object_name().to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id: chunk_id.clone(), review_token, }), ) @@ -17184,11 +17090,11 @@ mod tests { let undo_err = handle_undo_draft_chunk( &state, authed_request(UndoDraftChunkRequest { - name: other_name, - chunk_id, + sandbox: other_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + chunk_id, }), ) .await @@ -19124,7 +19030,8 @@ mod tests { let alpha_req = with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-alpha".to_string(), + sandbox: "work".to_string(), + workspace_scope: None, }), "sb-alpha", ); @@ -19136,7 +19043,8 @@ mod tests { let beta_req = with_sandbox( Request::new(GetSandboxConfigRequest { - sandbox_id: "sb-beta".to_string(), + sandbox: "work".to_string(), + workspace_scope: None, }), "sb-beta", ); @@ -19285,7 +19193,10 @@ mod tests { let response = handle_update_config( &state, authed_request(UpdateConfigRequest { - name: "test-sandbox".to_string(), + sandbox: "test-sandbox".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(new_policy), setting_key: String::new(), setting_value: None, @@ -19294,9 +19205,6 @@ mod tests { merge_operations: vec![], expected_resource_version: current_version, annotations: HashMap::new(), - workspace_scope: Some(openshell_core::proto::workspace_selector( - "default".to_string(), - )), }), ) .await @@ -19383,7 +19291,10 @@ mod tests { let response = handle_update_config( &state, authed_request(UpdateConfigRequest { - name: "annotated-backfill".to_string(), + sandbox: "annotated-backfill".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(ProtoSandboxPolicy::default()), setting_key: String::new(), setting_value: None, @@ -19392,9 +19303,6 @@ mod tests { merge_operations: vec![], expected_resource_version: current_version, annotations: annotations.clone(), - workspace_scope: Some(openshell_core::proto::workspace_selector( - "default".to_string(), - )), }), ) .await @@ -19461,13 +19369,13 @@ mod tests { let response = handle_update_config( &state, authed_request(UpdateConfigRequest { - name: "same-hash".to_string(), + sandbox: "same-hash".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(policy), annotations: HashMap::from([( "openshell.nvidia.com/policy-signature".to_string(), "same-hash-signature".to_string(), )]), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -19548,10 +19456,10 @@ mod tests { let first = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "idempotent-provenance".to_string(), + sandbox: "idempotent-provenance".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(policy.clone()), annotations: annotations.clone(), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19561,10 +19469,10 @@ mod tests { let second = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "idempotent-provenance".to_string(), + sandbox: "idempotent-provenance".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(policy), annotations: annotations.clone(), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19616,9 +19524,9 @@ mod tests { let response = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "preserve-full".to_string(), - policy: Some(updated), + sandbox: "preserve-full".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(updated), ..Default::default() })), ) @@ -19665,7 +19573,8 @@ mod tests { let response = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "preserve-merge".to_string(), + sandbox: ("preserve-merge").to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), merge_operations: vec![PolicyMergeOperation { operation: Some(policy_merge_operation::Operation::AddRule( openshell_core::proto::AddNetworkRule { @@ -19682,7 +19591,6 @@ mod tests { }, )), }], - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19737,7 +19645,8 @@ mod tests { let response = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "merge-provenance".to_string(), + sandbox: ("merge-provenance").to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), merge_operations: vec![PolicyMergeOperation { operation: Some(policy_merge_operation::Operation::AddRule( openshell_core::proto::AddNetworkRule { @@ -19755,7 +19664,6 @@ mod tests { )), }], annotations: provenance.clone(), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19814,10 +19722,10 @@ mod tests { let response = handle_update_config( &state, authed_request(UpdateConfigRequest { - name: "preserve-backfill".to_string(), + sandbox: "preserve-backfill".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(ProtoSandboxPolicy::default()), expected_resource_version: current_version, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -19889,12 +19797,12 @@ mod tests { let error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: sandbox_name.to_string(), - policy: Some(unsafe_replacement), - expected_resource_version: current_version, + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + policy: Some(unsafe_replacement), + expected_resource_version: current_version, ..Default::default() })), ) @@ -19967,12 +19875,12 @@ mod tests { let error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: sandbox_name.to_string(), - policy: Some(mcp_policy_with_versions(versions)), - expected_resource_version: current_version, + sandbox: sandbox_name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + policy: Some(mcp_policy_with_versions(versions)), + expected_resource_version: current_version, ..Default::default() })), ) @@ -20051,12 +19959,12 @@ mod tests { let response = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: sandbox_name.clone(), - policy: Some(policy), - expected_resource_version: current_version, + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + policy: Some(policy), + expected_resource_version: current_version, ..Default::default() })), ) @@ -20138,16 +20046,16 @@ mod tests { handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: sandbox_name.to_string(), + sandbox: sandbox_name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(mcp_policy_with_versions(&[ "2025-11-25", "2025-06-18", "2025-03-26", ])), expected_resource_version: current_version, - workspace_scope: Some(openshell_core::proto::workspace_selector( - "default".to_string(), - )), ..Default::default() })), ) @@ -20219,10 +20127,10 @@ mod tests { let err = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "invalid-annotation".to_string(), + sandbox: "invalid-annotation".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(ProtoSandboxPolicy::default()), annotations: HashMap::from([("bad key".to_string(), "value".to_string())]), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -20250,12 +20158,12 @@ mod tests { let err = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { - name: "user-reserved-key".to_string(), + sandbox: "user-reserved-key".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(test_policy_with_rule( "_provider_work_github", "api.github.com", )), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -20319,10 +20227,10 @@ mod tests { &state, with_sandbox( Request::new(UpdateConfigRequest { - name: "sync-strip".to_string(), + sandbox: "sync-strip".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(synced_policy), expected_resource_version: current_version, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), "sb-sync-strip", @@ -20415,7 +20323,10 @@ mod tests { let err = handle_update_config( &state, authed_request(UpdateConfigRequest { - name: "test-sandbox".to_string(), + sandbox: "test-sandbox".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(new_policy), setting_key: String::new(), setting_value: None, @@ -20424,9 +20335,6 @@ mod tests { merge_operations: vec![], expected_resource_version: 99, // stale version annotations: HashMap::new(), - workspace_scope: Some(openshell_core::proto::workspace_selector( - "default".to_string(), - )), }), ) .await @@ -20516,7 +20424,10 @@ mod tests { handle_update_config( &state_clone, authed_request(UpdateConfigRequest { - name: "test-sandbox".to_string(), + sandbox: "test-sandbox".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(new_policy), setting_key: String::new(), setting_value: None, @@ -20525,9 +20436,6 @@ mod tests { merge_operations: vec![], expected_resource_version: initial_version, annotations: HashMap::new(), - workspace_scope: Some(openshell_core::proto::workspace_selector( - "default".to_string(), - )), }), ) .await @@ -20588,7 +20496,7 @@ mod tests { /// when targeting a workspace that does not exist. Returning `NOT_FOUND` /// would create a CWE-203 workspace-name oracle. #[tokio::test] - async fn non_member_gets_permission_denied_not_workspace_oracle() { + async fn non_member_gets_gets_not_found_without_sandbox_oracle() { let mut state = test_server_state().await; Arc::get_mut(&mut state).unwrap().admin_role = "openshell-admin".to_string(); @@ -20609,6 +20517,7 @@ mod tests { let err = handle_get_sandbox_policy_status( &state, non_member_request(GetSandboxPolicyStatusRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20617,14 +20526,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_get_sandbox_policy_status should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_get_sandbox_policy_status should return NotFound, got {:?}", err.code() ); let err = handle_list_sandbox_policies( &state, non_member_request(ListSandboxPoliciesRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20633,14 +20543,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_list_sandbox_policies should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_list_sandbox_policies should return NotFound, got {:?}", err.code() ); let err = handle_update_config( &state, non_member_request(UpdateConfigRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20649,14 +20560,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_update_config should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_update_config should return NotFound, got {:?}", err.code() ); let err = handle_get_draft_policy( &state, non_member_request(GetDraftPolicyRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20665,14 +20577,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_get_draft_policy should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_get_draft_policy should return NotFound, got {:?}", err.code() ); let err = handle_approve_draft_chunk( &state, non_member_request(ApproveDraftChunkRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20681,14 +20594,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_approve_draft_chunk should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_approve_draft_chunk should return NotFound, got {:?}", err.code() ); let err = handle_reject_draft_chunk( &state, non_member_request(RejectDraftChunkRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20697,14 +20611,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_reject_draft_chunk should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_reject_draft_chunk should return NotFound, got {:?}", err.code() ); let err = handle_approve_all_draft_chunks( &state, non_member_request(ApproveAllDraftChunksRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20713,14 +20628,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_approve_all_draft_chunks should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_approve_all_draft_chunks should return NotFound, got {:?}", err.code() ); let err = handle_edit_draft_chunk( &state, non_member_request(EditDraftChunkRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20729,14 +20645,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_edit_draft_chunk should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_edit_draft_chunk should return NotFound, got {:?}", err.code() ); let err = handle_undo_draft_chunk( &state, non_member_request(UndoDraftChunkRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -20745,48 +20662,48 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_undo_draft_chunk should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_undo_draft_chunk should return NotFound, got {:?}", err.code() ); let err = handle_clear_draft_chunks( &state, non_member_request(ClearDraftChunksRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), - ..Default::default() }), ) .await .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_clear_draft_chunks should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_clear_draft_chunks should return NotFound, got {:?}", err.code() ); let err = handle_get_draft_history( &state, non_member_request(GetDraftHistoryRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), - ..Default::default() }), ) .await .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_get_draft_history should return PermissionDenied, got {:?}", + Code::NotFound, + "handle_get_draft_history should return NotFound, got {:?}", err.code() ); } - /// ID-only policy handlers hide cross-workspace resources, while requests - /// with an explicit workspace selector authorize that selector first. + /// Name-based policy handlers hide cross-workspace resources after + /// authorizing the explicit workspace selector. #[tokio::test] - async fn id_based_policy_handlers_hide_cross_workspace_sandboxes() { + async fn name_based_policy_handlers_hide_cross_workspace_sandboxes() { let mut state = test_server_state().await; Arc::get_mut(&mut state).unwrap().admin_role = "openshell-admin".to_string(); @@ -20817,7 +20734,8 @@ mod tests { let err = handle_get_sandbox_config( &state, non_member_request(GetSandboxConfigRequest { - sandbox_id: "sandbox-other".into(), + sandbox: "other".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("other-workspace")), }), ) .await @@ -20832,8 +20750,8 @@ mod tests { let err = handle_get_sandbox_logs( &state, non_member_request(GetSandboxLogsRequest { - sandbox_id: "sandbox-other".into(), - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + sandbox: "other".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("other-workspace")), ..Default::default() }), ) @@ -20841,8 +20759,8 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_get_sandbox_logs must authorize the selected workspace before lookup" + Code::NotFound, + "handle_get_sandbox_logs must hide unauthorized sandbox existence" ); } diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 0500b668e8..79f2ac1140 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -6652,12 +6652,12 @@ mod tests { let attached = super::super::sandbox::handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "sandbox-custom".to_string(), - provider_name: "custom-provider".to_string(), - expected_resource_version: 0, + sandbox: "sandbox-custom".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + provider_name: "custom-provider".to_string(), + expected_resource_version: 0, }), ) .await diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index d428e64569..855e84853b 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -124,33 +124,76 @@ impl Drop for WatchSandboxStream { } } -/// Fetch a sandbox by ID and authorize the caller in one step, returning -/// `NOT_FOUND` for both missing and unauthorized sandboxes so that callers -/// cannot distinguish the two cases (CWE-203). -pub(super) async fn fetch_and_authorize_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. +pub(super) async fn resolve_and_authorize_sandbox_name( state: &Arc, principal: &crate::auth::principal::Principal, - sandbox_id: &str, + sandbox_name: &str, + workspace_scope: Option<&openshell_core::proto::WorkspaceSelector>, + min_role: MinWorkspaceRole, ) -> Result { - let sandbox = state - .store - .get_message::(sandbox_id) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; + if sandbox_name.is_empty() { + return Err(Status::invalid_argument("sandbox is required")); + } + let sandbox = match principal { + crate::auth::principal::Principal::Sandbox(sandbox_principal) => { + let sandbox = state + .store + .get_message::(&sandbox_principal.sandbox_id) + .await + .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))?; + sandbox.filter(|sandbox| { + sandbox.metadata.as_ref().is_some_and(|metadata| { + metadata.name == sandbox_name + && workspace_scope.is_none_or(|scope| { + crate::auth::workspace_authz::selected_workspace_name(Some(scope)) + .is_ok_and(|workspace| workspace == sandbox.object_workspace()) + }) + }) + }) + } + crate::auth::principal::Principal::User(_) + | crate::auth::principal::Principal::Anonymous => { + let workspace = crate::auth::workspace_authz::selected_workspace_name(workspace_scope)?; + state + .store + .get_message_by_name::(workspace, sandbox_name) + .await + .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? + } + }; + let sandbox = match sandbox { + Some(sandbox) => sandbox, + None if matches!(principal, crate::auth::principal::Principal::Sandbox(_)) => { + return Err(Status::permission_denied( + "sandbox not found or not owned by caller", + )); + } + None => return Err(Status::not_found("sandbox not found")), + }; + authorize_sandbox_workspace( &state.store, &state.admin_role, principal, sandbox.object_workspace(), - MinWorkspaceRole::User, + min_role, ) .await - .map_err(|e| { - if e.code() == tonic::Code::PermissionDenied { + .map_err(|error| { + if error.code() == tonic::Code::PermissionDenied { Status::not_found("sandbox not found") } else { - e + error + } + })?; + crate::auth::guard::ensure_sandbox_scope(principal, sandbox.object_id()).map_err(|error| { + if error.code() == tonic::Code::PermissionDenied { + Status::permission_denied("sandbox not found or not owned by caller") + } else { + error } })?; Ok(sandbox) @@ -666,28 +709,14 @@ pub(super) async fn handle_get_sandbox( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))?; - - let sandbox = sandbox.ok_or_else(|| Status::not_found("sandbox not found"))?; Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), })) @@ -1024,18 +1053,15 @@ pub(super) async fn handle_list_sandbox_providers( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - let sandbox = sandbox_by_name(state, &workspace, &req.sandbox_name).await?; + let workspace = sandbox.object_workspace().to_string(); let providers = providers_for_sandbox(state, &sandbox, &workspace).await?; Ok(Response::new(ListSandboxProvidersResponse { providers })) } @@ -1046,17 +1072,18 @@ pub(super) async fn handle_attach_sandbox_provider( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = resolve_and_authorize_sandbox_name( + state, &principal, + &request.sandbox, request.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .ensure_active()?; + let workspace = + super::workspace::resolve_workspace(state.store.as_ref(), sandbox.object_workspace()) + .await? + .ensure_active()?; if request.provider_name.is_empty() { return Err(Status::invalid_argument("provider_name is required")); } @@ -1085,7 +1112,7 @@ pub(super) async fn handle_attach_sandbox_provider( })?; let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; - let sandbox = sandbox_by_name(state, &workspace, &request.sandbox_name).await?; + let sandbox_name = sandbox.object_name().to_string(); let sandbox_id = sandbox .metadata .as_ref() @@ -1120,7 +1147,7 @@ pub(super) async fn handle_attach_sandbox_provider( { candidate_spec.providers.push(request.provider_name.clone()); } - validate_sandbox_spec(&request.sandbox_name, &candidate_spec)?; + validate_sandbox_spec(&sandbox_name, &candidate_spec)?; let provider_profile_catalog = state .provider_profile_sources .snapshot_catalog(state.store.as_ref(), &workspace) @@ -1177,7 +1204,7 @@ pub(super) async fn handle_attach_sandbox_provider( let attached = attached.load(Ordering::Relaxed); info!( - sandbox_name = %request.sandbox_name, + sandbox_name = %sandbox_name, provider_name = %request.provider_name, attached, "AttachSandboxProvider request completed successfully" @@ -1195,17 +1222,15 @@ pub(super) async fn handle_detach_sandbox_provider( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = resolve_and_authorize_sandbox_name( + state, &principal, + &request.sandbox, request.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; + let workspace = sandbox.object_workspace().to_string(); if request.provider_name.is_empty() { return Err(Status::invalid_argument("provider_name is required")); } @@ -1220,7 +1245,7 @@ pub(super) async fn handle_detach_sandbox_provider( } let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; - let sandbox = sandbox_by_name(state, &workspace, &request.sandbox_name).await?; + let sandbox_name = sandbox.object_name().to_string(); let sandbox_id = sandbox .metadata .as_ref() @@ -1276,7 +1301,7 @@ pub(super) async fn handle_detach_sandbox_provider( let detached = detached.load(Ordering::Relaxed); info!( - sandbox_name = %request.sandbox_name, + sandbox_name = %sandbox_name, provider_name = %request.provider_name, detached, "DetachSandboxProvider request completed successfully" @@ -1311,21 +1336,16 @@ async fn handle_delete_sandbox_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let name = req.name; - if name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; + let workspace = sandbox.object_workspace().to_string(); + let name = sandbox.object_name().to_string(); let result = state.compute.delete_sandbox(&workspace, &name).await?; if result.deleted { @@ -1360,22 +1380,18 @@ async fn handle_stop_sandbox_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let resolved = resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - let sandbox = state.compute.stop_sandbox(&workspace, &req.name).await?; - info!(sandbox_name = %req.name, "StopSandbox request completed successfully"); + let workspace = resolved.object_workspace(); + let name = resolved.object_name(); + let sandbox = state.compute.stop_sandbox(workspace, name).await?; + info!(sandbox_name = %name, "StopSandbox request completed successfully"); Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), })) @@ -1404,44 +1420,23 @@ async fn handle_start_sandbox_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - if req.name.is_empty() { - return Err(Status::invalid_argument("name is required")); - } - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let resolved = resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - let sandbox = state.compute.start_sandbox(&workspace, &req.name).await?; - info!(sandbox_name = %req.name, "StartSandbox request completed successfully"); + let workspace = resolved.object_workspace(); + let name = resolved.object_name(); + let sandbox = state.compute.start_sandbox(workspace, name).await?; + info!(sandbox_name = %name, "StartSandbox request completed successfully"); Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), })) } -async fn sandbox_by_name( - state: &Arc, - workspace: &str, - name: &str, -) -> Result { - if name.is_empty() { - return Err(Status::invalid_argument("sandbox_name is required")); - } - - state - .store - .get_message_by_name::(workspace, name) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found")) -} - async fn providers_for_sandbox( state: &Arc, sandbox: &Sandbox, @@ -1490,12 +1485,15 @@ pub(super) async fn handle_watch_sandbox( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - if req.id.is_empty() { - return Err(Status::invalid_argument("id is required")); - } - let sandbox_id = req.id.clone(); - - let _sandbox = fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?; + let sandbox = resolve_and_authorize_sandbox_name( + state, + &principal, + &req.sandbox, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; + let sandbox_id = sandbox.object_id().to_string(); let follow_status = req.follow_status; let follow_logs = req.follow_logs; @@ -1749,9 +1747,6 @@ pub(super) async fn handle_exec_sandbox( let principal = super::extract_principal(&request)?; let req = request.into_inner(); - if req.sandbox_id.is_empty() { - return Err(Status::invalid_argument("sandbox_id is required")); - } if req.command.is_empty() { return Err(Status::invalid_argument("command is required")); } @@ -1762,7 +1757,14 @@ pub(super) async fn handle_exec_sandbox( } validate_exec_request_fields(&req)?; - let sandbox = fetch_and_authorize_sandbox(state, &principal, &req.sandbox_id).await?; + let sandbox = resolve_and_authorize_sandbox_name( + state, + &principal, + &req.sandbox, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; if SandboxPhase::try_from(sandbox.phase()).ok() != Some(SandboxPhase::Ready) { return Err(Status::failed_precondition("sandbox is not ready")); @@ -1878,7 +1880,14 @@ pub(super) async fn handle_forward_tcp( let target = validate_tcp_forward_init(&init)?; - let sandbox = fetch_and_authorize_sandbox(state, &principal, &init.sandbox_id).await?; + let sandbox = resolve_and_authorize_sandbox_name( + state, + &principal, + &init.sandbox, + init.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; // The main process may finish between minting the SSH token and opening // its transport. Keep the relay reachable until terminal delivery is @@ -2036,10 +2045,6 @@ fn decrement_ssh_connection_count(counts: &std::sync::Mutex } fn validate_tcp_forward_init(init: &TcpForwardInit) -> Result { - if init.sandbox_id.is_empty() { - return Err(Status::invalid_argument("sandbox_id is required")); - } - if let Some(target) = init.target.as_ref() { return match target { tcp_forward_init::Target::Ssh(_) => { @@ -2171,9 +2176,6 @@ fn validate_interactive_exec_start( )); }; - if req.sandbox_id.is_empty() { - return Err(Status::invalid_argument("sandbox_id is required")); - } if req.command.is_empty() { return Err(Status::invalid_argument("command is required")); } @@ -2203,7 +2205,14 @@ pub(super) async fn handle_exec_sandbox_interactive( let req = validate_interactive_exec_start(first_msg)?; - let sandbox = fetch_and_authorize_sandbox(state, &principal, &req.sandbox_id).await?; + let sandbox = resolve_and_authorize_sandbox_name( + state, + &principal, + &req.sandbox, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; if SandboxPhase::try_from(sandbox.phase()).ok() != Some(SandboxPhase::Ready) { return Err(Status::failed_precondition("sandbox is not ready")); @@ -2281,11 +2290,15 @@ pub(super) async fn handle_create_ssh_session( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - if req.sandbox_id.is_empty() { - return Err(Status::invalid_argument("sandbox_id is required")); - } - - let sandbox = fetch_and_authorize_sandbox(state, &principal, &req.sandbox_id).await?; + let sandbox = resolve_and_authorize_sandbox_name( + state, + &principal, + &req.sandbox, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; + let sandbox_id = sandbox.object_id().to_string(); if !sandbox_relay_reachable(state, &sandbox) { return Err(Status::failed_precondition("sandbox is not ready")); @@ -2309,7 +2322,7 @@ pub(super) async fn handle_create_ssh_session( workspace: sandbox.object_workspace().to_string(), deletion_timestamp_ms: 0, }), - sandbox_id: req.sandbox_id.clone(), + sandbox_id: sandbox_id.clone(), token: token.clone(), revoked: false, expires_at_ms, @@ -2350,7 +2363,7 @@ pub(super) async fn handle_create_ssh_session( }; Ok(Response::new(CreateSshSessionResponse { - sandbox_id: req.sandbox_id, + sandbox_id, token, gateway_host, gateway_port: gateway_port.into(), @@ -3252,7 +3265,8 @@ mod tests { fn build_remote_exec_command_basic() { use openshell_core::proto::ExecSandboxRequest; let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec!["ls".to_string(), "-la".to_string()], ..Default::default() }; @@ -3263,7 +3277,8 @@ mod tests { fn build_remote_exec_command_with_env_and_workdir() { use openshell_core::proto::ExecSandboxRequest; let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec![ "python".to_string(), "-c".to_string(), @@ -3283,7 +3298,8 @@ mod tests { fn build_remote_exec_command_rejects_null_bytes_in_args() { use openshell_core::proto::ExecSandboxRequest; let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec!["echo".to_string(), "hello\x00world".to_string()], ..Default::default() }; @@ -3294,7 +3310,8 @@ mod tests { fn build_remote_exec_command_rejects_newlines_in_workdir() { use openshell_core::proto::ExecSandboxRequest; let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec!["ls".to_string()], workdir: "/tmp\nmalicious".to_string(), ..Default::default() @@ -3307,7 +3324,8 @@ mod tests { fn build_remote_exec_command_accepts_multiline_script() { use openshell_core::proto::ExecSandboxRequest; let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec![ "python3".to_string(), "-c".to_string(), @@ -3324,7 +3342,8 @@ mod tests { fn build_remote_exec_command_multiline_with_single_quotes() { use openshell_core::proto::ExecSandboxRequest; let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec![ "python3".to_string(), "-c".to_string(), @@ -3344,7 +3363,8 @@ mod tests { fn tcp_forward_init_allows_loopback_targets() { for host in ["127.0.0.1", "::1", "localhost"] { let init = TcpForwardInit { - sandbox_id: "sbx".to_string(), + sandbox: "sbx".to_string(), + workspace_scope: None, service_id: String::new(), target: Some(tcp_forward_init::Target::Tcp(TcpRelayTarget { host: host.to_string(), @@ -3359,7 +3379,8 @@ mod tests { #[test] fn tcp_forward_init_allows_ssh_target() { let init = TcpForwardInit { - sandbox_id: "sbx".to_string(), + sandbox: "sbx".to_string(), + workspace_scope: None, target: Some(tcp_forward_init::Target::Ssh(SshRelayTarget::default())), ..Default::default() }; @@ -3372,7 +3393,8 @@ mod tests { #[test] fn tcp_forward_init_rejects_non_loopback_targets() { let init = TcpForwardInit { - sandbox_id: "sbx".to_string(), + sandbox: "sbx".to_string(), + workspace_scope: None, service_id: String::new(), target: Some(tcp_forward_init::Target::Tcp(TcpRelayTarget { host: "example.com".to_string(), @@ -3391,7 +3413,8 @@ mod tests { #[test] fn tcp_forward_init_rejects_invalid_port() { let init = TcpForwardInit { - sandbox_id: "sbx".to_string(), + sandbox: "sbx".to_string(), + workspace_scope: None, service_id: String::new(), target: Some(tcp_forward_init::Target::Tcp(TcpRelayTarget { host: "127.0.0.1".to_string(), @@ -3410,7 +3433,8 @@ mod tests { #[test] fn tcp_forward_init_requires_target() { let init = TcpForwardInit { - sandbox_id: "sbx".to_string(), + sandbox: "sbx".to_string(), + workspace_scope: None, ..Default::default() }; assert_eq!( @@ -3573,7 +3597,8 @@ mod tests { handle_watch_sandbox( &state, authed_request(WatchSandboxRequest { - id: sandbox.object_id().to_string(), + sandbox: "watched".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -3616,7 +3641,7 @@ mod tests { handle_delete_sandbox_inner( &delete_state, authed_request(DeleteSandboxRequest { - name: "reused-name".to_string(), + sandbox: "reused-name".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -3675,10 +3700,10 @@ mod tests { let response = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3716,10 +3741,10 @@ mod tests { let response = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3754,10 +3779,10 @@ mod tests { let response = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3805,10 +3830,10 @@ mod tests { let response = handle_detach_sandbox_provider( &state, authed_request(DetachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3830,10 +3855,10 @@ mod tests { let response = handle_detach_sandbox_provider( &state, authed_request(DetachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3877,10 +3902,10 @@ mod tests { let error = handle_detach_sandbox_provider( &state, authed_request(DetachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "work-gcp".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3917,7 +3942,7 @@ mod tests { let response = handle_list_sandbox_providers( &state, authed_request(ListSandboxProvidersRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) @@ -3945,10 +3970,10 @@ mod tests { let err = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "missing".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3998,7 +4023,7 @@ mod tests { } #[test] - fn interactive_exec_rejects_missing_sandbox_id() { + fn interactive_exec_rejects_missing_sandbox_name() { use openshell_core::proto::exec_sandbox_input; let msg = ExecSandboxInput { payload: Some(exec_sandbox_input::Payload::Start(ExecSandboxRequest { @@ -4008,7 +4033,7 @@ mod tests { }; let err = validate_interactive_exec_start(Some(msg)).unwrap_err(); assert_eq!(err.code(), tonic::Code::InvalidArgument); - assert!(err.message().contains("sandbox_id")); + assert!(err.message().contains("sandbox")); } #[test] @@ -4016,7 +4041,8 @@ mod tests { use openshell_core::proto::exec_sandbox_input; let msg = ExecSandboxInput { payload: Some(exec_sandbox_input::Payload::Start(ExecSandboxRequest { - sandbox_id: "test-id".to_string(), + sandbox: "test-id".to_string(), + workspace_scope: None, ..Default::default() })), }; @@ -4030,7 +4056,8 @@ mod tests { use openshell_core::proto::exec_sandbox_input; let msg = ExecSandboxInput { payload: Some(exec_sandbox_input::Payload::Start(ExecSandboxRequest { - sandbox_id: "test-id".to_string(), + sandbox: "test-id".to_string(), + workspace_scope: None, command: vec!["bash".to_string()], environment: std::iter::once(("bad key!".to_string(), "val".to_string())).collect(), ..Default::default() @@ -4046,7 +4073,8 @@ mod tests { use openshell_core::proto::exec_sandbox_input; let msg = ExecSandboxInput { payload: Some(exec_sandbox_input::Payload::Start(ExecSandboxRequest { - sandbox_id: "test-id".to_string(), + sandbox: "test-id".to_string(), + workspace_scope: None, command: vec!["bash".to_string()], tty: true, cols: 120, @@ -4055,7 +4083,7 @@ mod tests { })), }; let req = validate_interactive_exec_start(Some(msg)).unwrap(); - assert_eq!(req.sandbox_id, "test-id"); + assert_eq!(req.sandbox, "test-id"); assert_eq!(req.command, vec!["bash"]); assert!(req.tty); assert_eq!(req.cols, 120); @@ -4067,14 +4095,15 @@ mod tests { let state = test_server_state().await; let req = ExecSandboxRequest { - sandbox_id: "nonexistent".to_string(), + sandbox: "nonexistent".to_string(), + workspace_scope: None, command: vec!["bash".to_string()], tty: true, ..Default::default() }; let sandbox_result = state .store - .get_message::(&req.sandbox_id) + .get_message_by_name::("default", &req.sandbox) .await .unwrap(); assert!(sandbox_result.is_none()); @@ -4570,7 +4599,7 @@ mod tests { let fetched = handle_get_sandbox( &state, authed_request(GetSandboxRequest { - name: "annotated".to_string(), + sandbox: "annotated".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) @@ -4634,7 +4663,7 @@ mod tests { let fetched_process = handle_get_sandbox( &state, authed_request(GetSandboxRequest { - name: "partial-id".to_string(), + sandbox: "partial-id".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) @@ -5647,10 +5676,10 @@ mod tests { let err = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "provider-b".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5694,10 +5723,10 @@ mod tests { let response = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "provider-31".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5749,10 +5778,10 @@ mod tests { let err = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "provider-32".to_string(), expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5796,10 +5825,10 @@ mod tests { let err = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: long_name, expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5823,10 +5852,10 @@ mod tests { let err = handle_detach_sandbox_provider( &state, authed_request(DetachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: long_name, expected_resource_version: 0, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5852,7 +5881,8 @@ mod tests { handle_create_ssh_session( &state1, authed_request(CreateSshSessionRequest { - sandbox_id: "sandbox-work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5863,7 +5893,8 @@ mod tests { handle_create_ssh_session( &state2, authed_request(CreateSshSessionRequest { - sandbox_id: "sandbox-work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5916,7 +5947,8 @@ mod tests { let response = handle_create_ssh_session( &state, authed_request(CreateSshSessionRequest { - sandbox_id: "sandbox-work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await; @@ -5944,7 +5976,8 @@ mod tests { let response = handle_create_ssh_session( &state, authed_request(CreateSshSessionRequest { - sandbox_id: "sandbox-work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6022,10 +6055,10 @@ mod tests { let response = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "github".to_string(), expected_resource_version: current_version, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6074,10 +6107,10 @@ mod tests { let err = handle_attach_sandbox_provider( &state, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "github".to_string(), expected_resource_version: 99, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6137,10 +6170,10 @@ mod tests { let response = handle_detach_sandbox_provider( &state, authed_request(DetachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "github".to_string(), expected_resource_version: current_version, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6189,10 +6222,10 @@ mod tests { let err = handle_detach_sandbox_provider( &state, authed_request(DetachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: "github".to_string(), expected_resource_version: 99, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6270,10 +6303,10 @@ mod tests { handle_attach_sandbox_provider( &state_clone, authed_request(AttachSandboxProviderRequest { - sandbox_name: "work".to_string(), + sandbox: "work".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), provider_name: format!("provider-{i}"), expected_resource_version: initial_version, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6356,7 +6389,7 @@ mod tests { let got = handle_get_sandbox( &state, authed_request(GetSandboxRequest { - name: "shared-name".to_string(), + sandbox: "shared-name".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), @@ -6371,7 +6404,7 @@ mod tests { let got = handle_get_sandbox( &state, authed_request(GetSandboxRequest { - name: "shared-name".to_string(), + sandbox: "shared-name".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "beta".to_string(), )), @@ -6446,7 +6479,7 @@ mod tests { let got = handle_get_sandbox( &state, authed_request(GetSandboxRequest { - name: "shared-name".to_string(), + sandbox: "shared-name".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "beta".to_string(), )), @@ -6518,10 +6551,9 @@ mod tests { ); } - /// Non-members must receive `PERMISSION_DENIED` — never `NOT_FOUND` — when - /// calling workspace-scoped sandbox RPCs with a workspace they do not belong - /// to. If `authorize_workspace` ran *after* a store lookup the error code - /// would leak whether the workspace name exists (CWE-203 oracle). + /// Workspace collection operations reject non-members, while operations on + /// a sandbox reference hide both missing and unauthorized sandboxes as + /// `NOT_FOUND` to avoid an object-existence oracle. #[tokio::test] async fn non_member_gets_permission_denied_not_workspace_oracle() { use crate::auth::identity::{Identity, IdentityProvider}; @@ -6569,16 +6601,16 @@ mod tests { let err = handle_get_sandbox( &state, non_member_request(GetSandboxRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), - name: "any".into(), }), ) .await .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_get_sandbox should reject non-members with PermissionDenied" + Code::NotFound, + "handle_get_sandbox should hide unauthorized sandbox existence" ); // --- handle_list_sandboxes --- @@ -6601,22 +6633,23 @@ mod tests { let err = handle_list_sandbox_providers( &state, non_member_request(ListSandboxProvidersRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), - ..Default::default() }), ) .await .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_list_sandbox_providers should reject non-members with PermissionDenied" + Code::NotFound, + "handle_list_sandbox_providers should hide unauthorized sandbox existence" ); // --- handle_attach_sandbox_provider --- let err = handle_attach_sandbox_provider( &state, non_member_request(AttachSandboxProviderRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -6625,14 +6658,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_attach_sandbox_provider should reject non-members with PermissionDenied" + Code::NotFound, + "handle_attach_sandbox_provider should hide unauthorized sandbox existence" ); // --- handle_detach_sandbox_provider --- let err = handle_detach_sandbox_provider( &state, non_member_request(DetachSandboxProviderRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -6641,8 +6675,8 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_detach_sandbox_provider should reject non-members with PermissionDenied" + Code::NotFound, + "handle_detach_sandbox_provider should hide unauthorized sandbox existence" ); // --- handle_delete_sandbox --- @@ -6650,49 +6684,49 @@ mod tests { let err = handle_delete_sandbox( &state, non_member_request(DeleteSandboxRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), - name: "any".into(), }), ) .await .unwrap_err(); assert_eq!( err.code(), - Code::PermissionDenied, - "handle_delete_sandbox should reject non-members with PermissionDenied" + Code::NotFound, + "handle_delete_sandbox should hide unauthorized sandbox existence" ); for result in [ handle_stop_sandbox( &state, non_member_request(StopSandboxRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), - name: "any".into(), }), ) .await, handle_start_sandbox( &state, non_member_request(StartSandboxRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), - name: "any".into(), }), ) .await, ] { assert_eq!( result.unwrap_err().code(), - Code::PermissionDenied, - "lifecycle handlers should reject non-members" + Code::NotFound, + "lifecycle handlers should hide unauthorized sandbox existence" ); } } - /// ID-based data-plane handlers must return `NOT_FOUND` — never + /// Name-based data-plane handlers must return `NOT_FOUND` — never /// `PERMISSION_DENIED` — when the caller lacks workspace access, so that /// cross-workspace sandbox existence cannot be inferred (CWE-203). #[tokio::test] - async fn id_based_handlers_hide_cross_workspace_sandboxes() { + async fn name_based_handlers_hide_cross_workspace_sandboxes() { use crate::auth::identity::{Identity, IdentityProvider}; use crate::auth::principal::{Principal, UserPrincipal}; use tonic::Code; @@ -6722,7 +6756,8 @@ mod tests { let err = handle_watch_sandbox( &state, non_member_request(WatchSandboxRequest { - id: "sandbox-cross-ws".into(), + sandbox: "cross-ws".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("other-workspace")), ..Default::default() }), ) @@ -6738,7 +6773,8 @@ mod tests { let err = handle_create_ssh_session( &state, non_member_request(CreateSshSessionRequest { - sandbox_id: "sandbox-cross-ws".into(), + sandbox: "cross-ws".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("other-workspace")), }), ) .await @@ -6762,7 +6798,8 @@ mod tests { let response = handle_create_ssh_session( &state, authed_request(CreateSshSessionRequest { - sandbox_id: "sandbox-ws-test".to_string(), + sandbox: "ws-test".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await diff --git a/crates/openshell-server/src/grpc/service.rs b/crates/openshell-server/src/grpc/service.rs index a691ec0ecf..3ee4f5992f 100644 --- a/crates/openshell-server/src/grpc/service.rs +++ b/crates/openshell-server/src/grpc/service.rs @@ -7,9 +7,9 @@ use std::sync::Arc; use openshell_core::proto::datamodel::v1::ObjectMeta; use openshell_core::proto::{ DeleteServiceRequest, DeleteServiceResponse, ExposeServiceRequest, GetServiceRequest, - ListServicesRequest, ListServicesResponse, Sandbox, ServiceEndpoint, ServiceEndpointResponse, + ListServicesRequest, ListServicesResponse, ServiceEndpoint, ServiceEndpointResponse, }; -use openshell_core::{ObjectId, ObjectWorkspace}; +use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; use prost::Message as _; use tonic::{Request, Response, Status}; use uuid::Uuid; @@ -17,14 +17,12 @@ use uuid::Uuid; use crate::ServerState; use crate::auth::workspace_authz::{ AuthorizedWorkspaceScope, MinWorkspaceRole, authorize_list_workspace_selector, - authorize_workspace_selector, }; use crate::pagination::Pagination; use crate::persistence::{ObjectListQuery, ObjectType, WriteCondition}; use crate::service_routing; const MAX_SERVICE_NAME_LEN: usize = super::MAX_ROUTABLE_NAME_LEN; -const MAX_SANDBOX_NAME_LEN: usize = super::MAX_ROUTABLE_NAME_LEN; pub(super) async fn handle_expose_service( state: &Arc, @@ -32,32 +30,26 @@ pub(super) async fn handle_expose_service( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .ensure_active()?; - validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?; + let workspace = + super::workspace::resolve_workspace(state.store.as_ref(), sandbox.object_workspace()) + .await? + .ensure_active()?; + let sandbox_name = sandbox.object_name(); validate_optional_endpoint_name("service", &req.service, MAX_SERVICE_NAME_LEN)?; if req.target_port == 0 || req.target_port > u32::from(u16::MAX) { return Err(Status::invalid_argument("target_port must be in 1..=65535")); } - let sandbox = state - .store - .get_message_by_name::(&workspace, &req.sandbox) - .await - .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? - .ok_or_else(|| Status::not_found("sandbox not found"))?; - let now = crate::persistence::current_time_ms(); - let key = service_routing::endpoint_key(&req.sandbox, &req.service); + let key = service_routing::endpoint_key(sandbox_name, &req.service); // Fetch existing endpoint to determine create vs. update path let existing = state @@ -93,7 +85,7 @@ pub(super) async fn handle_expose_service( let labels_json = serde_json::to_string(&HashMap::from([( "sandbox".to_string(), - req.sandbox.clone(), + sandbox_name.to_string(), )])) .map_err(|e| Status::internal(format!("serialize labels failed: {e}")))?; @@ -102,14 +94,14 @@ pub(super) async fn handle_expose_service( id: id.clone(), name: key.clone(), created_at_ms, - labels: HashMap::from([("sandbox".to_string(), req.sandbox.clone())]), + labels: HashMap::from([("sandbox".to_string(), sandbox_name.to_string())]), resource_version: 0, annotations: HashMap::new(), workspace: workspace.clone(), deletion_timestamp_ms: 0, }), sandbox_id: sandbox.object_id().to_string(), - sandbox_name: req.sandbox.clone(), + sandbox_name: sandbox_name.to_string(), service_name: req.service.clone(), target_port: req.target_port, domain: true, @@ -135,7 +127,7 @@ pub(super) async fn handle_expose_service( meta.resource_version = result.resource_version; } - let url = service_routing::endpoint_url(&state.config, &workspace, &req.sandbox, &req.service) + let url = service_routing::endpoint_url(&state.config, &workspace, sandbox_name, &req.service) .unwrap_or_default(); service_routing::emit_service_endpoint_config_event(&endpoint, &url, created); @@ -151,21 +143,19 @@ pub(super) async fn handle_get_service( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?; + let workspace = sandbox.object_workspace(); + let sandbox_name = sandbox.object_name(); validate_optional_endpoint_name("service", &req.service, MAX_SERVICE_NAME_LEN)?; - let endpoint = get_service_endpoint(state, &workspace, &req.sandbox, &req.service) + let endpoint = get_service_endpoint(state, workspace, sandbox_name, &req.service) .await? .ok_or_else(|| Status::not_found("service endpoint not found"))?; @@ -179,9 +169,16 @@ pub(super) async fn handle_list_services( let principal = super::extract_principal(&request)?; let req = request.into_inner(); if !req.sandbox.is_empty() { - validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?; + validate_optional_endpoint_name("sandbox", &req.sandbox, MAX_SERVICE_NAME_LEN)?; + super::sandbox::resolve_and_authorize_sandbox_name( + state, + &principal, + &req.sandbox, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; } - let scope = authorize_list_workspace_selector( &state.store, &state.admin_role, @@ -251,29 +248,27 @@ pub(super) async fn handle_delete_service( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace_selector( - &state.store, - &state.admin_role, + let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( + state, &principal, + &req.sandbox, req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?; + let workspace = sandbox.object_workspace(); + let sandbox_name = sandbox.object_name(); validate_optional_endpoint_name("service", &req.service, MAX_SERVICE_NAME_LEN)?; - let endpoint = get_service_endpoint(state, &workspace, &req.sandbox, &req.service).await?; + let endpoint = get_service_endpoint(state, workspace, sandbox_name, &req.service).await?; let Some(endpoint) = endpoint else { return Ok(Response::new(DeleteServiceResponse { deleted: false })); }; - let key = service_routing::endpoint_key(&req.sandbox, &req.service); + let key = service_routing::endpoint_key(sandbox_name, &req.service); let deleted = state .store - .delete_by_name(ServiceEndpoint::object_type(), &workspace, &key) + .delete_by_name(ServiceEndpoint::object_type(), workspace, &key) .await .map_err(|e| Status::internal(format!("delete endpoint failed: {e}")))?; @@ -317,6 +312,7 @@ fn service_endpoint_response( } #[allow(clippy::result_large_err)] +#[cfg(test)] fn validate_endpoint_name(field: &str, value: &str, max_len: usize) -> Result<(), Status> { if value.is_empty() { return Err(Status::invalid_argument(format!("{field} is required"))); @@ -369,7 +365,7 @@ fn is_dns_label(value: &str) -> bool { mod tests { use super::*; use crate::grpc::test_support::{authed_request, test_server_state}; - use openshell_core::proto::SandboxPhase; + use openshell_core::proto::{Sandbox, SandboxPhase}; async fn seed_sandbox(state: &Arc, name: &str) { let mut sandbox = Sandbox { @@ -424,12 +420,12 @@ mod tests { &state, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), - target_port: 8080, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), + target_port: 8080, + domain: true, }), ) .await @@ -461,10 +457,10 @@ mod tests { &state, authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), }), ) .await @@ -476,10 +472,10 @@ mod tests { &state, authed_request(DeleteServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), }), ) .await @@ -491,10 +487,10 @@ mod tests { &state, authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), }), ) .await @@ -530,12 +526,12 @@ mod tests { &state1, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), - target_port: 8080, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), + target_port: 8080, + domain: true, }), ) .await @@ -547,12 +543,12 @@ mod tests { &state2, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), - target_port: 9090, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), + target_port: 9090, + domain: true, }), ) .await @@ -598,12 +594,12 @@ mod tests { &state, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), - target_port: 7070, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), + target_port: 7070, + domain: true, }), ) .await @@ -616,12 +612,12 @@ mod tests { &state1, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), - target_port: 8080, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), + target_port: 8080, + domain: true, }), ) .await @@ -633,12 +629,12 @@ mod tests { &state2, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), - target_port: 9090, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), + target_port: 9090, + domain: true, }), ) .await @@ -660,10 +656,10 @@ mod tests { &state, authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), }), ) .await @@ -721,12 +717,12 @@ mod tests { &state, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), - target_port: 8080, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), + target_port: 8080, + domain: true, }), ) .await @@ -736,12 +732,12 @@ mod tests { &state, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), - target_port: 9090, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "beta".to_string(), )), + service: "web".to_string(), + target_port: 9090, + domain: true, }), ) .await @@ -752,10 +748,10 @@ mod tests { &state, authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), }), ) .await @@ -768,10 +764,10 @@ mod tests { &state, authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "beta".to_string(), )), + service: "web".to_string(), }), ) .await @@ -825,10 +821,10 @@ mod tests { &state, authed_request(DeleteServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "web".to_string(), }), ) .await @@ -856,10 +852,10 @@ mod tests { &state, authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), - service: "web".to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( "beta".to_string(), )), + service: "web".to_string(), }), ) .await @@ -873,12 +869,12 @@ mod tests { &state, authed_request(ExposeServiceRequest { sandbox: "my-sandbox".to_string(), - service: "api".to_string(), - target_port: 3000, - domain: true, workspace_scope: Some(openshell_core::proto::workspace_selector( "default".to_string(), )), + service: "api".to_string(), + target_port: 3000, + domain: true, }), ) .await @@ -903,7 +899,7 @@ mod tests { /// when targeting a workspace that does not exist. Returning `NOT_FOUND` /// would create a CWE-203 workspace-name oracle. #[tokio::test] - async fn non_member_gets_permission_denied_not_workspace_oracle() { + async fn non_member_gets_gets_not_found_without_sandbox_oracle() { use crate::auth::identity::{Identity, IdentityProvider}; use crate::auth::principal::{Principal, UserPrincipal}; @@ -927,6 +923,7 @@ mod tests { let err = handle_expose_service( &state, non_member_request(ExposeServiceRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -935,14 +932,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - tonic::Code::PermissionDenied, - "handle_expose_service should return PermissionDenied, got {:?}", + tonic::Code::NotFound, + "handle_expose_service should return NotFound, got {:?}", err.code() ); let err = handle_get_service( &state, non_member_request(GetServiceRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -951,14 +949,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - tonic::Code::PermissionDenied, - "handle_get_service should return PermissionDenied, got {:?}", + tonic::Code::NotFound, + "handle_get_service should return NotFound, got {:?}", err.code() ); let err = handle_list_services( &state, non_member_request(ListServicesRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -967,14 +966,15 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - tonic::Code::PermissionDenied, - "handle_list_services should return PermissionDenied, got {:?}", + tonic::Code::NotFound, + "handle_list_services should return NotFound, got {:?}", err.code() ); let err = handle_delete_service( &state, non_member_request(DeleteServiceRequest { + sandbox: ("any").to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), @@ -983,8 +983,8 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - tonic::Code::PermissionDenied, - "handle_delete_service should return PermissionDenied, got {:?}", + tonic::Code::NotFound, + "handle_delete_service should return NotFound, got {:?}", err.code() ); } diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index dac34524c1..e258f17c30 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -43,6 +43,9 @@ pub(super) const MAX_MAIN_PROCESS_ARGV_SIZE: usize = 256 * 1024; /// Command arguments only reject NUL (newlines are valid for inline scripts). /// Environment values and workdir reject both NUL and newlines. pub(super) fn validate_exec_request_fields(req: &ExecSandboxRequest) -> Result<(), Status> { + if req.sandbox.is_empty() { + return Err(Status::invalid_argument("sandbox is required")); + } if req.command.len() > MAX_EXEC_COMMAND_ARGS { return Err(Status::invalid_argument(format!( "command array exceeds {MAX_EXEC_COMMAND_ARGS} argument limit" @@ -1351,7 +1354,8 @@ mod tests { #[test] fn validate_exec_request_rejects_reserved_env_key() { let req = ExecSandboxRequest { - sandbox_id: "id".to_string(), + sandbox: "id".to_string(), + workspace_scope: None, command: vec!["echo".to_string()], environment: std::iter::once(("OPENSHELL_SANDBOX_ID".to_string(), "evil".to_string())) .collect(), @@ -1368,7 +1372,8 @@ mod tests { #[test] fn validate_exec_request_allows_pyfunc_helper_key() { let req = ExecSandboxRequest { - sandbox_id: "id".to_string(), + sandbox: "id".to_string(), + workspace_scope: None, command: vec!["python".to_string()], environment: std::iter::once(("OPENSHELL_PYFUNC_B64".to_string(), "data".to_string())) .collect(), @@ -2246,7 +2251,8 @@ mod tests { #[test] fn validate_exec_allows_newlines_in_command_args() { let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec![ "python3".to_string(), "-c".to_string(), @@ -2260,7 +2266,8 @@ mod tests { #[test] fn validate_exec_still_rejects_null_bytes_in_command_args() { let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec!["echo".to_string(), "hello\x00world".to_string()], ..Default::default() }; @@ -2271,7 +2278,8 @@ mod tests { #[test] fn validate_exec_still_rejects_newlines_in_workdir() { let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec!["ls".to_string()], workdir: "/tmp\nmalicious".to_string(), ..Default::default() @@ -2283,7 +2291,8 @@ mod tests { #[test] fn validate_exec_still_rejects_newlines_in_env_values() { let req = ExecSandboxRequest { - sandbox_id: "test".to_string(), + sandbox: "test".to_string(), + workspace_scope: None, command: vec!["ls".to_string()], environment: std::iter::once(("VAR".to_string(), "val\nmalicious".to_string())) .collect(), diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 8f7c55914f..4358931bd4 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -119,7 +119,7 @@ mod tests { const STORAGE_V1_SCHEMA_SHA256: &str = "79c72615d957fc0653c672f61998bf7d8d21b757bc05d07b3fff92bd70fc8f52"; const PUBLIC_RPC_SCHEMA_SHA256: &str = - "0f14943574349d02bdc61076c8c5a59a98b627325564ef1a6d21d7941825dc46"; + "6c803d61db1b667d78a6fd7e781316e681bf9093eaef7010f07cb3b9aa672ccb"; const DURABLE_SCHEMA_SHA256: &str = "920a5243dfb37ce709f0f562a47d17791a5ede90fd7f662ed01542abd60a0dfb"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = diff --git a/crates/openshell-supervisor-process/src/debug_rpc.rs b/crates/openshell-supervisor-process/src/debug_rpc.rs index 6f885a69db..eecb45ec20 100644 --- a/crates/openshell-supervisor-process/src/debug_rpc.rs +++ b/crates/openshell-supervisor-process/src/debug_rpc.rs @@ -7,10 +7,10 @@ //! flow (issue #1354). A `docker exec` (or `kubectl exec`) into a //! running sandbox can issue raw sandbox-class gRPC calls without //! standing up a custom binary inside the sandbox image — useful for -//! confirming the cross-sandbox IDOR guard and renewal semantics. +//! confirming the cross-sandbox authorization guard and renewal semantics. //! //! Subcommands: -//! - `get-sandbox-config --sandbox-id ` — call `GetSandboxConfig` +//! - `get-sandbox-config --sandbox ` — call `GetSandboxConfig` //! - `refresh` — call `RefreshSandboxToken` //! - `show-token` — print a token fingerprint and expiry, never the bearer //! - `show-principal` — pretty-print the decoded JWT claims @@ -53,7 +53,7 @@ const USAGE: &str = "\ usage: openshell-sandbox debug-rpc [options] commands: - get-sandbox-config --sandbox-id call GetSandboxConfig + get-sandbox-config --sandbox call GetSandboxConfig refresh renew the gateway JWT show-token print JWT fingerprint and expiry show-principal print decoded JWT claims @@ -71,12 +71,13 @@ async fn open_client() -> Result> { } async fn run_get_sandbox_config(args: &[String]) -> Result { - let sandbox_id = parse_flag(args, "--sandbox-id") - .ok_or_else(|| miette::miette!("get-sandbox-config: --sandbox-id is required"))?; + let sandbox_name = parse_flag(args, "--sandbox") + .ok_or_else(|| miette::miette!("get-sandbox-config: --sandbox is required"))?; let mut client = open_client().await?; let resp = client .get_sandbox_config(GetSandboxConfigRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_name.to_string(), + workspace_scope: None, }) .await; match resp { @@ -252,22 +253,22 @@ mod tests { #[test] fn parse_flag_handles_space_separated() { - let args: Vec = ["--sandbox-id", "abc-123"] + let args: Vec = ["--sandbox", "abc-123"] .iter() .map(ToString::to_string) .collect(); - assert_eq!(parse_flag(&args, "--sandbox-id"), Some("abc-123")); + assert_eq!(parse_flag(&args, "--sandbox"), Some("abc-123")); } #[test] fn parse_flag_handles_equals_separated() { - let args: Vec = ["--sandbox-id=abc-123".to_string()].to_vec(); - assert_eq!(parse_flag(&args, "--sandbox-id"), Some("abc-123")); + let args: Vec = ["--sandbox=abc-123".to_string()].to_vec(); + assert_eq!(parse_flag(&args, "--sandbox"), Some("abc-123")); } #[test] fn parse_flag_returns_none_when_missing() { let args: Vec = ["--other".to_string(), "x".to_string()].to_vec(); - assert!(parse_flag(&args, "--sandbox-id").is_none()); + assert!(parse_flag(&args, "--sandbox").is_none()); } } diff --git a/crates/openshell-tui/src/app.rs b/crates/openshell-tui/src/app.rs index 64d9bd5806..b63bb62e3c 100644 --- a/crates/openshell-tui/src/app.rs +++ b/crates/openshell-tui/src/app.rs @@ -3225,13 +3225,6 @@ impl App { // Helpers // ------------------------------------------------------------------ - /// Get the ID of the currently selected sandbox. - pub fn selected_sandbox_id(&self) -> Option<&str> { - self.sandbox_ids - .get(self.sandbox_selected) - .map(String::as_str) - } - /// Get the name of the currently selected sandbox. pub fn selected_sandbox_name(&self) -> Option<&str> { self.sandbox_names diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 55fbcaff87..37c774d3bb 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -697,23 +697,22 @@ fn spawn_log_stream(app: &mut App, tx: mpsc::UnboundedSender) { // Cancel any previous stream. app.cancel_log_stream(); - let sandbox_id = match app.selected_sandbox_id() { - Some(id) => id.to_string(), + let sandbox_name = match app.selected_sandbox_name() { + Some(name) => name.to_string(), None => return, }; - - let mut client = app.client.clone(); let workspace = app.selected_sandbox_workspace(); + let mut client = app.client.clone(); let handle = tokio::spawn(async move { // Phase 1: Fetch initial history via unary RPC. let req = openshell_core::proto::GetSandboxLogsRequest { - sandbox_id: sandbox_id.clone(), + sandbox: sandbox_name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(&workspace)), lines: 500, since_ms: 0, sources: vec![], min_level: String::new(), - workspace_scope: Some(named_workspace_scope(workspace)), }; match tokio::time::timeout(Duration::from_secs(5), client.get_sandbox_logs(req)).await { @@ -750,7 +749,8 @@ fn spawn_log_stream(app: &mut App, tx: mpsc::UnboundedSender) { // Phase 2: Stream live logs via WatchSandbox. let req = openshell_core::proto::WatchSandboxRequest { - id: sandbox_id, + sandbox: sandbox_name, + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), follow_status: false, follow_logs: true, follow_events: false, @@ -815,8 +815,10 @@ async fn handle_sandbox_delete(app: &mut App, tx: mpsc::UnboundedSender) } let req = openshell_core::proto::DeleteSandboxRequest { - name: sandbox_name, - workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())), + sandbox: sandbox_name, + workspace_scope: Some(openshell_core::proto::workspace_selector( + app.selected_sandbox_workspace(), + )), }; match app.client.delete_sandbox(req).await { Ok(_) => { @@ -849,37 +851,43 @@ async fn fetch_sandbox_detail(app: &mut App) { }; let req = openshell_core::proto::GetSandboxRequest { - name: sandbox_name.clone(), - workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())), + sandbox: sandbox_name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + app.selected_sandbox_workspace(), + )), }; // Step 1: Fetch sandbox metadata (providers, sandbox ID). - let sandbox_id = + let found = match tokio::time::timeout(Duration::from_secs(5), app.client.get_sandbox(req)).await { Ok(Ok(resp)) => { if let Some(sandbox) = resp.into_inner().sandbox { if let Some(spec) = &sandbox.spec { app.sandbox_providers_list.clone_from(&spec.providers); } - let id = sandbox.object_id().to_string(); - if id.is_empty() { None } else { Some(id) } + true } else { - None + false } } Ok(Err(e)) => { app.status_text = format!("failed to fetch sandbox detail: {}", e.message()); - None + false } Err(_) => { app.status_text = "sandbox detail request timed out".to_string(); - None + false } }; // Step 2: Fetch the current live policy (includes updates since creation). - if let Some(id) = sandbox_id { - let policy_req = openshell_core::proto::GetSandboxConfigRequest { sandbox_id: id }; + if found { + let policy_req = openshell_core::proto::GetSandboxConfigRequest { + sandbox: sandbox_name, + workspace_scope: Some(openshell_core::proto::workspace_selector( + app.selected_sandbox_workspace(), + )), + }; match tokio::time::timeout( Duration::from_secs(5), @@ -932,36 +940,11 @@ async fn handle_shell_connect( None => return Ok(()), }; - // Step 1: Get sandbox ID. - let sandbox_id = { - let req = openshell_core::proto::GetSandboxRequest { - name: sandbox_name.clone(), - workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())), - }; - match tokio::time::timeout(Duration::from_secs(5), app.client.get_sandbox(req)).await { - Ok(Ok(resp)) => { - if let Some(s) = resp.into_inner().sandbox { - s.object_id().to_string() - } else { - app.status_text = "sandbox not found".to_string(); - return Ok(()); - } - } - Ok(Err(e)) => { - app.status_text = format!("failed to get sandbox: {}", e.message()); - return Ok(()); - } - Err(_) => { - app.status_text = "get sandbox timed out".to_string(); - return Ok(()); - } - } - }; - - // Step 2: Create SSH session. + let workspace = app.selected_sandbox_workspace(); let session = { let req = openshell_core::proto::CreateSshSessionRequest { - sandbox_id: sandbox_id.clone(), + sandbox: sandbox_name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }; match tokio::time::timeout(Duration::from_secs(5), app.client.create_ssh_session(req)).await { @@ -999,7 +982,7 @@ async fn handle_shell_connect( let proxy_command = build_proxy_command( &exe.to_string_lossy(), &gateway_url, - &session.sandbox_id, + &sandbox_name, &session.token, &app.gateway_name, ); @@ -1091,35 +1074,10 @@ async fn handle_exec_command( command: &str, workspace: &str, ) -> Result<()> { - // Step 1: Resolve sandbox → SSH session (same as handle_shell_connect). - let sandbox_id = { - let req = openshell_core::proto::GetSandboxRequest { - name: sandbox_name.to_string(), - workspace_scope: Some(named_workspace_scope(workspace)), - }; - match tokio::time::timeout(Duration::from_secs(5), app.client.get_sandbox(req)).await { - Ok(Ok(resp)) => { - if let Some(s) = resp.into_inner().sandbox { - s.object_id().to_string() - } else { - app.status_text = format!("exec: sandbox {sandbox_name} not found"); - return Ok(()); - } - } - Ok(Err(e)) => { - app.status_text = format!("exec: failed to get sandbox: {}", e.message()); - return Ok(()); - } - Err(_) => { - app.status_text = "exec: get sandbox timed out".to_string(); - return Ok(()); - } - } - }; - let session = { let req = openshell_core::proto::CreateSshSessionRequest { - sandbox_id: sandbox_id.clone(), + sandbox: sandbox_name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }; match tokio::time::timeout(Duration::from_secs(5), app.client.create_ssh_session(req)).await { @@ -1156,7 +1114,7 @@ async fn handle_exec_command( let proxy_command = build_proxy_command( &exe.to_string_lossy(), &gateway_url, - &session.sandbox_id, + sandbox_name, &session.token, &app.gateway_name, ); @@ -1504,7 +1462,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { // If ports or command are set, wait for Ready before finishing. if need_ready { let mut attempts = 0; - let sandbox_id = loop { + let _sandbox_id = loop { attempts += 1; if attempts > 150 { let _ = tx.send(Event::CreateResult(Err( @@ -1515,8 +1473,8 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { tokio::time::sleep(Duration::from_secs(2)).await; let req = openshell_core::proto::GetSandboxRequest { - name: sandbox_name.clone(), - workspace_scope: Some(named_workspace_scope(&workspace)), + sandbox: sandbox_name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(&workspace)), }; // Retry on transient errors. if let Ok(resp) = client.get_sandbox(req).await @@ -1541,7 +1499,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { &endpoint, &gateway_name, &sandbox_name, - &sandbox_id, + &workspace, &ports, ) .await; @@ -1564,7 +1522,7 @@ async fn start_port_forwards( endpoint: &str, gateway_name: &str, sandbox_name: &str, - sandbox_id: &str, + workspace: &str, specs: &[openshell_core::forward::ForwardSpec], ) -> Vec { let mut warnings = Vec::new(); @@ -1572,7 +1530,8 @@ async fn start_port_forwards( // Create SSH session. let session = { let req = openshell_core::proto::CreateSshSessionRequest { - sandbox_id: sandbox_id.to_string(), + sandbox: sandbox_name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }; match tokio::time::timeout(Duration::from_secs(10), client.create_ssh_session(req)).await { Ok(Ok(resp)) => resp.into_inner(), @@ -1611,7 +1570,7 @@ async fn start_port_forwards( let proxy_command = build_proxy_command( &exe.to_string_lossy(), &gateway_url, - &session.sandbox_id, + sandbox_name, &session.token, gateway_name, ); @@ -1678,7 +1637,7 @@ async fn start_port_forwards( match result { Ok(Ok(true)) => { - if let Some(pid) = openshell_core::forward::find_ssh_forward_pid(&sid, port_val) { + if let Some(pid) = openshell_core::forward::find_ssh_forward_pid(&name, port_val) { let _ = openshell_core::forward::write_forward_pid( &name, port_val, pid, &sid, &bind_addr, ); @@ -1947,9 +1906,9 @@ fn spawn_draft_approve(app: &App, tx: mpsc::UnboundedSender) { tokio::spawn(async move { let req = openshell_core::proto::ApproveDraftChunkRequest { - name, + sandbox: name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), chunk_id, - workspace_scope: Some(named_workspace_scope(workspace)), review_token, }; match tokio::time::timeout(Duration::from_secs(5), client.approve_draft_chunk(req)).await { @@ -1992,10 +1951,10 @@ fn spawn_draft_reject(app: &App, tx: mpsc::UnboundedSender) { tokio::spawn(async move { let req = openshell_core::proto::RejectDraftChunkRequest { - name, + sandbox: name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), chunk_id, reason: String::new(), - workspace_scope: Some(named_workspace_scope(workspace)), }; match tokio::time::timeout(Duration::from_secs(5), client.reject_draft_chunk(req)).await { Ok(Ok(_)) => { @@ -2042,9 +2001,9 @@ fn spawn_draft_approve_all( }) .collect(); let req = openshell_core::proto::ApproveAllDraftChunksRequest { - name, + sandbox: name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), include_security_flagged: false, - workspace_scope: Some(named_workspace_scope(workspace)), approvals, }; match tokio::time::timeout( @@ -2404,10 +2363,10 @@ async fn refresh_global_settings(app: &mut App) { // Check for an active global policy only while the caller can read it. let policy_req = openshell_core::proto::ListSandboxPoliciesRequest { - name: String::new(), page_size: 1, page_token: String::new(), global: true, + sandbox: String::new(), workspace_scope: None, }; match tokio::time::timeout( @@ -2484,7 +2443,6 @@ fn spawn_set_global_setting(app: &App, tx: mpsc::UnboundedSender) { }; let req = UpdateConfigRequest { - name: String::new(), setting_key: key, setting_value: Some(SettingValue { value: Some(value) }), global: true, @@ -2517,7 +2475,6 @@ fn spawn_delete_global_setting(app: &App, tx: mpsc::UnboundedSender) { use openshell_core::proto::UpdateConfigRequest; let req = UpdateConfigRequest { - name: String::new(), setting_key: key, delete_setting: true, global: true, @@ -2585,10 +2542,10 @@ fn spawn_set_sandbox_setting(app: &App, tx: mpsc::UnboundedSender) { }; let req = UpdateConfigRequest { - name, + sandbox: name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), setting_key: key, setting_value: Some(SettingValue { value: Some(value) }), - workspace_scope: Some(named_workspace_scope(workspace)), ..Default::default() }; @@ -2623,10 +2580,10 @@ fn spawn_delete_sandbox_setting(app: &App, tx: mpsc::UnboundedSender) { use openshell_core::proto::UpdateConfigRequest; let req = UpdateConfigRequest { - name, + sandbox: name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), setting_key: key, delete_setting: true, - workspace_scope: Some(named_workspace_scope(workspace)), ..Default::default() }; @@ -2790,12 +2747,16 @@ fn apply_sandbox_refresh(app: &mut App, sandboxes: Vec id.to_string(), + let sandbox_name = match app.selected_sandbox_name() { + Some(name) => name.to_string(), None => return, }; + let workspace = app.selected_sandbox_workspace(); - let policy_req = openshell_core::proto::GetSandboxConfigRequest { sandbox_id }; + let policy_req = openshell_core::proto::GetSandboxConfigRequest { + sandbox: sandbox_name, + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + }; match tokio::time::timeout( Duration::from_secs(5), @@ -2833,9 +2794,11 @@ async fn refresh_draft_chunks(app: &mut App) { }; let req = openshell_core::proto::GetDraftPolicyRequest { - name: sandbox_name, + sandbox: sandbox_name, + workspace_scope: Some(openshell_core::proto::workspace_selector( + app.selected_sandbox_workspace(), + )), status_filter: String::new(), - workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())), }; if let Ok(Ok(resp)) = @@ -2911,7 +2874,7 @@ async fn fetch_sandbox_draft_counts( let mut client = client.clone(); async move { let req = openshell_core::proto::GetDraftPolicyRequest { - name, + sandbox: name, status_filter: "pending".to_string(), workspace_scope: Some(named_workspace_scope(workspace)), }; diff --git a/docs/sandboxes/manage-workspaces.mdx b/docs/sandboxes/manage-workspaces.mdx index 5bff9755e9..1aca0ed884 100644 --- a/docs/sandboxes/manage-workspaces.mdx +++ b/docs/sandboxes/manage-workspaces.mdx @@ -168,7 +168,9 @@ The public API represents this choice with a `WorkspaceSelector` oneof. Set `all_workspaces` marker on list requests that support it. Omitting the selector or sending an unset selector is invalid for workspace-scoped operations. The all-workspaces variant is accepted only by sandbox, sandbox template, provider, -and service list requests, and it requires Platform Admin access. +and service list requests, and it requires Platform Admin access. For services, +choose either a sandbox name or `--all-workspaces`; the options are mutually +exclusive. Clients migrating from the previous request fields should make the scope explicit: @@ -180,6 +182,24 @@ explicit: | `all_workspaces: true` | `workspace_scope.all_workspaces: {}` | | `global: true` | Omit `workspace_scope` | +Every sandbox-scoped RPC identifies the sandbox by its human-readable name and +keeps workspace selection in a separate field: + +```json +{ + "sandbox": "agent", + "workspaceScope": { + "workspace": "team-ml" + } +} +``` + +Public requests do not accept canonical sandbox IDs as references. Fields that +previously accepted `sandbox_id` are removed and their protobuf field numbers +are reserved. Resolve stored IDs to sandbox names before calling the public +API. List RPCs that are not filtered to one sandbox continue to use only the +request-level `workspace_scope` selector. + The Rust and Python SDKs expose separate all-workspaces list methods. The Go SDK passes `AllWorkspaces: true` in `ListOptions` to `ListAll`, and the TypeScript SDK uses a discriminated option type, so a caller cannot select a diff --git a/examples/governance-interceptor/src/main.rs b/examples/governance-interceptor/src/main.rs index 3636ec0570..986ab423c0 100644 --- a/examples/governance-interceptor/src/main.rs +++ b/examples/governance-interceptor/src/main.rs @@ -1251,11 +1251,11 @@ async fn propagate_policy_to_running_sandboxes( .map_or(0, |metadata| metadata.resource_version); let result = client .update_config(UpdateConfigRequest { - name: name.clone(), + sandbox: name.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(policy_state.policy_proto.clone()), annotations: policy_update_annotations(policy_state, &correlation_id), expected_resource_version: resource_version, - workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }) .await; diff --git a/examples/governance-interceptor/src/smoke_client.rs b/examples/governance-interceptor/src/smoke_client.rs index 25d9ef2f69..19eb2c8d72 100644 --- a/examples/governance-interceptor/src/smoke_client.rs +++ b/examples/governance-interceptor/src/smoke_client.rs @@ -57,7 +57,8 @@ async fn main() -> Result<(), Box> { let before = client .get_sandbox_config(GetSandboxConfigRequest { - sandbox_id: sandbox_id.clone(), + sandbox: sandbox_name.clone(), + workspace_scope: None, }) .await? .into_inner(); @@ -83,9 +84,9 @@ async fn main() -> Result<(), Box> { let policy_result = client .update_config(UpdateConfigRequest { - name: sandbox_name.clone(), - policy: Some(widened_policy), + sandbox: sandbox_name.clone(), workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(widened_policy), ..Default::default() }) .await; @@ -108,7 +109,7 @@ async fn main() -> Result<(), Box> { client .submit_policy_analysis(SubmitPolicyAnalysisRequest { - name: sandbox_name, + name: sandbox_name.clone(), network_activity_summaries: vec![NetworkActivitySummary { network_activity_count: 1, ..Default::default() @@ -120,7 +121,10 @@ async fn main() -> Result<(), Box> { .map_err(|status| format!("telemetry-only policy analysis was denied: {status}"))?; let after = client - .get_sandbox_config(GetSandboxConfigRequest { sandbox_id }) + .get_sandbox_config(GetSandboxConfigRequest { + sandbox: sandbox_name, + workspace_scope: None, + }) .await? .into_inner(); if after.version != before.version || after.policy_hash != before.policy_hash { diff --git a/proto/openshell.proto b/proto/openshell.proto index def6d7c473..46ca624054 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1221,10 +1221,8 @@ message BeginRootfsTarStagingResponse { // Get sandbox request. message GetSandboxRequest { reserved 2; - reserved "workspace"; - // Sandbox name (canonical lookup key). - string name = 1; - // Explicit workspace scope. The all-workspaces selection is invalid. + reserved "name", "workspace"; + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } @@ -1248,9 +1246,7 @@ message ListSandboxesRequest { message ListSandboxProvidersRequest { reserved 2; reserved "workspace"; - // Sandbox name (canonical lookup key). - string sandbox_name = 1; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } @@ -1258,8 +1254,7 @@ message ListSandboxProvidersRequest { message AttachSandboxProviderRequest { reserved 4; reserved "workspace"; - // Sandbox name (canonical lookup key). - string sandbox_name = 1; + string sandbox = 1; // Provider name to attach. string provider_name = 2; // Expected resource version for optimistic concurrency control. @@ -1267,7 +1262,6 @@ message AttachSandboxProviderRequest { // If non-zero, the server validates that the sandbox's current resource_version // matches this value before applying the mutation, returning ABORTED on mismatch. uint64 expected_resource_version = 3; - // Explicit workspace scope. The all-workspaces selection is invalid. openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } @@ -1275,8 +1269,7 @@ message AttachSandboxProviderRequest { message DetachSandboxProviderRequest { reserved 4; reserved "workspace"; - // Sandbox name (canonical lookup key). - string sandbox_name = 1; + string sandbox = 1; // Provider name to detach. string provider_name = 2; // Expected resource version for optimistic concurrency control. @@ -1284,37 +1277,30 @@ message DetachSandboxProviderRequest { // If non-zero, the server validates that the sandbox's current resource_version // matches this value before applying the mutation, returning ABORTED on mismatch. uint64 expected_resource_version = 3; - // Explicit workspace scope. The all-workspaces selection is invalid. openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } // Delete sandbox request. message DeleteSandboxRequest { reserved 2; - reserved "workspace"; - // Sandbox name (canonical lookup key). - string name = 1; - // Explicit workspace scope. The all-workspaces selection is invalid. + reserved "name", "workspace"; + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Stop sandbox request. message StopSandboxRequest { reserved 2; - reserved "workspace"; - // Sandbox name (canonical lookup key). - string name = 1; - // Explicit workspace scope. The all-workspaces selection is invalid. + reserved "name", "workspace"; + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Start sandbox request. message StartSandboxRequest { reserved 2; - reserved "workspace"; - // Sandbox name (canonical lookup key). - string name = 1; - // Explicit workspace scope. The all-workspaces selection is invalid. + reserved "name", "workspace"; + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } @@ -1356,8 +1342,10 @@ message DeleteSandboxResponse { // Create SSH session request. message CreateSshSessionRequest { - // Sandbox id. - string sandbox_id = 1; + reserved 1; + reserved "sandbox_id"; + string sandbox = 2; + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Create SSH session response. @@ -1398,15 +1386,13 @@ message CreateSshSessionResponse { message ExposeServiceRequest { reserved 5; reserved "workspace"; - // Sandbox name. - string sandbox = 1; // Service name within the sandbox. string service = 2; // Loopback TCP port inside the sandbox. uint32 target_port = 3; // Whether to print/use the browser-facing service URL. bool domain = 4; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; } @@ -1414,11 +1400,9 @@ message ExposeServiceRequest { message GetServiceRequest { reserved 3; reserved "workspace"; - // Sandbox name. - string sandbox = 1; // Service name within the sandbox. Empty selects the unnamed endpoint. string service = 2; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } @@ -1426,8 +1410,6 @@ message GetServiceRequest { message ListServicesRequest { reserved 4, 5; reserved "workspace", "all_workspaces"; - // Optional sandbox name. Empty lists endpoints for all sandboxes. - string sandbox = 1; // The maximum number of services to return. Zero uses 100. Values above // 1000 are coerced to 1000; negative values are invalid. int32 page_size = 2; @@ -1436,6 +1418,8 @@ message ListServicesRequest { string page_token = 3; // Explicit named or all-workspaces scope. openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; + // Optional sandbox name. Empty lists endpoints for all sandboxes. + string sandbox = 1; } // Response containing exposed sandbox service endpoints. @@ -1449,11 +1433,9 @@ message ListServicesResponse { message DeleteServiceRequest { reserved 3; reserved "workspace"; - // Sandbox name. - string sandbox = 1; // Service name within the sandbox. Empty selects the unnamed endpoint. string service = 2; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } @@ -1499,8 +1481,8 @@ message RevokeSshSessionResponse { // Execute command request. message ExecSandboxRequest { - // Sandbox id. - string sandbox_id = 1; + reserved 1; + reserved "sandbox_id"; // Command and arguments. repeated string command = 2; @@ -1532,6 +1514,8 @@ message ExecSandboxRequest { // sourced by them) are applied. When true, the command runs without those // files (`bash -c`), for automation that needs predictable startup behavior. bool no_login_shell = 10; + string sandbox = 11; + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 12; } // One stdout chunk from a sandbox exec. @@ -1560,8 +1544,10 @@ message ExecSandboxEvent { // Initial frame for one TCP forward stream. message TcpForwardInit { - // Sandbox id. - string sandbox_id = 1; + reserved 1; + reserved "sandbox_id"; + string sandbox = 2; + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; // Optional service identifier for audit/correlation. string service_id = 4; // Target the gateway should request from the supervisor. @@ -1622,8 +1608,8 @@ message SshSession { // Watch sandbox request. message WatchSandboxRequest { - // Sandbox id. - string id = 1; + reserved 1; + reserved "id"; // Stream sandbox status snapshots. bool follow_status = 2; @@ -1653,6 +1639,8 @@ message WatchSandboxRequest { // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. string log_min_level = 10; + string sandbox = 11; + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 12; } // One event in a sandbox watch stream. @@ -2208,10 +2196,7 @@ message ExchangeProviderSubjectTokenResponse { // Update sandbox policy request. message UpdateConfigRequest { reserved 10; - reserved "workspace"; - // Sandbox name (canonical lookup key). Required for sandbox-scoped updates. - // Not required when `global=true`. - string name = 1; + reserved "name", "workspace"; // The new policy to apply. // // Sandbox scope (`global=false`): @@ -2245,8 +2230,8 @@ message UpdateConfigRequest { // sandbox metadata as a convenience projection. For setting-only updates, it // only merges them into sandbox metadata. map annotations = 9; - // Explicit workspace scope for sandbox-scoped updates. Omit only when - // `global` is true; the all-workspaces selection is invalid. + // Required for sandbox-scoped updates and empty for global updates. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 11; } @@ -2310,15 +2295,12 @@ message UpdateConfigResponse { // Get sandbox policy status request. message GetSandboxPolicyStatusRequest { reserved 4; - reserved "workspace"; - // Sandbox name (canonical lookup key). Ignored when global is true. - string name = 1; + reserved "name", "workspace"; // The specific policy version to query. 0 means latest. uint32 version = 2; // Query global policy revisions instead of a sandbox-scoped one. bool global = 3; - // Explicit workspace scope for sandbox-scoped queries. Omit only when - // `global` is true; the all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } @@ -2333,9 +2315,7 @@ message GetSandboxPolicyStatusResponse { // List sandbox policies request. message ListSandboxPoliciesRequest { reserved 5; - reserved "workspace"; - // Sandbox name (canonical lookup key). Ignored when global is true. - string name = 1; + reserved "name", "workspace"; // The maximum number of revisions to return. Zero uses 100. Values above // 1000 are coerced to 1000; negative values are invalid. int32 page_size = 2; @@ -2344,8 +2324,7 @@ message ListSandboxPoliciesRequest { string page_token = 3; // List global policy revisions instead of sandbox-scoped ones. bool global = 4; - // Explicit workspace scope for sandbox-scoped queries. Omit only when - // `global` is true; the all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; } @@ -2419,10 +2398,8 @@ enum PolicyStatus { // Get sandbox logs request (one-shot fetch). message GetSandboxLogsRequest { - reserved 6; - reserved "workspace"; - // Sandbox id. - string sandbox_id = 1; + reserved 1, 6; + reserved "sandbox_id", "workspace"; // Maximum number of log lines to return. 0 means use default (2000). uint32 lines = 2; // Only include logs with timestamp >= this value (ms since epoch). 0 means no filter. @@ -2431,7 +2408,7 @@ message GetSandboxLogsRequest { repeated string sources = 4; // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. string min_level = 5; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 8; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 7; } @@ -2762,11 +2739,13 @@ message SubmitPolicyAnalysisRequest { // to watch. Other values are treated as agent-style (no dedup) so a new // mode does not silently collapse proposals. string analysis_mode = 3; - // Sandbox name. + // Sandbox name. The authenticated sandbox principal remains authoritative + // for this internal callback. string name = 4; // Anonymous network activity counters. repeated NetworkActivitySummary network_activity_summaries = 5; - // Workspace scope. Empty defaults to "default". + // Internal callback workspace. The gateway validates it against the + // authenticated sandbox principal. string workspace = 6; } @@ -2786,12 +2765,10 @@ message SubmitPolicyAnalysisResponse { // Get draft policy for a sandbox. message GetDraftPolicyRequest { reserved 3; - reserved "workspace"; - // Sandbox name. - string name = 1; + reserved "name", "workspace"; // Optional status filter: "pending", "approved", "rejected", or "" for all. string status_filter = 2; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } @@ -2809,15 +2786,13 @@ message GetDraftPolicyResponse { // Approve a single draft chunk. message ApproveDraftChunkRequest { reserved 3; - reserved "workspace"; - // Sandbox name. - string name = 1; + reserved "name", "workspace"; // Chunk ID to approve. string chunk_id = 2; // Token returned with the reviewed PolicyChunk. Approval fails with // FAILED_PRECONDITION if live decision inputs no longer match it. string review_token = 4; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } @@ -2831,14 +2806,12 @@ message ApproveDraftChunkResponse { // Reject a single draft chunk. message RejectDraftChunkRequest { reserved 4; - reserved "workspace"; - // Sandbox name. - string name = 1; + reserved "name", "workspace"; // Chunk ID to reject. string chunk_id = 2; // Optional reason for rejection (fed to LLM context in future analysis). string reason = 3; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } @@ -2852,15 +2825,13 @@ message DraftChunkApproval { message ApproveAllDraftChunksRequest { reserved 3; - reserved "workspace"; - // Sandbox name. - string name = 1; + reserved "name", "workspace"; // Include chunks with security_notes (default false: skips them). bool include_security_flagged = 2; // Exact reviewed chunks and tokens. The server validates them against one // live snapshot, stages compatible operations in order, and writes once. repeated DraftChunkApproval approvals = 4; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } @@ -2879,14 +2850,12 @@ message ApproveAllDraftChunksResponse { // Edit a pending chunk in-place. message EditDraftChunkRequest { reserved 4; - reserved "workspace"; - // Sandbox name. - string name = 1; + reserved "name", "workspace"; // Chunk ID to edit. string chunk_id = 2; // The modified rule (replaces existing proposed_rule). openshell.sandbox.v1.NetworkPolicyRule proposed_rule = 3; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } @@ -2895,12 +2864,10 @@ message EditDraftChunkResponse {} // Reverse an approval (remove merged rule from active policy). message UndoDraftChunkRequest { reserved 3; - reserved "workspace"; - // Sandbox name. - string name = 1; + reserved "name", "workspace"; // Chunk ID to undo. string chunk_id = 2; - // Explicit workspace scope. The all-workspaces selection is invalid. + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } @@ -2914,10 +2881,8 @@ message UndoDraftChunkResponse { // Clear all pending draft chunks for a sandbox. message ClearDraftChunksRequest { reserved 2; - reserved "workspace"; - // Sandbox name. - string name = 1; - // Explicit workspace scope. The all-workspaces selection is invalid. + reserved "name", "workspace"; + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } @@ -2929,10 +2894,8 @@ message ClearDraftChunksResponse { // Get decision history for a sandbox's draft policy. message GetDraftHistoryRequest { reserved 2; - reserved "workspace"; - // Sandbox name. - string name = 1; - // Explicit workspace scope. The all-workspaces selection is invalid. + reserved "name", "workspace"; + string sandbox = 1; openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } diff --git a/proto/sandbox.proto b/proto/sandbox.proto index c2b61d0b3a..9f64e08f5d 100644 --- a/proto/sandbox.proto +++ b/proto/sandbox.proto @@ -5,6 +5,7 @@ syntax = "proto3"; package openshell.sandbox.v1; +import "datamodel.proto"; import "google/protobuf/struct.proto"; // Sandbox-supervisor configuration and policy messages. @@ -315,10 +316,12 @@ message NetworkBinary { bool harness = 2 [deprecated = true]; } -// Request to get sandbox settings by sandbox ID. +// Request to get sandbox settings by sandbox name. message GetSandboxConfigRequest { - // The sandbox ID. - string sandbox_id = 1; + reserved 1; + reserved "sandbox_id"; + string sandbox = 2; + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Request to get gateway-global settings. diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index 931fa1e2ab..616e019e18 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -501,8 +501,9 @@ def exec( no_login_shell: bool = False, ) -> ExecResult: return self._client.exec( - self.sandbox.id, + self.sandbox.name, command, + workspace=self._workspace, stream_output=stream_output, workdir=workdir, env=env, @@ -523,8 +524,9 @@ def exec_python( timeout_seconds: int | None = None, ) -> ExecResult: return self._client.exec_python( - self.sandbox.id, + self.sandbox.name, function, + workspace=self._workspace, args=args, kwargs=kwargs, stream_output=stream_output, @@ -837,7 +839,8 @@ def sandbox_templates(self) -> SandboxTemplateClient: def get(self, sandbox_name: str, *, workspace: str) -> SandboxRef: response = self._stub.GetSandbox( openshell_pb2.GetSandboxRequest( - name=sandbox_name, workspace_scope=_workspace_scope(workspace) + sandbox=sandbox_name, + workspace_scope=_workspace_scope(workspace), ), timeout=self._timeout, ) @@ -956,7 +959,8 @@ def list_ids_for_all_workspaces( def delete(self, sandbox_name: str, *, workspace: str) -> bool: response = self._stub.DeleteSandbox( openshell_pb2.DeleteSandboxRequest( - name=sandbox_name, workspace_scope=_workspace_scope(workspace) + sandbox=sandbox_name, + workspace_scope=_workspace_scope(workspace), ), timeout=self._timeout, ) @@ -965,7 +969,8 @@ def delete(self, sandbox_name: str, *, workspace: str) -> bool: def stop(self, sandbox_name: str, *, workspace: str) -> SandboxRef: response = self._stub.StopSandbox( openshell_pb2.StopSandboxRequest( - name=sandbox_name, workspace_scope=_workspace_scope(workspace) + sandbox=sandbox_name, + workspace_scope=_workspace_scope(workspace), ), timeout=self._timeout, ) @@ -974,7 +979,8 @@ def stop(self, sandbox_name: str, *, workspace: str) -> SandboxRef: def start(self, sandbox_name: str, *, workspace: str) -> SandboxRef: response = self._stub.StartSandbox( openshell_pb2.StartSandboxRequest( - name=sandbox_name, workspace_scope=_workspace_scope(workspace) + sandbox=sandbox_name, + workspace_scope=_workspace_scope(workspace), ), timeout=self._timeout, ) @@ -1054,9 +1060,10 @@ def _wait_for_phase( def exec_stream( self, - sandbox_id: str, + sandbox_name: str, command: Sequence[str], *, + workspace: str, workdir: str | None = None, env: Mapping[str, str] | None = None, stdin: bytes | None = None, @@ -1067,7 +1074,8 @@ def exec_stream( raise SandboxError("command must not be empty") request = openshell_pb2.ExecSandboxRequest( - sandbox_id=sandbox_id, + sandbox=sandbox_name, + workspace_scope=_workspace_scope(workspace), command=list(command), workdir=workdir or "", environment=dict(env or {}), @@ -1110,9 +1118,10 @@ def exec_stream( def exec( self, - sandbox_id: str, + sandbox_name: str, command: Sequence[str], *, + workspace: str, stream_output: bool = False, workdir: str | None = None, env: Mapping[str, str] | None = None, @@ -1122,8 +1131,9 @@ def exec( ) -> ExecResult: result: ExecResult | None = None for item in self.exec_stream( - sandbox_id, + sandbox_name, command, + workspace=workspace, workdir=workdir, env=env, stdin=stdin, @@ -1145,9 +1155,10 @@ def exec( def exec_python( self, - sandbox_id: str, + sandbox_name: str, function: Callable[..., object], *, + workspace: str, args: Sequence[object] = (), kwargs: Mapping[str, object] | None = None, stream_output: bool = False, @@ -1162,8 +1173,9 @@ def exec_python( kwargs=kwargs, ) return self.exec( - sandbox_id, + sandbox_name, [_SANDBOX_PYTHON_BIN, "-c", _PYTHON_CLOUDPICKLE_BOOTSTRAP], + workspace=workspace, stream_output=stream_output, workdir=workdir, env=exec_env, diff --git a/python/openshell/sandbox_test.py b/python/openshell/sandbox_test.py index 0675bc7338..5e3b329606 100644 --- a/python/openshell/sandbox_test.py +++ b/python/openshell/sandbox_test.py @@ -55,6 +55,10 @@ def _request_selects_all_workspaces(request: Any) -> bool: return request.workspace_scope.WhichOneof("selection") == "all_workspaces" +def _request_sandbox(request: Any) -> str: + return cast("str", request.sandbox) + + def _client_credentials_fixture() -> dict[str, Any]: return json.loads( ( @@ -433,7 +437,12 @@ def test_exec_sends_stdin_payload() -> None: stub = _FakeStub() client = _client_with_fake_stub(stub) - result = client.exec("sandbox-1", ["python", "-c", "print('ok')"], stdin=b"payload") + result = client.exec( + "sandbox-1", + ["python", "-c", "print('ok')"], + workspace="default", + stdin=b"payload", + ) assert result.exit_code == 0 assert stub.request is not None @@ -447,7 +456,7 @@ def test_exec_python_serializes_callable_payload() -> None: def add(a: int, b: int) -> int: return a + b - result = client.exec_python("sandbox-1", add, args=(2, 3)) + result = client.exec_python("sandbox-1", add, workspace="default", args=(2, 3)) assert result.exit_code == 0 assert stub.request is not None @@ -2001,7 +2010,7 @@ def GetSandbox( return SimpleNamespace( sandbox=_make_sandbox_proto( "sandbox-1", - request.name, + _request_sandbox(request), workspace=_request_workspace(request) or "default", ) ) @@ -2025,7 +2034,7 @@ def StopSandbox( return SimpleNamespace( sandbox=_make_sandbox_proto( "sandbox-1", - request.name, + _request_sandbox(request), phase=openshell_pb2.SANDBOX_PHASE_STOPPED, workspace=_request_workspace(request) or "default", ) @@ -2041,7 +2050,7 @@ def StartSandbox( return SimpleNamespace( sandbox=_make_sandbox_proto( "sandbox-1", - request.name, + _request_sandbox(request), phase=openshell_pb2.SANDBOX_PHASE_STARTING, workspace=_request_workspace(request) or "default", ) @@ -2447,13 +2456,13 @@ def test_stop_and_start_forward_workspace_and_return_phase() -> None: stopped = client.stop("job-1", workspace="team-a") assert stub.stop_request is not None - assert stub.stop_request.name == "job-1" + assert _request_sandbox(stub.stop_request) == "job-1" assert _request_workspace(stub.stop_request) == "team-a" assert stopped.phase == openshell_pb2.SANDBOX_PHASE_STOPPED starting = client.start("job-1", workspace="team-a") assert stub.start_request is not None - assert stub.start_request.name == "job-1" + assert _request_sandbox(stub.start_request) == "job-1" assert _request_workspace(stub.start_request) == "team-a" assert starting.phase == openshell_pb2.SANDBOX_PHASE_STARTING @@ -2478,7 +2487,7 @@ def GetSandbox( return SimpleNamespace( sandbox=_make_sandbox_proto( "sandbox-1", - request.name, + _request_sandbox(request), phase=phase, workspace=_request_workspace(request) or "default", ) diff --git a/sdk/go/openshell/v1/config_client.go b/sdk/go/openshell/v1/config_client.go index b2cb3fd372..a3f2518416 100644 --- a/sdk/go/openshell/v1/config_client.go +++ b/sdk/go/openshell/v1/config_client.go @@ -25,13 +25,12 @@ func (c *configClient) GetSandbox(ctx context.Context, workspace, sandboxName st if sandboxName == "" { return nil, &StatusError{Code: ErrorInvalidArgument, Message: "sandbox name must not be empty"} } - sb, err := c.sandboxes.Get(ctx, workspace, sandboxName) - if err != nil { + if _, err := c.sandboxes.Get(ctx, workspace, sandboxName); err != nil { return nil, err } - resp, err := c.client.GetSandboxConfig(ctx, &sbv1.GetSandboxConfigRequest{ - SandboxId: sb.ID, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/config_client_test.go b/sdk/go/openshell/v1/config_client_test.go index 9fa8675ff2..e003be2227 100644 --- a/sdk/go/openshell/v1/config_client_test.go +++ b/sdk/go/openshell/v1/config_client_test.go @@ -143,7 +143,7 @@ func TestConfigGetSandbox(t *testing.T) { // Verify request was forwarded with resolved ID (stubSandboxResolver returns "sb-"). mock.mu.Lock() - assert.Equal(t, "sb-my-sandbox", mock.lastSandboxReq.GetSandboxId()) + assert.Equal(t, "my-sandbox", mock.lastSandboxReq.GetSandbox()) mock.mu.Unlock() // Scalar fields. @@ -222,7 +222,7 @@ func TestConfigGetSandbox_Error(t *testing.T) { // --- Name-to-ID resolution tests --- -func TestConfigGetSandbox_ResolvesNameToID(t *testing.T) { +func TestConfigGetSandbox_UsesName(t *testing.T) { mock := newMockConfigServer() mock.sandboxResp = &sbv1.GetSandboxConfigResponse{Version: 1} @@ -235,7 +235,7 @@ func TestConfigGetSandbox_ResolvesNameToID(t *testing.T) { // stubSandboxResolver returns ID "sb-" — verify the proto has the resolved ID, not the name. mock.mu.Lock() - assert.Equal(t, "sb-my-sandbox", mock.lastSandboxReq.GetSandboxId(), "GetSandbox should send resolved sandbox ID, not the name") + assert.Equal(t, "my-sandbox", mock.lastSandboxReq.GetSandbox()) mock.mu.Unlock() } @@ -361,7 +361,7 @@ func TestConfigUpdate_SandboxScope(t *testing.T) { mock.mu.Unlock() require.NotNil(t, req) - assert.Equal(t, "my-sandbox", req.GetName()) + assert.Equal(t, "my-sandbox", req.GetSandbox()) assert.Equal(t, "max_tokens", req.GetSettingKey()) assert.False(t, req.GetGlobal()) assert.Equal(t, uint64(4), req.GetExpectedResourceVersion()) @@ -397,7 +397,7 @@ func TestConfigUpdate_GlobalScope(t *testing.T) { mock.mu.Unlock() assert.True(t, req.GetGlobal()) - assert.Empty(t, req.GetName()) + assert.Empty(t, req.GetSandbox()) } func TestConfigUpdate_DeleteSetting(t *testing.T) { diff --git a/sdk/go/openshell/v1/exec_client.go b/sdk/go/openshell/v1/exec_client.go index c74fe7a5d3..8bc9c803b4 100644 --- a/sdk/go/openshell/v1/exec_client.go +++ b/sdk/go/openshell/v1/exec_client.go @@ -26,16 +26,15 @@ func (e *execClient) Run(ctx context.Context, workspace, sandboxName string, com if sandboxName == "" { return nil, &StatusError{Code: ErrorInvalidArgument, Message: "sandbox name must not be empty"} } - sb, err := e.sandboxes.Get(ctx, workspace, sandboxName) - if err != nil { + if _, err := e.sandboxes.Get(ctx, workspace, sandboxName); err != nil { return nil, err } - var opt *ExecOptions if len(opts) > 0 { opt = &opts[0] } - req := converter.ExecRequestToProto(sb.ID, command, opt) + req := converter.ExecRequestToProto(sandboxName, command, opt) + req.WorkspaceScope = namedWorkspaceScope(workspace) stream, err := e.client.ExecSandbox(ctx, req) if err != nil { @@ -61,16 +60,15 @@ func (e *execClient) Stream(ctx context.Context, workspace, sandboxName string, if sandboxName == "" { return nil, &StatusError{Code: ErrorInvalidArgument, Message: "sandbox name must not be empty"} } - sb, err := e.sandboxes.Get(ctx, workspace, sandboxName) - if err != nil { + if _, err := e.sandboxes.Get(ctx, workspace, sandboxName); err != nil { return nil, err } - var opt *ExecOptions if len(opts) > 0 { opt = &opts[0] } - req := converter.ExecRequestToProto(sb.ID, command, opt) + req := converter.ExecRequestToProto(sandboxName, command, opt) + req.WorkspaceScope = namedWorkspaceScope(workspace) streamCtx, cancel := context.WithCancel(ctx) stream, err := e.client.ExecSandbox(streamCtx, req) @@ -86,11 +84,9 @@ func (e *execClient) Interactive(ctx context.Context, workspace, sandboxName str if sandboxName == "" { return nil, &StatusError{Code: ErrorInvalidArgument, Message: "sandbox name must not be empty"} } - sb, err := e.sandboxes.Get(ctx, workspace, sandboxName) - if err != nil { + if _, err := e.sandboxes.Get(ctx, workspace, sandboxName); err != nil { return nil, err } - var opt *ExecOptions if len(opts) > 0 { opt = &opts[0] @@ -103,7 +99,8 @@ func (e *execClient) Interactive(ctx context.Context, workspace, sandboxName str return nil, converter.FromGRPCError(err) } - startReq := converter.ExecInteractiveRequestToProto(sb.ID, command, cols, rows, opt) + startReq := converter.ExecInteractiveRequestToProto(sandboxName, command, cols, rows, opt) + startReq.WorkspaceScope = namedWorkspaceScope(workspace) if sendErr := stream.Send(&pb.ExecSandboxInput{ Payload: &pb.ExecSandboxInput_Start{Start: startReq}, }); sendErr != nil { diff --git a/sdk/go/openshell/v1/exec_client_test.go b/sdk/go/openshell/v1/exec_client_test.go index 183940ec8b..272c7f9fa9 100644 --- a/sdk/go/openshell/v1/exec_client_test.go +++ b/sdk/go/openshell/v1/exec_client_test.go @@ -236,7 +236,7 @@ func TestExecRun_WithOptions(t *testing.T) { mock.mu.Lock() defer mock.mu.Unlock() - assert.Equal(t, "sb-test-sandbox", mock.lastExecRequest.GetSandboxId()) + assert.Equal(t, "test-sandbox", mock.lastExecRequest.GetSandbox()) assert.Equal(t, []string{"ls"}, mock.lastExecRequest.GetCommand()) assert.Equal(t, "/tmp", mock.lastExecRequest.GetWorkdir()) assert.Equal(t, map[string]string{"FOO": "bar"}, mock.lastExecRequest.GetEnvironment()) @@ -380,7 +380,7 @@ func TestExecInteractive(t *testing.T) { startReq := startInput.GetStart() require.NotNil(t, startReq) - assert.Equal(t, "sb-test-sandbox", startReq.GetSandboxId()) + assert.Equal(t, "test-sandbox", startReq.GetSandbox()) assert.Equal(t, []string{"/bin/bash"}, startReq.GetCommand()) assert.True(t, startReq.GetTty()) assert.Equal(t, uint32(80), startReq.GetCols()) @@ -532,7 +532,7 @@ func TestExecInteractive_ConcurrentReadAndExitCode(t *testing.T) { // --- Name-to-ID resolution tests --- -func TestExecRun_ResolvesNameToID(t *testing.T) { +func TestExecRun_UsesName(t *testing.T) { mock := newMockExecServer() mock.execEvents = []*pb.ExecSandboxEvent{ {Payload: &pb.ExecSandboxEvent_Exit{Exit: &pb.ExecSandboxExit{ExitCode: 0}}}, @@ -546,7 +546,7 @@ func TestExecRun_ResolvesNameToID(t *testing.T) { mock.mu.Lock() defer mock.mu.Unlock() // Verify the proto request contains the resolved ID, not the name - assert.Equal(t, "sb-my-sandbox", mock.lastExecRequest.GetSandboxId()) + assert.Equal(t, "my-sandbox", mock.lastExecRequest.GetSandbox()) } func TestExecRun_ResolutionError(t *testing.T) { @@ -560,7 +560,7 @@ func TestExecRun_ResolutionError(t *testing.T) { assert.True(t, IsNotFound(err)) } -func TestExecStream_ResolvesNameToID(t *testing.T) { +func TestExecStream_UsesName(t *testing.T) { mock := newMockExecServer() mock.execEvents = []*pb.ExecSandboxEvent{ {Payload: &pb.ExecSandboxEvent_Exit{Exit: &pb.ExecSandboxExit{ExitCode: 0}}}, @@ -576,7 +576,7 @@ func TestExecStream_ResolvesNameToID(t *testing.T) { mock.mu.Lock() defer mock.mu.Unlock() - assert.Equal(t, "sb-my-sandbox", mock.lastExecRequest.GetSandboxId()) + assert.Equal(t, "my-sandbox", mock.lastExecRequest.GetSandbox()) } func TestExecStream_ResolutionError(t *testing.T) { @@ -590,7 +590,7 @@ func TestExecStream_ResolutionError(t *testing.T) { assert.True(t, IsNotFound(err)) } -func TestExecInteractive_ResolvesNameToID(t *testing.T) { +func TestExecInteractive_UsesName(t *testing.T) { mock := newMockExecServer() mock.interactiveEvents = []*pb.ExecSandboxEvent{ {Payload: &pb.ExecSandboxEvent_Exit{Exit: &pb.ExecSandboxExit{ExitCode: 0}}}, @@ -609,7 +609,7 @@ func TestExecInteractive_ResolvesNameToID(t *testing.T) { require.NotEmpty(t, mock.receivedInputs) startReq := mock.receivedInputs[0].GetStart() require.NotNil(t, startReq) - assert.Equal(t, "sb-my-sandbox", startReq.GetSandboxId()) + assert.Equal(t, "my-sandbox", startReq.GetSandbox()) } func TestExecInteractive_ResolutionError(t *testing.T) { diff --git a/sdk/go/openshell/v1/fake/policy.go b/sdk/go/openshell/v1/fake/policy.go index 323b17faa6..49c62cda92 100644 --- a/sdk/go/openshell/v1/fake/policy.go +++ b/sdk/go/openshell/v1/fake/policy.go @@ -171,8 +171,7 @@ func (c *fakePolicyClient) GetStatus(_ context.Context, workspace, sandboxName s } // List returns policy revisions. When the global option is set, it returns -// global revisions; otherwise it returns all sandbox-scoped revisions for the -// given workspace. +// global revisions; otherwise it returns revisions for the specified sandbox. func (c *fakePolicyClient) List(workspace, sandboxName string, opts ...v1.ListPolicyOption) (*v1.Pager[types.SandboxPolicyRevision], error) { if c.closedFunc() { return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} diff --git a/sdk/go/openshell/v1/file_client.go b/sdk/go/openshell/v1/file_client.go index 7e9edad176..dd57410630 100644 --- a/sdk/go/openshell/v1/file_client.go +++ b/sdk/go/openshell/v1/file_client.go @@ -44,6 +44,9 @@ func (f *fileClient) Upload(ctx context.Context, workspace, sandboxName string, if remotePath == "" { return &StatusError{Code: ErrorInvalidArgument, Message: "remote path must not be empty"} } + if _, err := f.sandboxes.Get(ctx, workspace, sandboxName); err != nil { + return err + } info, err := os.Stat(localPath) if err != nil { @@ -53,13 +56,9 @@ func (f *fileClient) Upload(ctx context.Context, workspace, sandboxName string, return fmt.Errorf("local path is a directory, not a file: %s", localPath) } - sb, err := f.sandboxes.Get(ctx, workspace, sandboxName) - if err != nil { - return err - } - session, err := f.client.CreateSshSession(ctx, &pb.CreateSshSessionRequest{ - SandboxId: sb.ID, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return converter.FromGRPCError(err) @@ -86,14 +85,13 @@ func (f *fileClient) Download(ctx context.Context, workspace, sandboxName string if remotePath == "" { return &StatusError{Code: ErrorInvalidArgument, Message: "remote path must not be empty"} } - - sb, err := f.sandboxes.Get(ctx, workspace, sandboxName) - if err != nil { + if _, err := f.sandboxes.Get(ctx, workspace, sandboxName); err != nil { return err } session, err := f.client.CreateSshSession(ctx, &pb.CreateSshSessionRequest{ - SandboxId: sb.ID, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/file_client_test.go b/sdk/go/openshell/v1/file_client_test.go index ac08c8dec0..e5ac3984d8 100644 --- a/sdk/go/openshell/v1/file_client_test.go +++ b/sdk/go/openshell/v1/file_client_test.go @@ -109,7 +109,7 @@ func TestFileUpload(t *testing.T) { err := client.Upload(context.Background(), "default", "test-sandbox", localPath, "/remote/upload.txt") require.NoError(t, err) - assert.Equal(t, "sb-test-sandbox", mock.lastCreateReq.GetSandboxId()) + assert.Equal(t, "test-sandbox", mock.lastCreateReq.GetSandbox()) assert.Equal(t, 1, mock.createCallCount) } @@ -164,7 +164,7 @@ func TestFileDownload(t *testing.T) { err := client.Download(context.Background(), "default", "test-sandbox", "/remote/file.txt", localPath) require.NoError(t, err) - assert.Equal(t, "sb-test-sandbox", mock.lastCreateReq.GetSandboxId()) + assert.Equal(t, "test-sandbox", mock.lastCreateReq.GetSandbox()) assert.Equal(t, 1, mock.createCallCount) } @@ -242,7 +242,7 @@ func TestFileDownload_EmptyRemotePath(t *testing.T) { // --- Name-to-ID resolution tests --- -func TestFileUpload_ResolvesNameToID(t *testing.T) { +func TestFileUpload_UsesName(t *testing.T) { mock := newMockFileServer() mock.createResp = &pb.CreateSshSessionResponse{ SandboxId: "sb-my-sandbox", @@ -261,7 +261,7 @@ func TestFileUpload_ResolvesNameToID(t *testing.T) { _ = client.Upload(context.Background(), "default", "my-sandbox", localPath, "/remote/file.txt") // Verify the proto request contains the resolved ID, not the name - assert.Equal(t, "sb-my-sandbox", mock.lastCreateReq.GetSandboxId()) + assert.Equal(t, "my-sandbox", mock.lastCreateReq.GetSandbox()) } func TestFileUpload_ResolutionError(t *testing.T) { @@ -299,7 +299,7 @@ func TestFileUpload_ResolutionError(t *testing.T) { assert.Equal(t, 0, mock.createCallCount) } -func TestFileDownload_ResolvesNameToID(t *testing.T) { +func TestFileDownload_UsesName(t *testing.T) { mock := newMockFileServer() client, cleanup := setupFileTest(t, mock) defer cleanup() @@ -307,7 +307,7 @@ func TestFileDownload_ResolvesNameToID(t *testing.T) { localPath := filepath.Join(t.TempDir(), "downloaded.txt") _ = client.Download(context.Background(), "default", "my-sandbox", "/remote/file.txt", localPath) - assert.Equal(t, "sb-my-sandbox", mock.lastCreateReq.GetSandboxId()) + assert.Equal(t, "my-sandbox", mock.lastCreateReq.GetSandbox()) } func TestFileDownload_ResolutionError(t *testing.T) { diff --git a/sdk/go/openshell/v1/internal/converter/exec.go b/sdk/go/openshell/v1/internal/converter/exec.go index 3c73f0c23a..9bb53ec011 100644 --- a/sdk/go/openshell/v1/internal/converter/exec.go +++ b/sdk/go/openshell/v1/internal/converter/exec.go @@ -37,10 +37,10 @@ func ExecChunkFromEvent(event *pb.ExecSandboxEvent) (*types.ExecChunk, int, erro } // ExecRequestToProto builds a proto ExecSandboxRequest for Run/Stream modes. -func ExecRequestToProto(sandboxID string, command []string, opts *types.ExecOptions) *pb.ExecSandboxRequest { +func ExecRequestToProto(sandboxName string, command []string, opts *types.ExecOptions) *pb.ExecSandboxRequest { req := &pb.ExecSandboxRequest{ - SandboxId: sandboxID, - Command: CopyStringSlice(command), + Sandbox: sandboxName, + Command: CopyStringSlice(command), } if opts != nil { req.Workdir = opts.WorkDir @@ -51,8 +51,8 @@ func ExecRequestToProto(sandboxID string, command []string, opts *types.ExecOpti } // ExecInteractiveRequestToProto builds a proto ExecSandboxRequest for Interactive mode. -func ExecInteractiveRequestToProto(sandboxID string, command []string, cols, rows uint32, opts *types.ExecOptions) *pb.ExecSandboxRequest { - req := ExecRequestToProto(sandboxID, command, opts) +func ExecInteractiveRequestToProto(sandboxName string, command []string, cols, rows uint32, opts *types.ExecOptions) *pb.ExecSandboxRequest { + req := ExecRequestToProto(sandboxName, command, opts) req.Tty = true req.Cols = cols req.Rows = rows diff --git a/sdk/go/openshell/v1/internal/converter/exec_test.go b/sdk/go/openshell/v1/internal/converter/exec_test.go index 078a72d4d1..daac515c64 100644 --- a/sdk/go/openshell/v1/internal/converter/exec_test.go +++ b/sdk/go/openshell/v1/internal/converter/exec_test.go @@ -117,7 +117,7 @@ func TestExecRequestToProto(t *testing.T) { }) require.NotNil(t, req) - assert.Equal(t, "sb-1", req.SandboxId) + assert.Equal(t, "sb-1", req.GetSandbox()) assert.Equal(t, []string{"ls", "-la"}, req.Command) assert.Equal(t, "/home/user", req.Workdir) assert.Equal(t, map[string]string{"FOO": "bar"}, req.Environment) @@ -128,7 +128,7 @@ func TestExecRequestToProto_NilOptions(t *testing.T) { req := ExecRequestToProto("sb-2", []string{"echo", "hi"}, nil) require.NotNil(t, req) - assert.Equal(t, "sb-2", req.SandboxId) + assert.Equal(t, "sb-2", req.GetSandbox()) assert.Equal(t, []string{"echo", "hi"}, req.Command) assert.Empty(t, req.Workdir) assert.Nil(t, req.Environment) @@ -141,7 +141,7 @@ func TestExecRequestToProto_Interactive(t *testing.T) { }) require.NotNil(t, req) - assert.Equal(t, "sb-3", req.SandboxId) + assert.Equal(t, "sb-3", req.GetSandbox()) assert.Equal(t, []string{"/bin/bash"}, req.Command) assert.Equal(t, "/root", req.Workdir) assert.Equal(t, map[string]string{"TERM": "xterm"}, req.Environment) diff --git a/sdk/go/openshell/v1/internal/converter/setting.go b/sdk/go/openshell/v1/internal/converter/setting.go index 495545938d..03c67091fe 100644 --- a/sdk/go/openshell/v1/internal/converter/setting.go +++ b/sdk/go/openshell/v1/internal/converter/setting.go @@ -180,7 +180,6 @@ func ConfigUpdateToProto(cu *v1.ConfigUpdate) (*pb.UpdateConfigRequest, error) { return nil, nil } req := &pb.UpdateConfigRequest{ - Name: cu.Name, SettingKey: cu.SettingKey, SettingValue: SettingValueToProto(cu.SettingValue), DeleteSetting: cu.DeleteSetting, @@ -188,6 +187,9 @@ func ConfigUpdateToProto(cu *v1.ConfigUpdate) (*pb.UpdateConfigRequest, error) { ExpectedResourceVersion: cu.ExpectedResourceVersion, Annotations: CopyStringMap(cu.Annotations), } + if !cu.Global { + req.Sandbox = cu.Name + } // Convert typed SDK SandboxPolicy to proto SandboxPolicy. policy, err := SandboxPolicyToProtoChecked(cu.Policy) diff --git a/sdk/go/openshell/v1/internal/converter/setting_test.go b/sdk/go/openshell/v1/internal/converter/setting_test.go index f546902429..ace8f6dfb0 100644 --- a/sdk/go/openshell/v1/internal/converter/setting_test.go +++ b/sdk/go/openshell/v1/internal/converter/setting_test.go @@ -429,7 +429,7 @@ func TestConfigUpdateToProto(t *testing.T) { require.NoError(t, err) require.NotNil(t, req) - assert.Equal(t, "my-sandbox", req.Name) + assert.Equal(t, "my-sandbox", req.GetSandbox()) assert.Equal(t, "timeout", req.SettingKey) require.NotNil(t, req.SettingValue) assert.Equal(t, int64(60), req.SettingValue.GetIntValue()) @@ -495,7 +495,7 @@ func TestConfigUpdateToProto_GlobalScope(t *testing.T) { require.NotNil(t, req) assert.True(t, req.Global) - assert.Empty(t, req.Name) + assert.Empty(t, req.GetSandbox()) } func TestConfigUpdateToProto_NilSettingValue(t *testing.T) { @@ -779,7 +779,7 @@ func TestConfigUpdateToProto_WithMergeOperations(t *testing.T) { require.NoError(t, err) require.NotNil(t, req) - assert.Equal(t, "my-sandbox", req.GetName()) + assert.Equal(t, "my-sandbox", req.GetSandbox()) require.Len(t, req.GetMergeOperations(), 3) // First: RemoveRule diff --git a/sdk/go/openshell/v1/policy_client.go b/sdk/go/openshell/v1/policy_client.go index 16f1fe4ec2..56fc3c4adb 100644 --- a/sdk/go/openshell/v1/policy_client.go +++ b/sdk/go/openshell/v1/policy_client.go @@ -23,9 +23,9 @@ func newPolicyClient(conn grpc.ClientConnInterface) *policyClient { func (p *policyClient) GetDraft(ctx context.Context, workspace, sandboxName string, opts ...GetDraftOption) (*DraftPolicy, error) { cfg := types.ApplyGetDraftOptions(opts) resp, err := p.client.GetDraftPolicy(ctx, &pb.GetDraftPolicyRequest{ - Name: sandboxName, - StatusFilter: cfg.StatusFilter(), + Sandbox: sandboxName, WorkspaceScope: namedWorkspaceScope(workspace), + StatusFilter: cfg.StatusFilter(), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -35,9 +35,9 @@ func (p *policyClient) GetDraft(ctx context.Context, workspace, sandboxName stri func (p *policyClient) ApproveDraftChunk(ctx context.Context, workspace, sandboxName, chunkID, reviewToken string) (*ApproveResult, error) { resp, err := p.client.ApproveDraftChunk(ctx, &pb.ApproveDraftChunkRequest{ - Name: sandboxName, - ChunkId: chunkID, + Sandbox: sandboxName, WorkspaceScope: namedWorkspaceScope(workspace), + ChunkId: chunkID, ReviewToken: reviewToken, }) if err != nil { @@ -48,10 +48,10 @@ func (p *policyClient) ApproveDraftChunk(ctx context.Context, workspace, sandbox func (p *policyClient) RejectDraftChunk(ctx context.Context, workspace, sandboxName, chunkID, reason string) error { _, err := p.client.RejectDraftChunk(ctx, &pb.RejectDraftChunkRequest{ - Name: sandboxName, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), ChunkId: chunkID, Reason: reason, - WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return converter.FromGRPCError(err) @@ -69,10 +69,10 @@ func (p *policyClient) ApproveAllDraftChunks(ctx context.Context, workspace, san }) } resp, err := p.client.ApproveAllDraftChunks(ctx, &pb.ApproveAllDraftChunksRequest{ - Name: sandboxName, IncludeSecurityFlagged: cfg.IncludeSecurityFlagged(), - WorkspaceScope: namedWorkspaceScope(workspace), Approvals: approvals, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -82,7 +82,7 @@ func (p *policyClient) ApproveAllDraftChunks(ctx context.Context, workspace, san func (p *policyClient) ClearDraftChunks(ctx context.Context, workspace, sandboxName string) (*ClearResult, error) { resp, err := p.client.ClearDraftChunks(ctx, &pb.ClearDraftChunksRequest{ - Name: sandboxName, + Sandbox: sandboxName, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -93,7 +93,7 @@ func (p *policyClient) ClearDraftChunks(ctx context.Context, workspace, sandboxN func (p *policyClient) GetDraftHistory(ctx context.Context, workspace, sandboxName string) ([]DraftHistoryEntry, error) { resp, err := p.client.GetDraftHistory(ctx, &pb.GetDraftHistoryRequest{ - Name: sandboxName, + Sandbox: sandboxName, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -115,11 +115,11 @@ func (p *policyClient) GetDraftHistory(ctx context.Context, workspace, sandboxNa func (p *policyClient) GetStatus(ctx context.Context, workspace, sandboxName string, opts ...GetStatusOption) (*PolicyStatusResult, error) { cfg := types.ApplyGetStatusOptions(opts) req := &pb.GetSandboxPolicyStatusRequest{ - Name: sandboxName, Version: cfg.Version(), Global: cfg.Global(), } if !cfg.Global() { + req.Sandbox = sandboxName req.WorkspaceScope = namedWorkspaceScope(workspace) } resp, err := p.client.GetSandboxPolicyStatus(ctx, req) @@ -139,7 +139,7 @@ func (p *policyClient) List(workspace, sandboxName string, opts ...ListPolicyOpt } return newPager(cfg.PageToken(), func(ctx context.Context, pageToken string) (*Page[SandboxPolicyRevision], error) { req := &pb.ListSandboxPoliciesRequest{ - Name: sandboxName, PageSize: cfg.PageSize(), PageToken: pageToken, Global: cfg.Global(), + Sandbox: sandboxName, PageSize: cfg.PageSize(), PageToken: pageToken, Global: cfg.Global(), } if !cfg.Global() { req.WorkspaceScope = namedWorkspaceScope(workspace) @@ -168,10 +168,10 @@ func (p *policyClient) ListAll(ctx context.Context, workspace, sandboxName strin func (p *policyClient) EditDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string, proposedRule *NetworkPolicyRule) error { _, err := p.client.EditDraftChunk(ctx, &pb.EditDraftChunkRequest{ - Name: sandboxName, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), ChunkId: chunkID, ProposedRule: converter.NetworkPolicyRuleToProto(proposedRule), - WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return converter.FromGRPCError(err) @@ -181,9 +181,9 @@ func (p *policyClient) EditDraftChunk(ctx context.Context, workspace, sandboxNam func (p *policyClient) UndoDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string) (*UndoResult, error) { resp, err := p.client.UndoDraftChunk(ctx, &pb.UndoDraftChunkRequest{ - Name: sandboxName, - ChunkId: chunkID, + Sandbox: sandboxName, WorkspaceScope: namedWorkspaceScope(workspace), + ChunkId: chunkID, }) if err != nil { return nil, converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/policy_client_test.go b/sdk/go/openshell/v1/policy_client_test.go index 10e8fd7bb3..9eca97d0f3 100644 --- a/sdk/go/openshell/v1/policy_client_test.go +++ b/sdk/go/openshell/v1/policy_client_test.go @@ -235,7 +235,7 @@ func TestPolicyGetDraft(t *testing.T) { // Verify request was forwarded. mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastGetDraftReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastGetDraftReq.GetSandbox()) assert.Empty(t, mock.lastGetDraftReq.GetStatusFilter()) mock.mu.Unlock() @@ -321,7 +321,7 @@ func TestPolicyApproveDraftChunk(t *testing.T) { // Verify request was forwarded. mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastApproveReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastApproveReq.GetSandbox()) assert.Equal(t, "chunk-1", mock.lastApproveReq.GetChunkId()) assert.Equal(t, "token-1", mock.lastApproveReq.GetReviewToken()) mock.mu.Unlock() @@ -358,7 +358,7 @@ func TestPolicyRejectDraftChunk(t *testing.T) { // Verify request was forwarded. mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastRejectReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastRejectReq.GetSandbox()) assert.Equal(t, "chunk-2", mock.lastRejectReq.GetChunkId()) assert.Equal(t, "too broad", mock.lastRejectReq.GetReason()) mock.mu.Unlock() @@ -400,7 +400,7 @@ func TestPolicyApproveAllDraftChunks(t *testing.T) { // Verify default: security-flagged NOT included. mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastApproveAllReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastApproveAllReq.GetSandbox()) assert.False(t, mock.lastApproveAllReq.GetIncludeSecurityFlagged()) mock.mu.Unlock() @@ -466,7 +466,7 @@ func TestPolicyClearDraftChunks(t *testing.T) { // Verify request was forwarded. mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastClearReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastClearReq.GetSandbox()) mock.mu.Unlock() assert.Equal(t, uint32(4), result.ChunksCleared) @@ -517,7 +517,7 @@ func TestPolicyGetDraftHistory(t *testing.T) { // Verify request was forwarded. mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastHistoryReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastHistoryReq.GetSandbox()) mock.mu.Unlock() assert.Equal(t, "approved", entries[0].EventType) @@ -583,7 +583,7 @@ func TestPolicyGetStatus(t *testing.T) { // Verify request was forwarded (no version = latest). mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetSandbox()) assert.Equal(t, uint32(0), mock.lastStatusReq.GetVersion()) mock.mu.Unlock() @@ -649,8 +649,7 @@ func TestPolicyGetStatus_WithGlobal(t *testing.T) { // Verify global flag was forwarded in the proto request. mock.mu.Lock() assert.True(t, mock.lastStatusReq.GetGlobal()) - assert.Empty(t, mock.lastStatusReq.GetName()) - assert.Nil(t, mock.lastStatusReq.GetWorkspaceScope()) + assert.Empty(t, mock.lastStatusReq.GetSandbox()) mock.mu.Unlock() } @@ -675,8 +674,7 @@ func TestPolicyGetStatus_WithGlobalIgnoresNonEmptyName(t *testing.T) { mock.mu.Lock() assert.True(t, mock.lastStatusReq.GetGlobal()) - assert.Equal(t, "some-sandbox", mock.lastStatusReq.GetName()) - assert.Nil(t, mock.lastStatusReq.GetWorkspaceScope()) + assert.Empty(t, mock.lastStatusReq.GetSandbox()) mock.mu.Unlock() } @@ -732,7 +730,7 @@ func TestPolicyGetStatus_WithoutGlobal_PreservesExistingBehavior(t *testing.T) { mock.mu.Lock() assert.False(t, mock.lastStatusReq.GetGlobal()) assert.Equal(t, "default", mock.lastStatusReq.GetWorkspaceScope().GetWorkspace()) - assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetSandbox()) mock.mu.Unlock() } @@ -773,15 +771,15 @@ func TestPolicyList(t *testing.T) { client, cleanup := setupPolicyTest(t, mock) defer cleanup() - revisions, err := client.ListAll(context.Background(), "default", "sandbox") + revisions, err := client.ListAll(context.Background(), "default", "my-sandbox") require.NoError(t, err) require.Len(t, revisions, 2) // Verify request was forwarded (no pagination options). mock.mu.Lock() + assert.Equal(t, "my-sandbox", mock.lastListReq.GetSandbox()) assert.Equal(t, "default", mock.lastListReq.GetWorkspaceScope().GetWorkspace()) - assert.Equal(t, "sandbox", mock.lastListReq.GetName()) assert.Equal(t, int32(0), mock.lastListReq.GetPageSize()) assert.Empty(t, mock.lastListReq.GetPageToken()) mock.mu.Unlock() @@ -806,7 +804,7 @@ func TestPolicyList_WithPageSize(t *testing.T) { client, cleanup := setupPolicyTest(t, mock) defer cleanup() - revisions, err := client.ListAll(context.Background(), "default", "sandbox", + revisions, err := client.ListAll(context.Background(), "default", "my-sandbox", types.WithPageSize(10), ) @@ -827,7 +825,7 @@ func TestPolicyList_Empty(t *testing.T) { client, cleanup := setupPolicyTest(t, mock) defer cleanup() - revisions, err := client.ListAll(context.Background(), "default", "sandbox") + revisions, err := client.ListAll(context.Background(), "default", "my-sandbox") require.NoError(t, err) assert.NotNil(t, revisions) @@ -856,7 +854,7 @@ func TestPolicyList_WithGlobal(t *testing.T) { // Verify global flag was forwarded in the proto request. mock.mu.Lock() assert.True(t, mock.lastListReq.GetGlobal()) - assert.Nil(t, mock.lastListReq.GetWorkspaceScope()) + assert.Empty(t, mock.lastListReq.GetSandbox()) mock.mu.Unlock() } @@ -878,7 +876,7 @@ func TestPolicyList_WithGlobalIgnoresWorkspace(t *testing.T) { mock.mu.Lock() assert.True(t, mock.lastListReq.GetGlobal()) - assert.Nil(t, mock.lastListReq.GetWorkspaceScope()) + assert.Empty(t, mock.lastListReq.GetSandbox()) mock.mu.Unlock() } @@ -920,7 +918,7 @@ func TestPolicyList_WithoutGlobal_PreservesExistingBehavior(t *testing.T) { client, cleanup := setupPolicyTest(t, mock) defer cleanup() - revisions, err := client.ListAll(context.Background(), "default", "sandbox") + revisions, err := client.ListAll(context.Background(), "default", "my-sandbox") require.NoError(t, err) require.Len(t, revisions, 1) @@ -928,6 +926,7 @@ func TestPolicyList_WithoutGlobal_PreservesExistingBehavior(t *testing.T) { // Verify global flag is false by default. mock.mu.Lock() assert.False(t, mock.lastListReq.GetGlobal()) + assert.Equal(t, "my-sandbox", mock.lastListReq.GetSandbox()) assert.Equal(t, "default", mock.lastListReq.GetWorkspaceScope().GetWorkspace()) mock.mu.Unlock() } @@ -939,7 +938,7 @@ func TestPolicyList_Error(t *testing.T) { client, cleanup := setupPolicyTest(t, mock) defer cleanup() - revisions, err := client.ListAll(context.Background(), "default", "sandbox") + revisions, err := client.ListAll(context.Background(), "default", "my-sandbox") assert.Nil(t, revisions) require.Error(t, err) @@ -966,7 +965,7 @@ func TestPolicyEditDraftChunk(t *testing.T) { // Verify request was forwarded. mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastEditReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastEditReq.GetSandbox()) assert.Equal(t, "chunk-1", mock.lastEditReq.GetChunkId()) require.NotNil(t, mock.lastEditReq.GetProposedRule()) assert.Equal(t, "allow-https", mock.lastEditReq.GetProposedRule().GetName()) @@ -1005,7 +1004,7 @@ func TestPolicyUndoDraftChunk(t *testing.T) { // Verify request was forwarded. mock.mu.Lock() - assert.Equal(t, "my-sandbox", mock.lastUndoReq.GetName()) + assert.Equal(t, "my-sandbox", mock.lastUndoReq.GetSandbox()) assert.Equal(t, "chunk-3", mock.lastUndoReq.GetChunkId()) mock.mu.Unlock() diff --git a/sdk/go/openshell/v1/sandbox_client.go b/sdk/go/openshell/v1/sandbox_client.go index 0760d4469b..9917cbf5e5 100644 --- a/sdk/go/openshell/v1/sandbox_client.go +++ b/sdk/go/openshell/v1/sandbox_client.go @@ -89,7 +89,7 @@ func validateTemplateCreateSpec(spec *SandboxSpec) error { func (s *sandboxClient) Get(ctx context.Context, workspace, name string) (*Sandbox, error) { resp, err := s.client.GetSandbox(ctx, &pb.GetSandboxRequest{ - Name: name, + Sandbox: name, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -143,7 +143,7 @@ func (s *sandboxClient) ListAll(ctx context.Context, workspace string, opts ...L func (s *sandboxClient) Delete(ctx context.Context, workspace, name string) error { _, err := s.client.DeleteSandbox(ctx, &pb.DeleteSandboxRequest{ - Name: name, + Sandbox: name, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -154,7 +154,7 @@ func (s *sandboxClient) Delete(ctx context.Context, workspace, name string) erro func (s *sandboxClient) Stop(ctx context.Context, workspace, name string) (*Sandbox, error) { resp, err := s.client.StopSandbox(ctx, &pb.StopSandboxRequest{ - Name: name, + Sandbox: name, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -165,7 +165,7 @@ func (s *sandboxClient) Stop(ctx context.Context, workspace, name string) (*Sand func (s *sandboxClient) Start(ctx context.Context, workspace, name string) (*Sandbox, error) { resp, err := s.client.StartSandbox(ctx, &pb.StartSandboxRequest{ - Name: name, + Sandbox: name, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -176,9 +176,9 @@ func (s *sandboxClient) Start(ctx context.Context, workspace, name string) (*San func (s *sandboxClient) AttachProvider(ctx context.Context, workspace, sandboxName, providerName string, expectedResourceVersion uint64) (*AttachProviderResult, error) { resp, err := s.client.AttachSandboxProvider(ctx, &pb.AttachSandboxProviderRequest{ - SandboxName: sandboxName, ProviderName: providerName, ExpectedResourceVersion: expectedResourceVersion, + Sandbox: sandboxName, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -192,9 +192,9 @@ func (s *sandboxClient) AttachProvider(ctx context.Context, workspace, sandboxNa func (s *sandboxClient) DetachProvider(ctx context.Context, workspace, sandboxName, providerName string, expectedResourceVersion uint64) (*DetachProviderResult, error) { resp, err := s.client.DetachSandboxProvider(ctx, &pb.DetachSandboxProviderRequest{ - SandboxName: sandboxName, ProviderName: providerName, ExpectedResourceVersion: expectedResourceVersion, + Sandbox: sandboxName, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -208,7 +208,7 @@ func (s *sandboxClient) DetachProvider(ctx context.Context, workspace, sandboxNa func (s *sandboxClient) ListProviders(ctx context.Context, workspace, sandboxName string) ([]*Provider, error) { resp, err := s.client.ListSandboxProviders(ctx, &pb.ListSandboxProvidersRequest{ - SandboxName: sandboxName, + Sandbox: sandboxName, WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { @@ -294,15 +294,14 @@ func (s *sandboxClient) Watch(ctx context.Context, workspace, name string, opts if len(opts) > 0 { watchOpts = opts[0] } - - sb, err := s.Get(ctx, workspace, name) - if err != nil { + if _, err := s.Get(ctx, workspace, name); err != nil { return nil, err } streamCtx, streamCancel := context.WithCancel(ctx) stream, err := s.client.WatchSandbox(streamCtx, &pb.WatchSandboxRequest{ - Id: sb.ID, + Sandbox: name, + WorkspaceScope: namedWorkspaceScope(workspace), FollowStatus: true, StopOnTerminal: watchOpts.StopOnTerminal, }) @@ -368,18 +367,16 @@ func (s *sandboxClient) Watch(ctx context.Context, workspace, name string, opts } func (s *sandboxClient) GetLogs(ctx context.Context, workspace, sandboxName string, opts ...LogOption) (*LogResult, error) { - sb, err := s.Get(ctx, workspace, sandboxName) - if err != nil { + if _, err := s.Get(ctx, workspace, sandboxName); err != nil { return nil, err } - cfg := types.ApplyLogOptions(opts) req := &pb.GetSandboxLogsRequest{ - SandboxId: sb.ID, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), Lines: cfg.Lines(), Sources: cfg.Sources(), MinLevel: cfg.MinLevel(), - WorkspaceScope: namedWorkspaceScope(workspace), } if !cfg.Since().IsZero() { req.SinceMs = converter.MillisFromTime(cfg.Since()) diff --git a/sdk/go/openshell/v1/sandbox_client_test.go b/sdk/go/openshell/v1/sandbox_client_test.go index 0494249775..8184aa1931 100644 --- a/sdk/go/openshell/v1/sandbox_client_test.go +++ b/sdk/go/openshell/v1/sandbox_client_test.go @@ -84,9 +84,10 @@ func (s *mockSandboxServer) GetSandbox(_ context.Context, req *pb.GetSandboxRequ if s.getErr != nil { return nil, s.getErr } - sb, ok := s.sandboxes[req.GetName()] + name := req.GetSandbox() + sb, ok := s.sandboxes[name] if !ok { - return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetName()) + return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name) } cloned := proto.Clone(sb).(*pb.Sandbox) return &pb.SandboxResponse{Sandbox: cloned}, nil @@ -134,20 +135,22 @@ func (s *mockSandboxServer) DeleteSandbox(_ context.Context, req *pb.DeleteSandb if s.deleteErr != nil { return nil, s.deleteErr } - _, ok := s.sandboxes[req.GetName()] + name := req.GetSandbox() + _, ok := s.sandboxes[name] if !ok { - return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetName()) + return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name) } - delete(s.sandboxes, req.GetName()) + delete(s.sandboxes, name) return &pb.DeleteSandboxResponse{Deleted: true}, nil } func (s *mockSandboxServer) StopSandbox(_ context.Context, req *pb.StopSandboxRequest) (*pb.SandboxResponse, error) { s.mu.Lock() defer s.mu.Unlock() - sb, ok := s.sandboxes[req.GetName()] + name := req.GetSandbox() + sb, ok := s.sandboxes[name] if !ok { - return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetName()) + return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name) } sb.Status.Phase = pb.SandboxPhase_SANDBOX_PHASE_STOPPED return &pb.SandboxResponse{Sandbox: proto.Clone(sb).(*pb.Sandbox)}, nil @@ -156,9 +159,10 @@ func (s *mockSandboxServer) StopSandbox(_ context.Context, req *pb.StopSandboxRe func (s *mockSandboxServer) StartSandbox(_ context.Context, req *pb.StartSandboxRequest) (*pb.SandboxResponse, error) { s.mu.Lock() defer s.mu.Unlock() - sb, ok := s.sandboxes[req.GetName()] + name := req.GetSandbox() + sb, ok := s.sandboxes[name] if !ok { - return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetName()) + return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name) } sb.Status.Phase = pb.SandboxPhase_SANDBOX_PHASE_STARTING return &pb.SandboxResponse{Sandbox: proto.Clone(sb).(*pb.Sandbox)}, nil @@ -170,9 +174,10 @@ func (s *mockSandboxServer) AttachSandboxProvider(_ context.Context, req *pb.Att if s.attachErr != nil { return nil, s.attachErr } - sb, ok := s.sandboxes[req.GetSandboxName()] + name := req.GetSandbox() + sb, ok := s.sandboxes[name] if !ok { - return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetSandboxName()) + return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name) } sb.Spec.Providers = append(sb.Spec.Providers, req.GetProviderName()) return &pb.AttachSandboxProviderResponse{Sandbox: sb, Attached: true}, nil @@ -184,9 +189,10 @@ func (s *mockSandboxServer) DetachSandboxProvider(_ context.Context, req *pb.Det if s.detachErr != nil { return nil, s.detachErr } - sb, ok := s.sandboxes[req.GetSandboxName()] + name := req.GetSandbox() + sb, ok := s.sandboxes[name] if !ok { - return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetSandboxName()) + return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name) } return &pb.DetachSandboxProviderResponse{Sandbox: sb, Detached: true}, nil } @@ -195,7 +201,7 @@ func (s *mockSandboxServer) ListSandboxProviders(_ context.Context, req *pb.List if s.listProvErr != nil { return nil, s.listProvErr } - provs := s.providers[req.GetSandboxName()] + provs := s.providers[req.GetSandbox()] return &pb.ListSandboxProvidersResponse{Providers: provs}, nil } @@ -984,7 +990,7 @@ func TestSandboxWatch_MidStreamErrorDeliveredAsStatusError(t *testing.T) { // --- T016: Watch name-to-ID resolution verification tests --- -func TestSandboxWatch_ResolvesNameToID(t *testing.T) { +func TestSandboxWatch_UsesName(t *testing.T) { mock := newMockSandboxServer() mock.sandboxes["my-sandbox"] = &pb.Sandbox{ Metadata: &dm.ObjectMeta{Id: "resolved-id-123", Name: "my-sandbox"}, @@ -1005,12 +1011,12 @@ func TestSandboxWatch_ResolvesNameToID(t *testing.T) { require.NoError(t, err) defer w.Stop() - // Verify the WatchSandboxRequest.Id contains the resolved ID, not the name + // Verify the WatchSandboxRequest reference contains the resolved ID, not the name. mock.mu.Lock() req := mock.watchRequest mock.mu.Unlock() require.NotNil(t, req) - assert.Equal(t, "resolved-id-123", req.GetId(), "Watch should send resolved sandbox ID, not the name") + assert.Equal(t, "my-sandbox", req.GetSandbox()) } func TestSandboxWatch_ResolutionError(t *testing.T) { @@ -1221,7 +1227,7 @@ func TestSandboxGetLogs(t *testing.T) { // Verify name→id resolution: the proto request should contain the sandbox ID mock.mu.Lock() - assert.Equal(t, "sb-id-123", mock.getLogsRequest.GetSandboxId()) + assert.Equal(t, "log-sb", mock.getLogsRequest.GetSandbox()) mock.mu.Unlock() } @@ -1255,7 +1261,7 @@ func TestSandboxGetLogs_WithOptions(t *testing.T) { mock.mu.Lock() req := mock.getLogsRequest mock.mu.Unlock() - assert.Equal(t, "sb-id-opts", req.GetSandboxId()) + assert.Equal(t, "opts-sb", req.GetSandbox()) assert.Equal(t, uint32(50), req.GetLines()) assert.Equal(t, since.UnixMilli(), req.GetSinceMs()) assert.Equal(t, []string{"gateway", "sandbox"}, req.GetSources()) diff --git a/sdk/go/openshell/v1/service_client.go b/sdk/go/openshell/v1/service_client.go index fcca16936b..17948e388a 100644 --- a/sdk/go/openshell/v1/service_client.go +++ b/sdk/go/openshell/v1/service_client.go @@ -22,10 +22,10 @@ func newServiceClient(conn grpc.ClientConnInterface) *serviceClient { func (s *serviceClient) Expose(ctx context.Context, workspace, sandboxName, serviceName string, targetPort uint32, domain bool) (*ServiceEndpoint, error) { resp, err := s.client.ExposeService(ctx, &pb.ExposeServiceRequest{ Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), Service: serviceName, TargetPort: targetPort, Domain: domain, - WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -36,8 +36,8 @@ func (s *serviceClient) Expose(ctx context.Context, workspace, sandboxName, serv func (s *serviceClient) Get(ctx context.Context, workspace, sandboxName, serviceName string) (*ServiceEndpoint, error) { resp, err := s.client.GetService(ctx, &pb.GetServiceRequest{ Sandbox: sandboxName, - Service: serviceName, WorkspaceScope: namedWorkspaceScope(workspace), + Service: serviceName, }) if err != nil { return nil, converter.FromGRPCError(err) @@ -88,8 +88,8 @@ func (s *serviceClient) ListAll(ctx context.Context, workspace, sandboxName stri func (s *serviceClient) Delete(ctx context.Context, workspace, sandboxName, serviceName string) error { _, err := s.client.DeleteService(ctx, &pb.DeleteServiceRequest{ Sandbox: sandboxName, - Service: serviceName, WorkspaceScope: namedWorkspaceScope(workspace), + Service: serviceName, }) if err != nil { return converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/service_client_test.go b/sdk/go/openshell/v1/service_client_test.go index ade5de5af4..93855737a2 100644 --- a/sdk/go/openshell/v1/service_client_test.go +++ b/sdk/go/openshell/v1/service_client_test.go @@ -76,9 +76,10 @@ func (s *mockServiceServer) GetService(_ context.Context, req *pb.GetServiceRequ return nil, s.getErr } - ep, ok := s.endpoints[serviceKey(req.GetSandbox(), req.GetService())] + sandboxName := req.GetSandbox() + ep, ok := s.endpoints[serviceKey(sandboxName, req.GetService())] if !ok { - return nil, status.Errorf(codes.NotFound, "service %q not found in sandbox %q", req.GetService(), req.GetSandbox()) + return nil, status.Errorf(codes.NotFound, "service %q not found in sandbox %q", req.GetService(), sandboxName) } return ep, nil } @@ -93,8 +94,9 @@ func (s *mockServiceServer) ListServices(_ context.Context, req *pb.ListServices var services []*pb.ServiceEndpointResponse for key, ep := range s.endpoints { - prefix := req.GetSandbox() + "/" - if req.GetSandbox() == "" || (len(key) >= len(prefix) && key[:len(prefix)] == prefix) { + sandboxName := req.GetSandbox() + prefix := sandboxName + "/" + if sandboxName == "" || (len(key) >= len(prefix) && key[:len(prefix)] == prefix) { services = append(services, ep) } } @@ -108,10 +110,11 @@ func (s *mockServiceServer) DeleteService(_ context.Context, req *pb.DeleteServi return nil, s.deleteErr } - key := serviceKey(req.GetSandbox(), req.GetService()) + sandboxName := req.GetSandbox() + key := serviceKey(sandboxName, req.GetService()) _, ok := s.endpoints[key] if !ok { - return nil, status.Errorf(codes.NotFound, "service %q not found in sandbox %q", req.GetService(), req.GetSandbox()) + return nil, status.Errorf(codes.NotFound, "service %q not found in sandbox %q", req.GetService(), sandboxName) } delete(s.endpoints, key) return &pb.DeleteServiceResponse{Deleted: true}, nil diff --git a/sdk/go/openshell/v1/ssh.go b/sdk/go/openshell/v1/ssh.go index 19a4b65a3e..0e61a5f804 100644 --- a/sdk/go/openshell/v1/ssh.go +++ b/sdk/go/openshell/v1/ssh.go @@ -31,7 +31,7 @@ func WithTunnelServiceID(id string) TunnelOption { // SSHInterface defines operations for managing SSH sessions. type SSHInterface interface { - CreateSession(ctx context.Context, workspace, sandboxID string) (*SSHSession, error) + CreateSession(ctx context.Context, workspace, sandboxName string) (*SSHSession, error) RevokeSession(ctx context.Context, workspace, token string) (bool, error) Tunnel(ctx context.Context, workspace, sandboxName string, port uint32, opts ...TunnelOption) (io.ReadWriteCloser, error) } diff --git a/sdk/go/openshell/v1/ssh_client.go b/sdk/go/openshell/v1/ssh_client.go index 6e7ee1a462..c6a4a36ca5 100644 --- a/sdk/go/openshell/v1/ssh_client.go +++ b/sdk/go/openshell/v1/ssh_client.go @@ -30,9 +30,10 @@ func newSSHClient(conn grpc.ClientConnInterface, sandboxes SandboxInterface) *ss } } -func (s *sshClient) CreateSession(ctx context.Context, _, sandboxID string) (*SSHSession, error) { +func (s *sshClient) CreateSession(ctx context.Context, workspace, sandboxName string) (*SSHSession, error) { resp, err := s.client.CreateSshSession(ctx, &pb.CreateSshSessionRequest{ - SandboxId: sandboxID, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -63,16 +64,14 @@ func (s *sshClient) Tunnel(ctx context.Context, workspace, sandboxName string, p Message: fmt.Sprintf("port must be in range 1-65535, got %d", port), } } + if _, err := s.sandboxes.Get(ctx, workspace, sandboxName); err != nil { + return nil, err + } var cfg tunnelConfig options.Apply(&cfg, opts) - sandbox, err := s.sandboxes.Get(ctx, workspace, sandboxName) - if err != nil { - return nil, err - } - - session, err := s.CreateSession(ctx, workspace, sandbox.ID) + session, err := s.CreateSession(ctx, workspace, sandboxName) if err != nil { return nil, err } @@ -94,7 +93,8 @@ func (s *sshClient) Tunnel(ctx context.Context, workspace, sandboxName string, p initFrame := &pb.TcpForwardFrame{ Payload: &pb.TcpForwardFrame_Init{ Init: &pb.TcpForwardInit{ - SandboxId: sandbox.ID, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), ServiceId: cfg.serviceID, AuthorizationToken: session.Token, Target: &pb.TcpForwardInit_Ssh{ diff --git a/sdk/go/openshell/v1/ssh_client_test.go b/sdk/go/openshell/v1/ssh_client_test.go index d06edb115b..f2380ba915 100644 --- a/sdk/go/openshell/v1/ssh_client_test.go +++ b/sdk/go/openshell/v1/ssh_client_test.go @@ -51,13 +51,14 @@ func (s *mockSSHServer) CreateSshSession(_ context.Context, req *pb.CreateSshSes return nil, s.createErr } - token := "tok-" + req.GetSandboxId() + sandboxName := req.GetSandbox() + token := "tok-" + sandboxName if s.nextToken != "" { token = s.nextToken } resp := &pb.CreateSshSessionResponse{ - SandboxId: req.GetSandboxId(), + SandboxId: "id-" + sandboxName, Token: token, GatewayHost: "gw.example.com", GatewayPort: 2222, @@ -65,7 +66,7 @@ func (s *mockSSHServer) CreateSshSession(_ context.Context, req *pb.CreateSshSes HostKeyFingerprint: "SHA256:abc123", ExpiresAtMs: 1700000000000, } - s.sessions[req.GetSandboxId()] = resp + s.sessions[sandboxName] = resp s.tokens[token] = true return resp, nil } @@ -225,7 +226,7 @@ func TestSSHCreateSession(t *testing.T) { require.NoError(t, err) require.NotNil(t, session) - assert.Equal(t, "my-sandbox", session.SandboxID) + assert.Equal(t, "id-my-sandbox", session.SandboxID) assert.Equal(t, "tok-my-sandbox", session.Token) assert.Equal(t, "gw.example.com", session.GatewayHost) assert.Equal(t, uint32(2222), session.GatewayPort) @@ -332,7 +333,7 @@ func TestSSHTunnel_Success(t *testing.T) { mock.mu.Unlock() require.NotNil(t, init) - assert.Equal(t, "sb-123", init.GetSandboxId()) + assert.Equal(t, "my-sandbox", init.GetSandbox()) assert.NotEmpty(t, init.GetAuthorizationToken()) assert.NotNil(t, init.GetSsh(), "target should be SshRelayTarget") } diff --git a/sdk/go/openshell/v1/tcp_client.go b/sdk/go/openshell/v1/tcp_client.go index e0795a9682..e5f89b8457 100644 --- a/sdk/go/openshell/v1/tcp_client.go +++ b/sdk/go/openshell/v1/tcp_client.go @@ -37,9 +37,7 @@ func (t *tcpClient) Forward(ctx context.Context, workspace, sandboxName string, Message: fmt.Sprintf("port must be in range 1-65535, got %d", port), } } - - sb, err := t.sandboxes.Get(ctx, workspace, sandboxName) - if err != nil { + if _, err := t.sandboxes.Get(ctx, workspace, sandboxName); err != nil { return nil, err } @@ -56,8 +54,9 @@ func (t *tcpClient) Forward(ctx context.Context, workspace, sandboxName string, initFrame := &pb.TcpForwardFrame{ Payload: &pb.TcpForwardFrame_Init{ Init: &pb.TcpForwardInit{ - SandboxId: sb.ID, - ServiceId: cfg.serviceID, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), + ServiceId: cfg.serviceID, Target: &pb.TcpForwardInit_Tcp{ Tcp: &pb.TcpRelayTarget{ Host: "127.0.0.1", diff --git a/sdk/go/openshell/v1/tcp_client_test.go b/sdk/go/openshell/v1/tcp_client_test.go index 4ea96dbbab..35b3751020 100644 --- a/sdk/go/openshell/v1/tcp_client_test.go +++ b/sdk/go/openshell/v1/tcp_client_test.go @@ -130,7 +130,7 @@ func TestTCPForward_InitFrame(t *testing.T) { mock.mu.Unlock() require.NotNil(t, init) - assert.Equal(t, "sb-my-sandbox", init.GetSandboxId()) + assert.Equal(t, "my-sandbox", init.GetSandbox()) assert.Empty(t, init.GetServiceId(), "service_id should be empty per FR-007a") assert.Empty(t, init.GetAuthorizationToken()) @@ -336,7 +336,7 @@ func TestTCPForward_WithServiceID(t *testing.T) { require.NotNil(t, init) assert.Equal(t, "audit-svc", init.GetServiceId()) - assert.Equal(t, "sb-my-sandbox", init.GetSandboxId()) + assert.Equal(t, "my-sandbox", init.GetSandbox()) } func TestTCPForward_WithoutOptions_BackwardCompat(t *testing.T) { @@ -393,7 +393,7 @@ func TestTCPForward_ServerError(t *testing.T) { // --- Name-to-ID resolution tests --- -func TestTCPForward_ResolvesNameToID(t *testing.T) { +func TestTCPForward_UsesName(t *testing.T) { mock := newMockTCPServer() client, cleanup := setupTCPTest(t, mock) defer cleanup() @@ -416,7 +416,7 @@ func TestTCPForward_ResolvesNameToID(t *testing.T) { require.NotNil(t, init) // stubSandboxResolver returns ID "sb-" — verify the proto has the resolved ID, not the name - assert.Equal(t, "sb-my-sandbox", init.GetSandboxId(), "Forward should send resolved sandbox ID, not the name") + assert.Equal(t, "my-sandbox", init.GetSandbox()) } func TestTCPForward_ResolutionError(t *testing.T) { diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 735c17bc88..7017b2c0b1 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -3086,10 +3086,8 @@ func (x *BeginRootfsTarStagingResponse) GetExpiresAtMs() int64 { // Get sandbox request. type GetSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3125,9 +3123,9 @@ func (*GetSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{41} } -func (x *GetSandboxRequest) GetName() string { +func (x *GetSandboxRequest) GetSandbox() string { if x != nil { - return x.Name + return x.Sandbox } return "" } @@ -3216,10 +3214,8 @@ func (x *ListSandboxesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelecto // List providers attached to a sandbox request. type ListSandboxProvidersRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3255,9 +3251,9 @@ func (*ListSandboxProvidersRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{43} } -func (x *ListSandboxProvidersRequest) GetSandboxName() string { +func (x *ListSandboxProvidersRequest) GetSandbox() string { if x != nil { - return x.SandboxName + return x.Sandbox } return "" } @@ -3271,20 +3267,18 @@ func (x *ListSandboxProvidersRequest) GetWorkspaceScope() *datamodelv1.Workspace // Attach provider to sandbox request. type AttachSandboxProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // Provider name to attach. ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` // Expected resource version for optimistic concurrency control. // If 0, the server uses the current version (backward compatibility). // If non-zero, the server validates that the sandbox's current resource_version // matches this value before applying the mutation, returning ABORTED on mismatch. - ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. - WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttachSandboxProviderRequest) Reset() { @@ -3317,9 +3311,9 @@ func (*AttachSandboxProviderRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{44} } -func (x *AttachSandboxProviderRequest) GetSandboxName() string { +func (x *AttachSandboxProviderRequest) GetSandbox() string { if x != nil { - return x.SandboxName + return x.Sandbox } return "" } @@ -3347,20 +3341,18 @@ func (x *AttachSandboxProviderRequest) GetWorkspaceScope() *datamodelv1.Workspac // Detach provider from sandbox request. type DetachSandboxProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // Provider name to detach. ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` // Expected resource version for optimistic concurrency control. // If 0, the server uses the current version (backward compatibility). // If non-zero, the server validates that the sandbox's current resource_version // matches this value before applying the mutation, returning ABORTED on mismatch. - ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. - WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DetachSandboxProviderRequest) Reset() { @@ -3393,9 +3385,9 @@ func (*DetachSandboxProviderRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{45} } -func (x *DetachSandboxProviderRequest) GetSandboxName() string { +func (x *DetachSandboxProviderRequest) GetSandbox() string { if x != nil { - return x.SandboxName + return x.Sandbox } return "" } @@ -3423,10 +3415,8 @@ func (x *DetachSandboxProviderRequest) GetWorkspaceScope() *datamodelv1.Workspac // Delete sandbox request. type DeleteSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3462,9 +3452,9 @@ func (*DeleteSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{46} } -func (x *DeleteSandboxRequest) GetName() string { +func (x *DeleteSandboxRequest) GetSandbox() string { if x != nil { - return x.Name + return x.Sandbox } return "" } @@ -3478,10 +3468,8 @@ func (x *DeleteSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelecto // Stop sandbox request. type StopSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3517,9 +3505,9 @@ func (*StopSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{47} } -func (x *StopSandboxRequest) GetName() string { +func (x *StopSandboxRequest) GetSandbox() string { if x != nil { - return x.Name + return x.Sandbox } return "" } @@ -3533,10 +3521,8 @@ func (x *StopSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector // Start sandbox request. type StartSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3572,9 +3558,9 @@ func (*StartSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{48} } -func (x *StartSandboxRequest) GetName() string { +func (x *StartSandboxRequest) GetSandbox() string { if x != nil { - return x.Name + return x.Sandbox } return "" } @@ -3885,11 +3871,11 @@ func (x *DeleteSandboxResponse) GetDeleted() bool { // Create SSH session request. type CreateSshSessionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,2,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateSshSessionRequest) Reset() { @@ -3922,13 +3908,20 @@ func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{55} } -func (x *CreateSshSessionRequest) GetSandboxId() string { +func (x *CreateSshSessionRequest) GetSandbox() string { if x != nil { - return x.SandboxId + return x.Sandbox } return "" } +func (x *CreateSshSessionRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + // Create SSH session response. // // Fields are interpolated into an SSH `ProxyCommand` string that OpenSSH @@ -4042,15 +4035,13 @@ func (x *CreateSshSessionResponse) GetExpiresAtMs() int64 { // Request to expose an HTTP service running inside a sandbox. type ExposeServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // Service name within the sandbox. Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` // Loopback TCP port inside the sandbox. TargetPort uint32 `protobuf:"varint,3,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` // Whether to print/use the browser-facing service URL. - Domain bool `protobuf:"varint,4,opt,name=domain,proto3" json:"domain,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + Domain bool `protobuf:"varint,4,opt,name=domain,proto3" json:"domain,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,6,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -4086,13 +4077,6 @@ func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{57} } -func (x *ExposeServiceRequest) GetSandbox() string { - if x != nil { - return x.Sandbox - } - return "" -} - func (x *ExposeServiceRequest) GetService() string { if x != nil { return x.Service @@ -4114,6 +4098,13 @@ func (x *ExposeServiceRequest) GetDomain() bool { return false } +func (x *ExposeServiceRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + func (x *ExposeServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { return x.WorkspaceScope @@ -4124,11 +4115,9 @@ func (x *ExposeServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelecto // Request to fetch an exposed sandbox service endpoint. type GetServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // Service name within the sandbox. Empty selects the unnamed endpoint. - Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -4164,16 +4153,16 @@ func (*GetServiceRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{58} } -func (x *GetServiceRequest) GetSandbox() string { +func (x *GetServiceRequest) GetService() string { if x != nil { - return x.Sandbox + return x.Service } return "" } -func (x *GetServiceRequest) GetService() string { +func (x *GetServiceRequest) GetSandbox() string { if x != nil { - return x.Service + return x.Sandbox } return "" } @@ -4188,8 +4177,6 @@ func (x *GetServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { // Request to list exposed sandbox service endpoints. type ListServicesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Optional sandbox name. Empty lists endpoints for all sandboxes. - Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // The maximum number of services to return. Zero uses 100. Values above // 1000 are coerced to 1000; negative values are invalid. PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` @@ -4198,8 +4185,10 @@ type ListServicesRequest struct { PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` // Explicit named or all-workspaces scope. WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,6,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Optional sandbox name. Empty lists endpoints for all sandboxes. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListServicesRequest) Reset() { @@ -4232,13 +4221,6 @@ func (*ListServicesRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{59} } -func (x *ListServicesRequest) GetSandbox() string { - if x != nil { - return x.Sandbox - } - return "" -} - func (x *ListServicesRequest) GetPageSize() int32 { if x != nil { return x.PageSize @@ -4260,6 +4242,13 @@ func (x *ListServicesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector return nil } +func (x *ListServicesRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + // Response containing exposed sandbox service endpoints. type ListServicesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4317,11 +4306,9 @@ func (x *ListServicesResponse) GetNextPageToken() string { // Request to delete an exposed sandbox service endpoint. type DeleteServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // Service name within the sandbox. Empty selects the unnamed endpoint. - Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -4357,16 +4344,16 @@ func (*DeleteServiceRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{61} } -func (x *DeleteServiceRequest) GetSandbox() string { +func (x *DeleteServiceRequest) GetService() string { if x != nil { - return x.Sandbox + return x.Service } return "" } -func (x *DeleteServiceRequest) GetService() string { +func (x *DeleteServiceRequest) GetSandbox() string { if x != nil { - return x.Service + return x.Sandbox } return "" } @@ -4663,8 +4650,6 @@ func (x *RevokeSshSessionResponse) GetRevoked() bool { // Execute command request. type ExecSandboxRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` // Command and arguments. Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` // Optional working directory. @@ -4686,9 +4671,11 @@ type ExecSandboxRequest struct { // (`bash -lc`) so user startup files (.bash_profile/.profile, and .bashrc if // sourced by them) are applied. When true, the command runs without those // files (`bash -c`), for automation that needs predictable startup behavior. - NoLoginShell bool `protobuf:"varint,10,opt,name=no_login_shell,json=noLoginShell,proto3" json:"no_login_shell,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + NoLoginShell bool `protobuf:"varint,10,opt,name=no_login_shell,json=noLoginShell,proto3" json:"no_login_shell,omitempty"` + Sandbox string `protobuf:"bytes,11,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,12,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExecSandboxRequest) Reset() { @@ -4721,13 +4708,6 @@ func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{67} } -func (x *ExecSandboxRequest) GetSandboxId() string { - if x != nil { - return x.SandboxId - } - return "" -} - func (x *ExecSandboxRequest) GetCommand() []string { if x != nil { return x.Command @@ -4791,6 +4771,20 @@ func (x *ExecSandboxRequest) GetNoLoginShell() bool { return false } +func (x *ExecSandboxRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ExecSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + // One stdout chunk from a sandbox exec. type ExecSandboxStdout struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -5027,9 +5021,9 @@ func (*ExecSandboxEvent_Exit) isExecSandboxEvent_Payload() {} // Initial frame for one TCP forward stream. type TcpForwardInit struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,2,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` // Optional service identifier for audit/correlation. ServiceId string `protobuf:"bytes,4,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` // Target the gateway should request from the supervisor. @@ -5076,13 +5070,20 @@ func (*TcpForwardInit) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{72} } -func (x *TcpForwardInit) GetSandboxId() string { +func (x *TcpForwardInit) GetSandbox() string { if x != nil { - return x.SandboxId + return x.Sandbox } return "" } +func (x *TcpForwardInit) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + func (x *TcpForwardInit) GetServiceId() string { if x != nil { return x.ServiceId @@ -5462,8 +5463,6 @@ func (x *SshSession) GetRevoked() bool { // Watch sandbox request. type WatchSandboxRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // Stream sandbox status snapshots. FollowStatus bool `protobuf:"varint,2,opt,name=follow_status,json=followStatus,proto3" json:"follow_status,omitempty"` // Stream openshell-server process logs correlated to this sandbox. @@ -5483,9 +5482,11 @@ type WatchSandboxRequest struct { // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. LogSources []string `protobuf:"bytes,9,rep,name=log_sources,json=logSources,proto3" json:"log_sources,omitempty"` // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. - LogMinLevel string `protobuf:"bytes,10,opt,name=log_min_level,json=logMinLevel,proto3" json:"log_min_level,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + LogMinLevel string `protobuf:"bytes,10,opt,name=log_min_level,json=logMinLevel,proto3" json:"log_min_level,omitempty"` + Sandbox string `protobuf:"bytes,11,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,12,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WatchSandboxRequest) Reset() { @@ -5518,13 +5519,6 @@ func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{77} } -func (x *WatchSandboxRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - func (x *WatchSandboxRequest) GetFollowStatus() bool { if x != nil { return x.FollowStatus @@ -5588,6 +5582,20 @@ func (x *WatchSandboxRequest) GetLogMinLevel() string { return "" } +func (x *WatchSandboxRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *WatchSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + // One event in a sandbox watch stream. type SandboxStreamEvent struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -8944,9 +8952,6 @@ func (x *ExchangeProviderSubjectTokenResponse) GetTokenType() string { // Update sandbox policy request. type UpdateConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). Required for sandbox-scoped updates. - // Not required when `global=true`. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The new policy to apply. // // Sandbox scope (`global=false`): @@ -8980,8 +8985,8 @@ type UpdateConfigRequest struct { // sandbox metadata as a convenience projection. For setting-only updates, it // only merges them into sandbox metadata. Annotations map[string]string `protobuf:"bytes,9,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Explicit workspace scope for sandbox-scoped updates. Omit only when - // `global` is true; the all-workspaces selection is invalid. + // Required for sandbox-scoped updates and empty for global updates. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,11,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -9017,13 +9022,6 @@ func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{127} } -func (x *UpdateConfigRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - func (x *UpdateConfigRequest) GetPolicy() *sandboxv1.SandboxPolicy { if x != nil { return x.Policy @@ -9080,6 +9078,13 @@ func (x *UpdateConfigRequest) GetAnnotations() map[string]string { return nil } +func (x *UpdateConfigRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + func (x *UpdateConfigRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { return x.WorkspaceScope @@ -9646,14 +9651,11 @@ func (x *UpdateConfigResponse) GetAnnotations() map[string]string { // Get sandbox policy status request. type GetSandboxPolicyStatusRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). Ignored when global is true. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The specific policy version to query. 0 means latest. Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` // Query global policy revisions instead of a sandbox-scoped one. - Global bool `protobuf:"varint,3,opt,name=global,proto3" json:"global,omitempty"` - // Explicit workspace scope for sandbox-scoped queries. Omit only when - // `global` is true; the all-workspaces selection is invalid. + Global bool `protobuf:"varint,3,opt,name=global,proto3" json:"global,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -9689,13 +9691,6 @@ func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{136} } -func (x *GetSandboxPolicyStatusRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - func (x *GetSandboxPolicyStatusRequest) GetVersion() uint32 { if x != nil { return x.Version @@ -9710,6 +9705,13 @@ func (x *GetSandboxPolicyStatusRequest) GetGlobal() bool { return false } +func (x *GetSandboxPolicyStatusRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + func (x *GetSandboxPolicyStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { return x.WorkspaceScope @@ -9775,8 +9777,6 @@ func (x *GetSandboxPolicyStatusResponse) GetActiveVersion() uint32 { // List sandbox policies request. type ListSandboxPoliciesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). Ignored when global is true. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The maximum number of revisions to return. Zero uses 100. Values above // 1000 are coerced to 1000; negative values are invalid. PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` @@ -9784,9 +9784,8 @@ type ListSandboxPoliciesRequest struct { // parameters except page_size must match the request that produced it. PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` // List global policy revisions instead of sandbox-scoped ones. - Global bool `protobuf:"varint,4,opt,name=global,proto3" json:"global,omitempty"` - // Explicit workspace scope for sandbox-scoped queries. Omit only when - // `global` is true; the all-workspaces selection is invalid. + Global bool `protobuf:"varint,4,opt,name=global,proto3" json:"global,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,6,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -9822,13 +9821,6 @@ func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{138} } -func (x *ListSandboxPoliciesRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - func (x *ListSandboxPoliciesRequest) GetPageSize() int32 { if x != nil { return x.PageSize @@ -9850,6 +9842,13 @@ func (x *ListSandboxPoliciesRequest) GetGlobal() bool { return false } +func (x *ListSandboxPoliciesRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + func (x *ListSandboxPoliciesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { return x.WorkspaceScope @@ -10140,8 +10139,6 @@ func (x *SandboxPolicyRevision) GetProvenance() map[string]string { // Get sandbox logs request (one-shot fetch). type GetSandboxLogsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` // Maximum number of log lines to return. 0 means use default (2000). Lines uint32 `protobuf:"varint,2,opt,name=lines,proto3" json:"lines,omitempty"` // Only include logs with timestamp >= this value (ms since epoch). 0 means no filter. @@ -10149,8 +10146,8 @@ type GetSandboxLogsRequest struct { // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. Sources []string `protobuf:"bytes,4,rep,name=sources,proto3" json:"sources,omitempty"` // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. - MinLevel string `protobuf:"bytes,5,opt,name=min_level,json=minLevel,proto3" json:"min_level,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + MinLevel string `protobuf:"bytes,5,opt,name=min_level,json=minLevel,proto3" json:"min_level,omitempty"` + Sandbox string `protobuf:"bytes,8,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,7,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -10186,13 +10183,6 @@ func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{143} } -func (x *GetSandboxLogsRequest) GetSandboxId() string { - if x != nil { - return x.SandboxId - } - return "" -} - func (x *GetSandboxLogsRequest) GetLines() uint32 { if x != nil { return x.Lines @@ -10221,6 +10211,13 @@ func (x *GetSandboxLogsRequest) GetMinLevel() string { return "" } +func (x *GetSandboxLogsRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + func (x *GetSandboxLogsRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { return x.WorkspaceScope @@ -12223,11 +12220,13 @@ type SubmitPolicyAnalysisRequest struct { // to watch. Other values are treated as agent-style (no dedup) so a new // mode does not silently collapse proposals. AnalysisMode string `protobuf:"bytes,3,opt,name=analysis_mode,json=analysisMode,proto3" json:"analysis_mode,omitempty"` - // Sandbox name. + // Sandbox name. The authenticated sandbox principal remains authoritative + // for this internal callback. Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` // Anonymous network activity counters. NetworkActivitySummaries []*NetworkActivitySummary `protobuf:"bytes,5,rep,name=network_activity_summaries,json=networkActivitySummaries,proto3" json:"network_activity_summaries,omitempty"` - // Workspace scope. Empty defaults to "default". + // Internal callback workspace. The gateway validates it against the + // authenticated sandbox principal. Workspace string `protobuf:"bytes,6,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -12382,11 +12381,9 @@ func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunkIds() []string { // Get draft policy for a sandbox. type GetDraftPolicyRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Optional status filter: "pending", "approved", "rejected", or "" for all. - StatusFilter string `protobuf:"bytes,2,opt,name=status_filter,json=statusFilter,proto3" json:"status_filter,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + StatusFilter string `protobuf:"bytes,2,opt,name=status_filter,json=statusFilter,proto3" json:"status_filter,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -12422,16 +12419,16 @@ func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{173} } -func (x *GetDraftPolicyRequest) GetName() string { +func (x *GetDraftPolicyRequest) GetStatusFilter() string { if x != nil { - return x.Name + return x.StatusFilter } return "" } -func (x *GetDraftPolicyRequest) GetStatusFilter() string { +func (x *GetDraftPolicyRequest) GetSandbox() string { if x != nil { - return x.StatusFilter + return x.Sandbox } return "" } @@ -12518,14 +12515,12 @@ func (x *GetDraftPolicyResponse) GetLastAnalyzedAtMs() int64 { // Approve a single draft chunk. type ApproveDraftChunkRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Chunk ID to approve. ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` // Token returned with the reviewed PolicyChunk. Approval fails with // FAILED_PRECONDITION if live decision inputs no longer match it. - ReviewToken string `protobuf:"bytes,4,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + ReviewToken string `protobuf:"bytes,4,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -12561,23 +12556,23 @@ func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{175} } -func (x *ApproveDraftChunkRequest) GetName() string { +func (x *ApproveDraftChunkRequest) GetChunkId() string { if x != nil { - return x.Name + return x.ChunkId } return "" } -func (x *ApproveDraftChunkRequest) GetChunkId() string { +func (x *ApproveDraftChunkRequest) GetReviewToken() string { if x != nil { - return x.ChunkId + return x.ReviewToken } return "" } -func (x *ApproveDraftChunkRequest) GetReviewToken() string { +func (x *ApproveDraftChunkRequest) GetSandbox() string { if x != nil { - return x.ReviewToken + return x.Sandbox } return "" } @@ -12646,13 +12641,11 @@ func (x *ApproveDraftChunkResponse) GetPolicyHash() string { // Reject a single draft chunk. type RejectDraftChunkRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Chunk ID to reject. ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` // Optional reason for rejection (fed to LLM context in future analysis). - Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -12688,23 +12681,23 @@ func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{177} } -func (x *RejectDraftChunkRequest) GetName() string { +func (x *RejectDraftChunkRequest) GetChunkId() string { if x != nil { - return x.Name + return x.ChunkId } return "" } -func (x *RejectDraftChunkRequest) GetChunkId() string { +func (x *RejectDraftChunkRequest) GetReason() string { if x != nil { - return x.ChunkId + return x.Reason } return "" } -func (x *RejectDraftChunkRequest) GetReason() string { +func (x *RejectDraftChunkRequest) GetSandbox() string { if x != nil { - return x.Reason + return x.Sandbox } return "" } @@ -12807,14 +12800,12 @@ func (x *DraftChunkApproval) GetReviewToken() string { type ApproveAllDraftChunksRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Include chunks with security_notes (default false: skips them). IncludeSecurityFlagged bool `protobuf:"varint,2,opt,name=include_security_flagged,json=includeSecurityFlagged,proto3" json:"include_security_flagged,omitempty"` // Exact reviewed chunks and tokens. The server validates them against one // live snapshot, stages compatible operations in order, and writes once. - Approvals []*DraftChunkApproval `protobuf:"bytes,4,rep,name=approvals,proto3" json:"approvals,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + Approvals []*DraftChunkApproval `protobuf:"bytes,4,rep,name=approvals,proto3" json:"approvals,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -12850,13 +12841,6 @@ func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{180} } -func (x *ApproveAllDraftChunksRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - func (x *ApproveAllDraftChunksRequest) GetIncludeSecurityFlagged() bool { if x != nil { return x.IncludeSecurityFlagged @@ -12871,6 +12855,13 @@ func (x *ApproveAllDraftChunksRequest) GetApprovals() []*DraftChunkApproval { return nil } +func (x *ApproveAllDraftChunksRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + func (x *ApproveAllDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { return x.WorkspaceScope @@ -12954,13 +12945,11 @@ func (x *ApproveAllDraftChunksResponse) GetChunksSkipped() uint32 { // Edit a pending chunk in-place. type EditDraftChunkRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Chunk ID to edit. ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` // The modified rule (replaces existing proposed_rule). - ProposedRule *sandboxv1.NetworkPolicyRule `protobuf:"bytes,3,opt,name=proposed_rule,json=proposedRule,proto3" json:"proposed_rule,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + ProposedRule *sandboxv1.NetworkPolicyRule `protobuf:"bytes,3,opt,name=proposed_rule,json=proposedRule,proto3" json:"proposed_rule,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -12996,13 +12985,6 @@ func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{182} } -func (x *EditDraftChunkRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - func (x *EditDraftChunkRequest) GetChunkId() string { if x != nil { return x.ChunkId @@ -13017,6 +12999,13 @@ func (x *EditDraftChunkRequest) GetProposedRule() *sandboxv1.NetworkPolicyRule { return nil } +func (x *EditDraftChunkRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + func (x *EditDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { return x.WorkspaceScope @@ -13063,11 +13052,9 @@ func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { // Reverse an approval (remove merged rule from active policy). type UndoDraftChunkRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Chunk ID to undo. - ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -13103,16 +13090,16 @@ func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{184} } -func (x *UndoDraftChunkRequest) GetName() string { +func (x *UndoDraftChunkRequest) GetChunkId() string { if x != nil { - return x.Name + return x.ChunkId } return "" } -func (x *UndoDraftChunkRequest) GetChunkId() string { +func (x *UndoDraftChunkRequest) GetSandbox() string { if x != nil { - return x.ChunkId + return x.Sandbox } return "" } @@ -13180,10 +13167,8 @@ func (x *UndoDraftChunkResponse) GetPolicyHash() string { // Clear all pending draft chunks for a sandbox. type ClearDraftChunksRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -13219,9 +13204,9 @@ func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{186} } -func (x *ClearDraftChunksRequest) GetName() string { +func (x *ClearDraftChunksRequest) GetSandbox() string { if x != nil { - return x.Name + return x.Sandbox } return "" } @@ -13280,10 +13265,8 @@ func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { // Get decision history for a sandbox's draft policy. type GetDraftHistoryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Explicit workspace scope. The all-workspaces selection is invalid. + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -13319,9 +13302,9 @@ func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{188} } -func (x *GetDraftHistoryRequest) GetName() string { +func (x *GetDraftHistoryRequest) GetSandbox() string { if x != nil { - return x.Name + return x.Sandbox } return "" } @@ -14511,38 +14494,38 @@ const file_openshell_proto_rawDesc = "" + "\vupload_path\x18\x02 \x01(\tR\n" + "uploadPath\x12\x1b\n" + "\tmax_bytes\x18\x03 \x01(\x04R\bmaxBytes\x12\"\n" + - "\rexpires_at_ms\x18\x04 \x01(\x03R\vexpiresAtMs\"\x8c\x01\n" + - "\x11GetSandboxRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12R\n" + - "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\xf4\x01\n" + + "\rexpires_at_ms\x18\x04 \x01(\x03R\vexpiresAtMs\"\x98\x01\n" + + "\x11GetSandboxRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\x04nameR\tworkspace\"\xf4\x01\n" + "\x14ListSandboxesRequest\x12\x1b\n" + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" + "\n" + "page_token\x18\x02 \x01(\tR\tpageToken\x12%\n" + "\x0elabel_selector\x18\x03 \x01(\tR\rlabelSelector\x12R\n" + - "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\tworkspaceR\x0eall_workspaces\"\xa5\x01\n" + - "\x1bListSandboxProvidersRequest\x12!\n" + - "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12R\n" + - "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\x87\x02\n" + - "\x1cAttachSandboxProviderRequest\x12!\n" + - "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12#\n" + + "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\tworkspaceR\x0eall_workspaces\"\x9c\x01\n" + + "\x1bListSandboxProvidersRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\xfe\x01\n" + + "\x1cAttachSandboxProviderRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12#\n" + "\rprovider_name\x18\x02 \x01(\tR\fproviderName\x12:\n" + "\x19expected_resource_version\x18\x03 \x01(\x04R\x17expectedResourceVersion\x12R\n" + - "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\tworkspace\"\x87\x02\n" + - "\x1cDetachSandboxProviderRequest\x12!\n" + - "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12#\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\tworkspace\"\xfe\x01\n" + + "\x1cDetachSandboxProviderRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12#\n" + "\rprovider_name\x18\x02 \x01(\tR\fproviderName\x12:\n" + "\x19expected_resource_version\x18\x03 \x01(\x04R\x17expectedResourceVersion\x12R\n" + - "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\tworkspace\"\x8f\x01\n" + - "\x14DeleteSandboxRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12R\n" + - "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\x8d\x01\n" + - "\x12StopSandboxRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12R\n" + - "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\x8e\x01\n" + - "\x13StartSandboxRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12R\n" + - "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"B\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\tworkspace\"\x9b\x01\n" + + "\x14DeleteSandboxRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\x04nameR\tworkspace\"\x99\x01\n" + + "\x12StopSandboxRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\x04nameR\tworkspace\"\x9a\x01\n" + + "\x13StartSandboxRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\x04nameR\tworkspace\"B\n" + "\x0fSandboxResponse\x12/\n" + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\"t\n" + "\x15ListSandboxesResponse\x123\n" + @@ -14557,10 +14540,11 @@ const file_openshell_proto_rawDesc = "" + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12\x1a\n" + "\bdetached\x18\x02 \x01(\bR\bdetached\"1\n" + "\x15DeleteSandboxResponse\x12\x18\n" + - "\adeleted\x18\x01 \x01(\bR\adeleted\"8\n" + - "\x17CreateSshSessionRequest\x12\x1d\n" + - "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x98\x02\n" + + "\adeleted\x18\x01 \x01(\bR\adeleted\"\x99\x01\n" + + "\x17CreateSshSessionRequest\x12\x18\n" + + "\asandbox\x18\x02 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x01\x10\x02R\n" + + "sandbox_id\"\x98\x02\n" + "\x18CreateSshSessionResponse\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1a\n" + @@ -14571,28 +14555,28 @@ const file_openshell_proto_rawDesc = "" + "\x14host_key_fingerprint\x18\a \x01(\tR\x12hostKeyFingerprint\x12\"\n" + "\rexpires_at_ms\x18\b \x01(\x03R\vexpiresAtMs\"\xe8\x01\n" + "\x14ExposeServiceRequest\x12\x18\n" + - "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + "\aservice\x18\x02 \x01(\tR\aservice\x12\x1f\n" + "\vtarget_port\x18\x03 \x01(\rR\n" + "targetPort\x12\x16\n" + - "\x06domain\x18\x04 \x01(\bR\x06domain\x12R\n" + + "\x06domain\x18\x04 \x01(\bR\x06domain\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x05\x10\x06R\tworkspace\"\xac\x01\n" + "\x11GetServiceRequest\x12\x18\n" + - "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + - "\aservice\x18\x02 \x01(\tR\aservice\x12R\n" + + "\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"\xe6\x01\n" + - "\x13ListServicesRequest\x12\x18\n" + - "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1b\n" + + "\x13ListServicesRequest\x12\x1b\n" + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x1d\n" + "\n" + "page_token\x18\x03 \x01(\tR\tpageToken\x12R\n" + - "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\tworkspaceR\x0eall_workspaces\"\x81\x01\n" + + "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandboxJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\tworkspaceR\x0eall_workspaces\"\x81\x01\n" + "\x14ListServicesResponse\x12A\n" + "\bservices\x18\x01 \x03(\v2%.openshell.v1.ServiceEndpointResponseR\bservices\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xaf\x01\n" + "\x14DeleteServiceRequest\x12\x18\n" + - "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + - "\aservice\x18\x02 \x01(\tR\aservice\x12R\n" + + "\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"1\n" + "\x15DeleteServiceResponse\x12\x18\n" + "\adeleted\x18\x01 \x01(\bR\adeleted\"\xef\x01\n" + @@ -14611,10 +14595,8 @@ const file_openshell_proto_rawDesc = "" + "\x17RevokeSshSessionRequest\x12\x1a\n" + "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\"4\n" + "\x18RevokeSshSessionResponse\x12\x18\n" + - "\arevoked\x18\x01 \x01(\bR\arevoked\"\x9b\x03\n" + - "\x12ExecSandboxRequest\x12\x1d\n" + - "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x18\n" + + "\arevoked\x18\x01 \x01(\bR\arevoked\"\xfc\x03\n" + + "\x12ExecSandboxRequest\x12\x18\n" + "\acommand\x18\x02 \x03(\tR\acommand\x12\x18\n" + "\aworkdir\x18\x03 \x01(\tR\aworkdir\x12S\n" + "\venvironment\x18\x04 \x03(\v21.openshell.v1.ExecSandboxRequest.EnvironmentEntryR\venvironment\x12'\n" + @@ -14624,10 +14606,13 @@ const file_openshell_proto_rawDesc = "" + "\x04cols\x18\b \x01(\rR\x04cols\x12\x12\n" + "\x04rows\x18\t \x01(\rR\x04rows\x12$\n" + "\x0eno_login_shell\x18\n" + - " \x01(\bR\fnoLoginShell\x1a>\n" + + " \x01(\bR\fnoLoginShell\x12\x18\n" + + "\asandbox\x18\v \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\f \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"'\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02R\n" + + "sandbox_id\"'\n" + "\x11ExecSandboxStdout\x12\x12\n" + "\x04data\x18\x01 \x01(\fR\x04data\"'\n" + "\x11ExecSandboxStderr\x12\x12\n" + @@ -14638,16 +14623,17 @@ const file_openshell_proto_rawDesc = "" + "\x06stdout\x18\x01 \x01(\v2\x1f.openshell.v1.ExecSandboxStdoutH\x00R\x06stdout\x129\n" + "\x06stderr\x18\x02 \x01(\v2\x1f.openshell.v1.ExecSandboxStderrH\x00R\x06stderr\x123\n" + "\x04exit\x18\x03 \x01(\v2\x1d.openshell.v1.ExecSandboxExitH\x00R\x04exitB\t\n" + - "\apayload\"\xf3\x01\n" + - "\x0eTcpForwardInit\x12\x1d\n" + - "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1d\n" + + "\apayload\"\xd4\x02\n" + + "\x0eTcpForwardInit\x12\x18\n" + + "\asandbox\x18\x02 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1d\n" + "\n" + "service_id\x18\x04 \x01(\tR\tserviceId\x120\n" + "\x03ssh\x18\x05 \x01(\v2\x1c.openshell.v1.SshRelayTargetH\x00R\x03ssh\x120\n" + "\x03tcp\x18\x06 \x01(\v2\x1c.openshell.v1.TcpRelayTargetH\x00R\x03tcp\x125\n" + "\x13authorization_token\x18\a \x01(\tB\x04\x88\xb5\x18\x01R\x12authorizationTokenB\b\n" + - "\x06target\"f\n" + + "\x06targetJ\x04\b\x01\x10\x02R\n" + + "sandbox_id\"f\n" + "\x0fTcpForwardFrame\x122\n" + "\x04init\x18\x01 \x01(\v2\x1c.openshell.v1.TcpForwardInitH\x00R\x04init\x12\x14\n" + "\x04data\x18\x02 \x01(\fH\x00R\x04dataB\t\n" + @@ -14667,9 +14653,8 @@ const file_openshell_proto_rawDesc = "" + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12\x1a\n" + "\x05token\x18\x03 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12\"\n" + "\rexpires_at_ms\x18\x04 \x01(\x03R\vexpiresAtMs\x12\x18\n" + - "\arevoked\x18\x05 \x01(\bR\arevoked\"\xe6\x02\n" + - "\x13WatchSandboxRequest\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12#\n" + + "\arevoked\x18\x05 \x01(\bR\arevoked\"\xce\x03\n" + + "\x13WatchSandboxRequest\x12#\n" + "\rfollow_status\x18\x02 \x01(\bR\ffollowStatus\x12\x1f\n" + "\vfollow_logs\x18\x03 \x01(\bR\n" + "followLogs\x12#\n" + @@ -14683,7 +14668,9 @@ const file_openshell_proto_rawDesc = "" + "\vlog_sources\x18\t \x03(\tR\n" + "logSources\x12\"\n" + "\rlog_min_level\x18\n" + - " \x01(\tR\vlogMinLevel\"\xcc\x02\n" + + " \x01(\tR\vlogMinLevel\x12\x18\n" + + "\asandbox\x18\v \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\f \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x01\x10\x02R\x02id\"\xcc\x02\n" + "\x12SandboxStreamEvent\x121\n" + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxH\x00R\asandbox\x120\n" + "\x03log\x18\x02 \x01(\v2\x1c.openshell.v1.SandboxLogLineH\x00R\x03log\x123\n" + @@ -14953,9 +14940,8 @@ const file_openshell_proto_rawDesc = "" + "\n" + "expires_in\x18\x02 \x01(\x03R\texpiresIn\x12\x1d\n" + "\n" + - "token_type\x18\x03 \x01(\tR\ttokenType\"\x95\x05\n" + - "\x13UpdateConfigRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + + "token_type\x18\x03 \x01(\tR\ttokenType\"\xa1\x05\n" + + "\x13UpdateConfigRequest\x12;\n" + "\x06policy\x18\x02 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x1f\n" + "\vsetting_key\x18\x03 \x01(\tR\n" + "settingKey\x12G\n" + @@ -14964,12 +14950,13 @@ const file_openshell_proto_rawDesc = "" + "\x06global\x18\x06 \x01(\bR\x06global\x12M\n" + "\x10merge_operations\x18\a \x03(\v2\".openshell.v1.PolicyMergeOperationR\x0fmergeOperations\x12:\n" + "\x19expected_resource_version\x18\b \x01(\x04R\x17expectedResourceVersion\x12T\n" + - "\vannotations\x18\t \x03(\v22.openshell.v1.UpdateConfigRequest.AnnotationsEntryR\vannotations\x12R\n" + + "\vannotations\x18\t \x03(\v22.openshell.v1.UpdateConfigRequest.AnnotationsEntryR\vannotations\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + "\x0fworkspace_scope\x18\v \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\n" + - "\x10\vR\tworkspace\"\xc7\x03\n" + + "\x10\vR\x04nameR\tworkspace\"\xc7\x03\n" + "\x14PolicyMergeOperation\x129\n" + "\badd_rule\x18\x01 \x01(\v2\x1c.openshell.v1.AddNetworkRuleH\x00R\aaddRule\x12N\n" + "\x0fremove_endpoint\x18\x02 \x01(\v2#.openshell.v1.RemoveNetworkEndpointH\x00R\x0eremoveEndpoint\x12B\n" + @@ -15010,22 +14997,22 @@ const file_openshell_proto_rawDesc = "" + "\vannotations\x18\x05 \x03(\v23.openshell.v1.UpdateConfigResponse.AnnotationsEntryR\vannotations\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xca\x01\n" + - "\x1dGetSandboxPolicyStatusRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd6\x01\n" + + "\x1dGetSandboxPolicyStatusRequest\x12\x18\n" + "\aversion\x18\x02 \x01(\rR\aversion\x12\x16\n" + - "\x06global\x18\x03 \x01(\bR\x06global\x12R\n" + - "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\tworkspace\"\x88\x01\n" + + "\x06global\x18\x03 \x01(\bR\x06global\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\x04nameR\tworkspace\"\x88\x01\n" + "\x1eGetSandboxPolicyStatusResponse\x12?\n" + "\brevision\x18\x01 \x01(\v2#.openshell.v1.SandboxPolicyRevisionR\brevision\x12%\n" + - "\x0eactive_version\x18\x02 \x01(\rR\ractiveVersion\"\xe9\x01\n" + - "\x1aListSandboxPoliciesRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1b\n" + + "\x0eactive_version\x18\x02 \x01(\rR\ractiveVersion\"\xf5\x01\n" + + "\x1aListSandboxPoliciesRequest\x12\x1b\n" + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x1d\n" + "\n" + "page_token\x18\x03 \x01(\tR\tpageToken\x12\x16\n" + - "\x06global\x18\x04 \x01(\bR\x06global\x12R\n" + - "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x05\x10\x06R\tworkspace\"\x88\x01\n" + + "\x06global\x18\x04 \x01(\bR\x06global\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x05\x10\x06R\x04nameR\tworkspace\"\x88\x01\n" + "\x1bListSandboxPoliciesResponse\x12A\n" + "\trevisions\x18\x01 \x03(\v2#.openshell.v1.SandboxPolicyRevisionR\trevisions\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xa7\x01\n" + @@ -15053,15 +15040,15 @@ const file_openshell_proto_rawDesc = "" + "provenance\x1a=\n" + "\x0fProvenanceEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x83\x02\n" + - "\x15GetSandboxLogsRequest\x12\x1d\n" + - "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x90\x02\n" + + "\x15GetSandboxLogsRequest\x12\x14\n" + "\x05lines\x18\x02 \x01(\rR\x05lines\x12\x19\n" + "\bsince_ms\x18\x03 \x01(\x03R\asinceMs\x12\x18\n" + "\asources\x18\x04 \x03(\tR\asources\x12\x1b\n" + - "\tmin_level\x18\x05 \x01(\tR\bminLevel\x12R\n" + - "\x0fworkspace_scope\x18\a \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x06\x10\aR\tworkspace\"i\n" + + "\tmin_level\x18\x05 \x01(\tR\bminLevel\x12\x18\n" + + "\asandbox\x18\b \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\a \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x01\x10\x02J\x04\b\x06\x10\aR\n" + + "sandbox_idR\tworkspace\"i\n" + "\x16PushSandboxLogsRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x120\n" + @@ -15226,67 +15213,67 @@ const file_openshell_proto_rawDesc = "" + "\x0faccepted_chunks\x18\x01 \x01(\rR\x0eacceptedChunks\x12'\n" + "\x0frejected_chunks\x18\x02 \x01(\rR\x0erejectedChunks\x12+\n" + "\x11rejection_reasons\x18\x03 \x03(\tR\x10rejectionReasons\x12,\n" + - "\x12accepted_chunk_ids\x18\x04 \x03(\tR\x10acceptedChunkIds\"\xb5\x01\n" + - "\x15GetDraftPolicyRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + - "\rstatus_filter\x18\x02 \x01(\tR\fstatusFilter\x12R\n" + - "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"\xc8\x01\n" + + "\x12accepted_chunk_ids\x18\x04 \x03(\tR\x10acceptedChunkIds\"\xc1\x01\n" + + "\x15GetDraftPolicyRequest\x12#\n" + + "\rstatus_filter\x18\x02 \x01(\tR\fstatusFilter\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\x04nameR\tworkspace\"\xc8\x01\n" + "\x16GetDraftPolicyResponse\x121\n" + "\x06chunks\x18\x01 \x03(\v2\x19.openshell.v1.PolicyChunkR\x06chunks\x12'\n" + "\x0frolling_summary\x18\x02 \x01(\tR\x0erollingSummary\x12#\n" + "\rdraft_version\x18\x03 \x01(\x04R\fdraftVersion\x12-\n" + - "\x13last_analyzed_at_ms\x18\x04 \x01(\x03R\x10lastAnalyzedAtMs\"\xd1\x01\n" + - "\x18ApproveDraftChunkRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + + "\x13last_analyzed_at_ms\x18\x04 \x01(\x03R\x10lastAnalyzedAtMs\"\xdd\x01\n" + + "\x18ApproveDraftChunkRequest\x12\x19\n" + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12!\n" + - "\freview_token\x18\x04 \x01(\tR\vreviewToken\x12R\n" + - "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"c\n" + + "\freview_token\x18\x04 \x01(\tR\vreviewToken\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\x04nameR\tworkspace\"c\n" + "\x19ApproveDraftChunkResponse\x12%\n" + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + - "policyHash\"\xc5\x01\n" + - "\x17RejectDraftChunkRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + + "policyHash\"\xd1\x01\n" + + "\x17RejectDraftChunkRequest\x12\x19\n" + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x16\n" + - "\x06reason\x18\x03 \x01(\tR\x06reason\x12R\n" + - "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\tworkspace\"\x1a\n" + + "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\x04nameR\tworkspace\"\x1a\n" + "\x18RejectDraftChunkResponse\"R\n" + "\x12DraftChunkApproval\x12\x19\n" + "\bchunk_id\x18\x01 \x01(\tR\achunkId\x12!\n" + - "\freview_token\x18\x02 \x01(\tR\vreviewToken\"\x91\x02\n" + - "\x1cApproveAllDraftChunksRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x128\n" + + "\freview_token\x18\x02 \x01(\tR\vreviewToken\"\x9d\x02\n" + + "\x1cApproveAllDraftChunksRequest\x128\n" + "\x18include_security_flagged\x18\x02 \x01(\bR\x16includeSecurityFlagged\x12>\n" + - "\tapprovals\x18\x04 \x03(\v2 .openshell.v1.DraftChunkApprovalR\tapprovals\x12R\n" + - "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"\xb7\x01\n" + + "\tapprovals\x18\x04 \x03(\v2 .openshell.v1.DraftChunkApprovalR\tapprovals\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\x04nameR\tworkspace\"\xb7\x01\n" + "\x1dApproveAllDraftChunksResponse\x12%\n" + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + "policyHash\x12'\n" + "\x0fchunks_approved\x18\x03 \x01(\rR\x0echunksApproved\x12%\n" + - "\x0echunks_skipped\x18\x04 \x01(\rR\rchunksSkipped\"\xf9\x01\n" + - "\x15EditDraftChunkRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + + "\x0echunks_skipped\x18\x04 \x01(\rR\rchunksSkipped\"\x85\x02\n" + + "\x15EditDraftChunkRequest\x12\x19\n" + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12L\n" + - "\rproposed_rule\x18\x03 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\fproposedRule\x12R\n" + - "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\tworkspace\"\x18\n" + - "\x16EditDraftChunkResponse\"\xab\x01\n" + - "\x15UndoDraftChunkRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + - "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12R\n" + - "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"`\n" + + "\rproposed_rule\x18\x03 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\fproposedRule\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x04\x10\x05R\x04nameR\tworkspace\"\x18\n" + + "\x16EditDraftChunkResponse\"\xb7\x01\n" + + "\x15UndoDraftChunkRequest\x12\x19\n" + + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\x04nameR\tworkspace\"`\n" + "\x16UndoDraftChunkResponse\x12%\n" + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + - "policyHash\"\x92\x01\n" + - "\x17ClearDraftChunksRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12R\n" + - "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"A\n" + + "policyHash\"\x9e\x01\n" + + "\x17ClearDraftChunksRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\x04nameR\tworkspace\"A\n" + "\x18ClearDraftChunksResponse\x12%\n" + - "\x0echunks_cleared\x18\x01 \x01(\rR\rchunksCleared\"\x91\x01\n" + - "\x16GetDraftHistoryRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12R\n" + - "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\x92\x01\n" + + "\x0echunks_cleared\x18\x01 \x01(\rR\rchunksCleared\"\x9d\x01\n" + + "\x16GetDraftHistoryRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\x04nameR\tworkspace\"\x92\x01\n" + "\x11DraftHistoryEntry\x12!\n" + "\ftimestamp_ms\x18\x01 \x01(\x03R\vtimestampMs\x12\x1d\n" + "\n" + @@ -15885,302 +15872,306 @@ var file_openshell_proto_depIdxs = []int32{ 241, // 58: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider 24, // 59: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 24, // 60: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 240, // 61: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 62: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 63: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 72, // 64: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 240, // 65: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 236, // 66: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 71, // 67: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 223, // 68: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 76, // 69: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 77, // 70: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 78, // 71: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 167, // 72: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 168, // 73: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 80, // 74: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 75, // 75: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 83, // 76: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 236, // 77: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 24, // 78: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 87, // 79: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 38, // 80: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 88, // 81: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 178, // 82: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 224, // 83: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 241, // 84: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 240, // 85: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 86: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 87: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 241, // 88: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 225, // 89: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 240, // 90: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 91: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 241, // 92: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 241, // 93: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 117, // 94: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 100, // 95: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 1, // 96: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 101, // 97: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 106, // 98: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 102, // 99: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 2, // 100: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 104, // 101: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 105, // 102: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 2, // 103: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 7, // 104: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 240, // 105: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 107, // 106: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 107: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 226, // 108: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 240, // 109: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 107, // 110: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 240, // 111: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 107, // 112: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 240, // 113: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 3, // 114: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 103, // 115: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 242, // 116: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 243, // 117: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 108, // 118: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 227, // 119: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 117, // 120: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 117, // 121: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 98, // 122: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 99, // 123: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 117, // 124: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 98, // 125: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 99, // 126: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 117, // 127: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 98, // 128: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 99, // 129: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 130, // 130: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 228, // 131: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 229, // 132: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 230, // 133: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 231, // 134: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 237, // 135: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 244, // 136: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 136, // 137: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 232, // 138: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 240, // 139: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 137, // 140: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 138, // 141: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 139, // 142: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 140, // 143: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 141, // 144: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 142, // 145: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 245, // 146: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 246, // 147: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 247, // 148: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 233, // 149: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 240, // 150: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 150, // 151: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 240, // 152: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 150, // 153: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 4, // 154: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 4, // 155: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 237, // 156: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 234, // 157: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 240, // 158: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 87, // 159: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 87, // 160: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 157, // 161: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 160, // 162: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 171, // 163: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 172, // 164: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 158, // 165: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 159, // 166: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 161, // 167: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 166, // 168: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 172, // 169: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 167, // 170: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 168, // 171: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 169, // 172: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 173, // 173: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 175, // 174: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 245, // 175: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 237, // 176: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 237, // 177: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 174, // 178: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 177, // 179: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 176, // 180: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 240, // 181: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 177, // 182: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 240, // 183: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 184: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 187, // 185: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 240, // 186: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 187: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 240, // 188: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 189: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 190: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 240, // 191: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 197, // 192: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 235, // 193: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 248, // 194: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 248, // 195: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 248, // 196: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 236, // 197: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 198: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 6, // 199: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 207, // 200: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 207, // 201: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 103, // 202: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 131, // 203: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 12, // 204: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 14, // 205: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 16, // 206: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 39, // 207: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 47, // 208: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 49, // 209: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 50, // 210: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 40, // 211: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 41, // 212: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 42, // 213: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 43, // 214: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 51, // 215: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 52, // 216: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 53, // 217: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 54, // 218: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 55, // 219: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 56, // 220: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 63, // 221: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 65, // 222: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 66, // 223: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 67, // 224: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 69, // 225: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 73, // 226: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 75, // 227: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 81, // 228: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 82, // 229: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 89, // 230: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 90, // 231: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 91, // 232: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 96, // 233: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 97, // 234: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 120, // 235: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 122, // 236: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 124, // 237: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 92, // 238: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 109, // 239: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 111, // 240: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 113, // 241: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 115, // 242: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 93, // 243: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 127, // 244: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 249, // 245: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 250, // 246: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 135, // 247: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 144, // 248: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 146, // 249: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 148, // 250: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 129, // 251: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 133, // 252: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 151, // 253: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 152, // 254: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 155, // 255: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 162, // 256: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 164, // 257: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 170, // 258: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 85, // 259: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 179, // 260: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 181, // 261: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 183, // 262: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 185, // 263: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 188, // 264: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 190, // 265: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 192, // 266: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 194, // 267: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 196, // 268: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 8, // 269: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 10, // 270: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 199, // 271: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 201, // 272: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 203, // 273: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 205, // 274: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 208, // 275: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 210, // 276: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 212, // 277: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 13, // 278: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 15, // 279: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 17, // 280: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 57, // 281: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 48, // 282: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 57, // 283: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 58, // 284: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 44, // 285: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 44, // 286: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 45, // 287: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 46, // 288: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 59, // 289: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 60, // 290: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 61, // 291: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 62, // 292: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 57, // 293: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 57, // 294: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 64, // 295: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 72, // 296: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 72, // 297: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 68, // 298: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 70, // 299: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 74, // 300: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 79, // 301: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 81, // 302: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 79, // 303: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 94, // 304: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 94, // 305: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 95, // 306: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 119, // 307: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 118, // 308: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 121, // 309: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 123, // 310: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 125, // 311: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 94, // 312: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 110, // 313: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 112, // 314: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 114, // 315: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 116, // 316: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 126, // 317: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 128, // 318: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 251, // 319: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 252, // 320: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 143, // 321: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 145, // 322: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 147, // 323: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 149, // 324: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 132, // 325: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 134, // 326: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 154, // 327: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 153, // 328: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 156, // 329: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 163, // 330: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 165, // 331: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 170, // 332: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 86, // 333: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 180, // 334: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 182, // 335: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 184, // 336: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 186, // 337: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 189, // 338: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 191, // 339: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 193, // 340: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 195, // 341: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 198, // 342: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 9, // 343: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 11, // 344: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 200, // 345: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 202, // 346: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 204, // 347: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 206, // 348: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 209, // 349: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 211, // 350: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 213, // 351: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 278, // [278:352] is the sub-list for method output_type - 204, // [204:278] is the sub-list for method input_type - 204, // [204:204] is the sub-list for extension type_name - 204, // [204:204] is the sub-list for extension extendee - 0, // [0:204] is the sub-list for field type_name + 240, // 61: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 62: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 63: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 64: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 72, // 65: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 240, // 66: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 236, // 67: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 71, // 68: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 223, // 69: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 240, // 70: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 76, // 71: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 77, // 72: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 78, // 73: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 240, // 74: openshell.v1.TcpForwardInit.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 167, // 75: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 168, // 76: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 80, // 77: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 75, // 78: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 83, // 79: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 236, // 80: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 240, // 81: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 24, // 82: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 87, // 83: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 38, // 84: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 88, // 85: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 178, // 86: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 224, // 87: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 241, // 88: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 240, // 89: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 90: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 91: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 241, // 92: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 225, // 93: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 240, // 94: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 95: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 241, // 96: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 241, // 97: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 117, // 98: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 100, // 99: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 1, // 100: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 101, // 101: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 106, // 102: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 102, // 103: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 2, // 104: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 104, // 105: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 105, // 106: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 2, // 107: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 7, // 108: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 240, // 109: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 107, // 110: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 2, // 111: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 226, // 112: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 240, // 113: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 107, // 114: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 240, // 115: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 107, // 116: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 240, // 117: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 3, // 118: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 103, // 119: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 242, // 120: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 243, // 121: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 108, // 122: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 227, // 123: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 117, // 124: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 117, // 125: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 98, // 126: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 99, // 127: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 117, // 128: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 98, // 129: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 99, // 130: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 117, // 131: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 98, // 132: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 99, // 133: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 130, // 134: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 228, // 135: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 229, // 136: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 230, // 137: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 231, // 138: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 237, // 139: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 244, // 140: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 136, // 141: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 232, // 142: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 240, // 143: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 137, // 144: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 138, // 145: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 139, // 146: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 140, // 147: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 141, // 148: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 142, // 149: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 245, // 150: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 246, // 151: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 247, // 152: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 233, // 153: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 240, // 154: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 150, // 155: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 240, // 156: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 150, // 157: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 4, // 158: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 4, // 159: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 237, // 160: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 234, // 161: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 240, // 162: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 87, // 163: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 87, // 164: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 157, // 165: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 160, // 166: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 171, // 167: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 172, // 168: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 158, // 169: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 159, // 170: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 161, // 171: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 166, // 172: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 172, // 173: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 167, // 174: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 168, // 175: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 169, // 176: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 173, // 177: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 175, // 178: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 245, // 179: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 237, // 180: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 237, // 181: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 174, // 182: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 177, // 183: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 176, // 184: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 240, // 185: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 177, // 186: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 240, // 187: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 188: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 187, // 189: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 240, // 190: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 245, // 191: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 240, // 192: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 193: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 194: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 195: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 197, // 196: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 235, // 197: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 248, // 198: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 248, // 199: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 248, // 200: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 236, // 201: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 202: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 6, // 203: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 207, // 204: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 207, // 205: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 103, // 206: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 131, // 207: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 12, // 208: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 14, // 209: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 16, // 210: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 39, // 211: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 47, // 212: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 49, // 213: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 50, // 214: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 40, // 215: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 41, // 216: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 42, // 217: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 43, // 218: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 51, // 219: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 52, // 220: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 53, // 221: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 54, // 222: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 55, // 223: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 56, // 224: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 63, // 225: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 65, // 226: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 66, // 227: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 67, // 228: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 69, // 229: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 73, // 230: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 75, // 231: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 81, // 232: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 82, // 233: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 89, // 234: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 90, // 235: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 91, // 236: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 96, // 237: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 97, // 238: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 120, // 239: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 122, // 240: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 124, // 241: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 92, // 242: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 109, // 243: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 111, // 244: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 113, // 245: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 115, // 246: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 93, // 247: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 127, // 248: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 249, // 249: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 250, // 250: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 135, // 251: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 144, // 252: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 146, // 253: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 148, // 254: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 129, // 255: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 133, // 256: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 151, // 257: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 152, // 258: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 155, // 259: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 162, // 260: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 164, // 261: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 170, // 262: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 85, // 263: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 179, // 264: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 181, // 265: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 183, // 266: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 185, // 267: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 188, // 268: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 190, // 269: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 192, // 270: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 194, // 271: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 196, // 272: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 8, // 273: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 10, // 274: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 199, // 275: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 201, // 276: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 203, // 277: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 205, // 278: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 208, // 279: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 210, // 280: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 212, // 281: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 13, // 282: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 15, // 283: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 17, // 284: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 57, // 285: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 48, // 286: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 57, // 287: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 58, // 288: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 44, // 289: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 44, // 290: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 45, // 291: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 46, // 292: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 59, // 293: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 60, // 294: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 61, // 295: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 62, // 296: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 57, // 297: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 57, // 298: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 64, // 299: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 72, // 300: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 72, // 301: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 68, // 302: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 70, // 303: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 74, // 304: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 79, // 305: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 81, // 306: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 79, // 307: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 94, // 308: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 94, // 309: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 95, // 310: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 119, // 311: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 118, // 312: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 121, // 313: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 123, // 314: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 125, // 315: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 94, // 316: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 110, // 317: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 112, // 318: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 114, // 319: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 116, // 320: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 126, // 321: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 128, // 322: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 251, // 323: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 252, // 324: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 143, // 325: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 145, // 326: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 147, // 327: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 149, // 328: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 132, // 329: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 134, // 330: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 154, // 331: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 153, // 332: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 156, // 333: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 163, // 334: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 165, // 335: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 170, // 336: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 86, // 337: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 180, // 338: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 182, // 339: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 184, // 340: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 186, // 341: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 189, // 342: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 191, // 343: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 193, // 344: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 195, // 345: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 198, // 346: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 9, // 347: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 11, // 348: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 200, // 349: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 202, // 350: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 204, // 351: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 206, // 352: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 209, // 353: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 211, // 354: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 213, // 355: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 282, // [282:356] is the sub-list for method output_type + 208, // [208:282] is the sub-list for method input_type + 208, // [208:208] is the sub-list for extension type_name + 208, // [208:208] is the sub-list for extension extendee + 0, // [0:208] is the sub-list for field type_name } func init() { file_openshell_proto_init() } diff --git a/sdk/go/proto/sandboxv1/sandbox.pb.go b/sdk/go/proto/sandboxv1/sandbox.pb.go index 989589002b..7be246ac6d 100644 --- a/sdk/go/proto/sandboxv1/sandbox.pb.go +++ b/sdk/go/proto/sandboxv1/sandbox.pb.go @@ -10,6 +10,7 @@ package sandboxv1 import ( + datamodelv1 "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" @@ -1494,13 +1495,13 @@ func (x *NetworkBinary) GetHarness() bool { return false } -// Request to get sandbox settings by sandbox ID. +// Request to get sandbox settings by sandbox name. type GetSandboxConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The sandbox ID. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,2,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSandboxConfigRequest) Reset() { @@ -1533,13 +1534,20 @@ func (*GetSandboxConfigRequest) Descriptor() ([]byte, []int) { return file_sandbox_proto_rawDescGZIP(), []int{16} } -func (x *GetSandboxConfigRequest) GetSandboxId() string { +func (x *GetSandboxConfigRequest) GetSandbox() string { if x != nil { - return x.SandboxId + return x.Sandbox } return "" } +func (x *GetSandboxConfigRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + // Request to get gateway-global settings. type GetGatewayConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2069,7 +2077,7 @@ var File_sandbox_proto protoreflect.FileDescriptor const file_sandbox_proto_rawDesc = "" + "\n" + - "\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xa8\x05\n" + + "\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x0fdatamodel.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xa8\x05\n" + "\rSandboxPolicy\x12\x18\n" + "\aversion\x18\x01 \x01(\rR\aversion\x12F\n" + "\n" + @@ -2199,10 +2207,11 @@ const file_sandbox_proto_rawDesc = "" + "\x03any\x18\x02 \x03(\tR\x03any\"A\n" + "\rNetworkBinary\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x1c\n" + - "\aharness\x18\x02 \x01(\bB\x02\x18\x01R\aharness\"8\n" + - "\x17GetSandboxConfigRequest\x12\x1d\n" + - "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x19\n" + + "\aharness\x18\x02 \x01(\bB\x02\x18\x01R\aharness\"\x99\x01\n" + + "\x17GetSandboxConfigRequest\x12\x18\n" + + "\asandbox\x18\x02 \x01(\tR\asandbox\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x01\x10\x02R\n" + + "sandbox_id\"\x19\n" + "\x17GetGatewayConfigRequest\"\x82\x02\n" + "\x18GetGatewayConfigResponse\x12X\n" + "\bsettings\x18\x01 \x03(\v2<.openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntryR\bsettings\x12+\n" + @@ -2271,41 +2280,42 @@ func file_sandbox_proto_rawDescGZIP() []byte { var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_sandbox_proto_goTypes = []any{ - (SettingScope)(0), // 0: openshell.sandbox.v1.SettingScope - (PolicySource)(0), // 1: openshell.sandbox.v1.PolicySource - (*SandboxPolicy)(nil), // 2: openshell.sandbox.v1.SandboxPolicy - (*FilesystemPolicy)(nil), // 3: openshell.sandbox.v1.FilesystemPolicy - (*LandlockPolicy)(nil), // 4: openshell.sandbox.v1.LandlockPolicy - (*ProcessPolicy)(nil), // 5: openshell.sandbox.v1.ProcessPolicy - (*NetworkPolicyRule)(nil), // 6: openshell.sandbox.v1.NetworkPolicyRule - (*NetworkMiddlewareConfig)(nil), // 7: openshell.sandbox.v1.NetworkMiddlewareConfig - (*MiddlewareEndpointSelector)(nil), // 8: openshell.sandbox.v1.MiddlewareEndpointSelector - (*NetworkCredentialBinding)(nil), // 9: openshell.sandbox.v1.NetworkCredentialBinding - (*NetworkEndpoint)(nil), // 10: openshell.sandbox.v1.NetworkEndpoint - (*McpOptions)(nil), // 11: openshell.sandbox.v1.McpOptions - (*GraphqlOperation)(nil), // 12: openshell.sandbox.v1.GraphqlOperation - (*L7DenyRule)(nil), // 13: openshell.sandbox.v1.L7DenyRule - (*L7Rule)(nil), // 14: openshell.sandbox.v1.L7Rule - (*L7Allow)(nil), // 15: openshell.sandbox.v1.L7Allow - (*L7QueryMatcher)(nil), // 16: openshell.sandbox.v1.L7QueryMatcher - (*NetworkBinary)(nil), // 17: openshell.sandbox.v1.NetworkBinary - (*GetSandboxConfigRequest)(nil), // 18: openshell.sandbox.v1.GetSandboxConfigRequest - (*GetGatewayConfigRequest)(nil), // 19: openshell.sandbox.v1.GetGatewayConfigRequest - (*GetGatewayConfigResponse)(nil), // 20: openshell.sandbox.v1.GetGatewayConfigResponse - (*SettingValue)(nil), // 21: openshell.sandbox.v1.SettingValue - (*EffectiveSetting)(nil), // 22: openshell.sandbox.v1.EffectiveSetting - (*GetSandboxConfigResponse)(nil), // 23: openshell.sandbox.v1.GetSandboxConfigResponse - (*SupervisorMiddlewareService)(nil), // 24: openshell.sandbox.v1.SupervisorMiddlewareService - nil, // 25: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry - nil, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry - nil, // 27: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry - nil, // 28: openshell.sandbox.v1.L7DenyRule.QueryEntry - nil, // 29: openshell.sandbox.v1.L7DenyRule.ParamsEntry - nil, // 30: openshell.sandbox.v1.L7Allow.QueryEntry - nil, // 31: openshell.sandbox.v1.L7Allow.ParamsEntry - nil, // 32: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - nil, // 33: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry - (*structpb.Struct)(nil), // 34: google.protobuf.Struct + (SettingScope)(0), // 0: openshell.sandbox.v1.SettingScope + (PolicySource)(0), // 1: openshell.sandbox.v1.PolicySource + (*SandboxPolicy)(nil), // 2: openshell.sandbox.v1.SandboxPolicy + (*FilesystemPolicy)(nil), // 3: openshell.sandbox.v1.FilesystemPolicy + (*LandlockPolicy)(nil), // 4: openshell.sandbox.v1.LandlockPolicy + (*ProcessPolicy)(nil), // 5: openshell.sandbox.v1.ProcessPolicy + (*NetworkPolicyRule)(nil), // 6: openshell.sandbox.v1.NetworkPolicyRule + (*NetworkMiddlewareConfig)(nil), // 7: openshell.sandbox.v1.NetworkMiddlewareConfig + (*MiddlewareEndpointSelector)(nil), // 8: openshell.sandbox.v1.MiddlewareEndpointSelector + (*NetworkCredentialBinding)(nil), // 9: openshell.sandbox.v1.NetworkCredentialBinding + (*NetworkEndpoint)(nil), // 10: openshell.sandbox.v1.NetworkEndpoint + (*McpOptions)(nil), // 11: openshell.sandbox.v1.McpOptions + (*GraphqlOperation)(nil), // 12: openshell.sandbox.v1.GraphqlOperation + (*L7DenyRule)(nil), // 13: openshell.sandbox.v1.L7DenyRule + (*L7Rule)(nil), // 14: openshell.sandbox.v1.L7Rule + (*L7Allow)(nil), // 15: openshell.sandbox.v1.L7Allow + (*L7QueryMatcher)(nil), // 16: openshell.sandbox.v1.L7QueryMatcher + (*NetworkBinary)(nil), // 17: openshell.sandbox.v1.NetworkBinary + (*GetSandboxConfigRequest)(nil), // 18: openshell.sandbox.v1.GetSandboxConfigRequest + (*GetGatewayConfigRequest)(nil), // 19: openshell.sandbox.v1.GetGatewayConfigRequest + (*GetGatewayConfigResponse)(nil), // 20: openshell.sandbox.v1.GetGatewayConfigResponse + (*SettingValue)(nil), // 21: openshell.sandbox.v1.SettingValue + (*EffectiveSetting)(nil), // 22: openshell.sandbox.v1.EffectiveSetting + (*GetSandboxConfigResponse)(nil), // 23: openshell.sandbox.v1.GetSandboxConfigResponse + (*SupervisorMiddlewareService)(nil), // 24: openshell.sandbox.v1.SupervisorMiddlewareService + nil, // 25: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + nil, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + nil, // 27: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + nil, // 28: openshell.sandbox.v1.L7DenyRule.QueryEntry + nil, // 29: openshell.sandbox.v1.L7DenyRule.ParamsEntry + nil, // 30: openshell.sandbox.v1.L7Allow.QueryEntry + nil, // 31: openshell.sandbox.v1.L7Allow.ParamsEntry + nil, // 32: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + nil, // 33: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + (*structpb.Struct)(nil), // 34: google.protobuf.Struct + (*datamodelv1.WorkspaceSelector)(nil), // 35: openshell.datamodel.v1.WorkspaceSelector } var file_sandbox_proto_depIdxs = []int32{ 3, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy @@ -2327,27 +2337,28 @@ var file_sandbox_proto_depIdxs = []int32{ 15, // 16: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow 30, // 17: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry 31, // 18: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry - 32, // 19: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - 21, // 20: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue - 0, // 21: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope - 2, // 22: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 33, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry - 1, // 24: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 24, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService - 6, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 7, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig - 12, // 28: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation - 16, // 29: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 30: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 31: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 32: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 21, // 33: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue - 22, // 34: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting - 35, // [35:35] is the sub-list for method output_type - 35, // [35:35] is the sub-list for method input_type - 35, // [35:35] is the sub-list for extension type_name - 35, // [35:35] is the sub-list for extension extendee - 0, // [0:35] is the sub-list for field type_name + 35, // 19: openshell.sandbox.v1.GetSandboxConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 32, // 20: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + 21, // 21: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue + 0, // 22: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope + 2, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 33, // 24: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + 1, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 24, // 26: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService + 6, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 7, // 28: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig + 12, // 29: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation + 16, // 30: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 16, // 31: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 16, // 32: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 16, // 33: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 21, // 34: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue + 22, // 35: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting + 36, // [36:36] is the sub-list for method output_type + 36, // [36:36] is the sub-list for method input_type + 36, // [36:36] is the sub-list for extension type_name + 36, // [36:36] is the sub-list for extension extendee + 0, // [0:36] is the sub-list for field type_name } func init() { file_sandbox_proto_init() } diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index d440c75108..da6282022f 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -58,6 +58,7 @@ function readySandbox( const enc = (s: string) => new TextEncoder().encode(s); type ScopedRequest = { + sandbox?: string; workspaceScope?: { selection?: { case?: string; value?: unknown } }; }; @@ -66,13 +67,17 @@ function selectedWorkspace(req: ScopedRequest): string | undefined { return selection?.case === 'workspace' && typeof selection.value === 'string' ? selection.value : undefined; } +function requestSandbox(req: ScopedRequest): string | undefined { + return req.sandbox; +} + function selectsAllWorkspaces(req: ScopedRequest): boolean { return req.workspaceScope?.selection?.case === 'allWorkspaces'; } describe('exec / execStream', () => { it('resolves the id via get, frames tty:false, and buffers the result (backward compat)', async () => { - let execReq: { sandboxId?: string; tty?: boolean; command?: string[] } = {}; + let execReq: ScopedRequest & { tty?: boolean; command?: string[] } = {}; const sandbox = client({ getSandbox: () => readySandbox('sb', 'sb-id-1'), // eslint-disable-next-line require-yield @@ -86,7 +91,7 @@ describe('exec / execStream', () => { }); const result = await sandbox.exec('sb', ['/bin/sh', '-c', 'echo hi']); - expect(execReq.sandboxId).toBe('sb-id-1'); + expect(requestSandbox(execReq)).toBe('sb'); expect(execReq.tty).toBe(false); expect(execReq.command).toEqual(['/bin/sh', '-c', 'echo hi']); expect(result.exitCode).toBe(3); @@ -353,13 +358,14 @@ describe('create', () => { }, getSandbox: (req) => { const workspace = selectedWorkspace(req); - if (req.name === 'exec') observed.execGet = workspace; - else if (req.name === 'interactive') observed.interactiveGet = workspace; - else if (req.name === 'ssh') observed.sshGet = workspace; - else if (req.name === 'forward') observed.forwardGet = workspace; - else if (req.name === 'config' && workspace) observed.configGets.push(workspace); + const name = requestSandbox(req); + if (name === 'exec') observed.execGet = workspace; + else if (name === 'interactive') observed.interactiveGet = workspace; + else if (name === 'ssh') observed.sshGet = workspace; + else if (name === 'forward') observed.forwardGet = workspace; + else if (name === 'config' && workspace) observed.configGets.push(workspace); else observed.get = req; - return readySandbox(req.name, `${req.name}-id`, 7n, undefined, workspace ?? 'default'); + return readySandbox(name ?? '', `${name}-id`, 7n, undefined, workspace ?? 'default'); }, listSandboxes: (req) => { observed.list = req; @@ -385,16 +391,26 @@ describe('create', () => { attachSandboxProvider: (req) => { observed.attach = req; return { - sandbox: readySandbox(req.sandboxName, 'attach-id', 7n, undefined, selectedWorkspace(req) ?? 'default') - .sandbox, + sandbox: readySandbox( + requestSandbox(req) ?? '', + 'attach-id', + 7n, + undefined, + selectedWorkspace(req) ?? 'default', + ).sandbox, attached: true, }; }, detachSandboxProvider: (req) => { observed.detach = req; return { - sandbox: readySandbox(req.sandboxName, 'detach-id', 7n, undefined, selectedWorkspace(req) ?? 'default') - .sandbox, + sandbox: readySandbox( + requestSandbox(req) ?? '', + 'detach-id', + 7n, + undefined, + selectedWorkspace(req) ?? 'default', + ).sandbox, detached: true, }; }, @@ -426,7 +442,7 @@ describe('create', () => { yield { payload: { case: 'exit', value: { exitCode: 0 } } }; }, createSshSession: (req) => ({ - sandboxId: req.sandboxId, + sandboxId: `${requestSandbox(req) ?? ''}-id`, token: 'tok', gatewayHost: 'gw', gatewayPort: 443, @@ -802,7 +818,7 @@ describe('Pushable', () => { describe('execInteractive', () => { it('sends start first with tty/cols/rows, streams output, and resolves done', async () => { const cases: string[] = []; - let started: { tty?: boolean; cols?: number; rows?: number; sandboxId?: string } | undefined; + let started: (ScopedRequest & { tty?: boolean; cols?: number; rows?: number }) | undefined; const sandbox = client({ getSandbox: () => readySandbox('sb', 'sb-id-9'), execSandboxInteractive: async function* (requests) { @@ -846,7 +862,7 @@ describe('execInteractive', () => { expect(started?.tty).toBe(true); expect(started?.cols).toBe(120); expect(started?.rows).toBe(40); - expect(started?.sandboxId).toBe('sb-id-9'); + expect(requestSandbox(started ?? {})).toBe('sb'); expect(out.join('')).toContain('ready\n'); expect(out.join('')).toContain('echo hi'); }); @@ -910,7 +926,8 @@ describe('exec done settlement', () => { describe('providers', () => { it('attach/detach assemble the request and map the changed flag + sandbox ref', async () => { let attachReq: { - sandboxName?: string; + sandbox?: string; + workspaceScope?: ScopedRequest['workspaceScope']; providerName?: string; expectedResourceVersion?: bigint; } = {}; @@ -930,7 +947,7 @@ describe('providers', () => { }); const attach = await sandbox.attachProvider('sb', 'claude'); - expect(attachReq.sandboxName).toBe('sb'); + expect(requestSandbox(attachReq)).toBe('sb'); expect(attachReq.providerName).toBe('claude'); expect(attachReq.expectedResourceVersion).toBe(0n); expect(attach.changed).toBe(true); @@ -1007,7 +1024,8 @@ describe('config / policy', () => { it('setPolicy sends global=false + version pin and (wait) polls until the hash matches', async () => { let updateReq: { - name?: string; + sandbox?: string; + workspaceScope?: ScopedRequest['workspaceScope']; global?: boolean; expectedResourceVersion?: bigint; policy?: unknown; @@ -1048,7 +1066,7 @@ describe('config / policy', () => { }, { wait: true, expectedResourceVersion: '7' }, ); - expect(updateReq.name).toBe('sb'); + expect(requestSandbox(updateReq)).toBe('sb'); expect(updateReq.global).toBe(false); expect(updateReq.expectedResourceVersion).toBe(7n); expect(updateReq.policy).toBeDefined(); @@ -1081,7 +1099,8 @@ describe('config / policy', () => { it('setSetting upserts a single sandbox-scoped setting (global=false)', async () => { let req: { - name?: string; + sandbox?: string; + workspaceScope?: ScopedRequest['workspaceScope']; settingKey?: string; global?: boolean; settingValue?: unknown; @@ -1100,7 +1119,7 @@ describe('config / policy', () => { const result = await sandbox.setSetting('sb', 'feature.enabled', { value: { case: 'boolValue', value: true }, }); - expect(req.name).toBe('sb'); + expect(requestSandbox(req)).toBe('sb'); expect(req.settingKey).toBe('feature.enabled'); expect(req.global).toBe(false); expect(req.settingValue).toMatchObject({ @@ -1217,9 +1236,9 @@ describe('ssh sessions', () => { describe('forward', () => { it('binds a local port and relays bytes both ways, minting + revoking a token', async () => { - let sshReq: { sandboxId?: string } = {}; + let sshReq: ScopedRequest = {}; let revokedToken: string | undefined; - let initFrame: { sandboxId?: string; authorizationToken?: string; target?: unknown } | undefined; + let initFrame: (ScopedRequest & { authorizationToken?: string; target?: unknown }) | undefined; const sandbox = client({ getSandbox: () => readySandbox('sb', 'sb-id-forward'), createSshSession: (req) => { @@ -1270,8 +1289,8 @@ describe('forward', () => { }); expect(echoed).toBe('ping-through-forward'); - expect(sshReq.sandboxId).toBe('sb-id-forward'); - expect(initFrame?.sandboxId).toBe('sb-id-forward'); + expect(requestSandbox(sshReq)).toBe('sb'); + expect(requestSandbox(initFrame ?? {})).toBe('sb'); expect(initFrame?.authorizationToken).toBe('fwd-tok'); expect(initFrame?.target).toMatchObject({ case: 'tcp', @@ -1515,7 +1534,10 @@ describe('raw escape hatch', () => { // raw returns the full generated message: the enum stays numeric, where the // curated get() would lowercase status.phase to 'ready'. - const resp = await sandbox.raw.getSandbox({ name: 'sb' }); + const resp = await sandbox.raw.getSandbox({ + sandbox: 'sb', + workspaceScope: { selection: { case: 'workspace', value: 'default' } }, + }); expect(resp.sandbox?.status?.phase).toBe(SandboxPhase.READY); expect(resp.sandbox?.metadata?.name).toBe('sb'); diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index 37f145c4c3..77373103ef 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -510,6 +510,10 @@ function listWorkspaceScope(options?: WorkspaceListScope | null): MessageInitSha return options?.allWorkspaces ? { selection: { case: 'allWorkspaces', value: {} } } : workspaceScope(options); } +function sandboxTarget(name: string, options?: SandboxWorkspaceOptions | null) { + return { sandbox: name, workspaceScope: workspaceScope(options) }; +} + function requestCallOptions(options?: SandboxCallOptions | null): CallOptions | undefined { if (!options) return undefined; const { workspace: _workspace, ...callOptions } = options; @@ -851,10 +855,7 @@ export class SandboxClient { async get(name: string, options?: SandboxCallOptions | null): Promise { try { - const resp = await this.grpc.getSandbox( - { name, workspaceScope: workspaceScope(options) }, - requestCallOptions(options), - ); + const resp = await this.grpc.getSandbox({ ...sandboxTarget(name, options) }, requestCallOptions(options)); return sandboxRef(resp.sandbox); } catch (e) { throw fromConnect(e); @@ -887,7 +888,7 @@ export class SandboxClient { async delete(name: string, options?: SandboxWorkspaceOptions | null): Promise { try { - const resp = await this.grpc.deleteSandbox({ name, workspaceScope: workspaceScope(options) }); + const resp = await this.grpc.deleteSandbox({ ...sandboxTarget(name, options) }); return resp.deleted; } catch (e) { throw fromConnect(e); @@ -956,14 +957,14 @@ export class SandboxClient { options?: ExecOptions | null, ): AsyncGenerator { try { - // Resolve the sandbox id first, exactly like the gateway client. - const sandbox = await this.get(name, { + // Preserve the existing preflight so lookup failures surface before the stream starts. + await this.get(name, { workspace: options?.workspace, ...(options?.signal ? { signal: options.signal } : {}), }); const stream = this.grpc.execSandbox( { - sandboxId: sandbox.id, + ...sandboxTarget(name, options), command, workdir: options?.workdir ?? '', environment: options?.environment ?? {}, @@ -1032,11 +1033,8 @@ export class SandboxClient { command: string[], options?: ExecInteractiveOptions | null, ): Promise { - let sandboxId: string; try { - sandboxId = ( - await this.get(name, { workspace: options?.workspace, ...(options?.signal ? { signal: options.signal } : {}) }) - ).id; + await this.get(name, { workspace: options?.workspace, ...(options?.signal ? { signal: options.signal } : {}) }); } catch (e) { throw e instanceof SdkError ? e : fromConnect(e); } @@ -1046,7 +1044,7 @@ export class SandboxClient { payload: { case: 'start', value: { - sandboxId, + ...sandboxTarget(name, options), command, workdir: options?.workdir ?? '', environment: options?.environment ?? {}, @@ -1178,7 +1176,15 @@ export class SandboxClient { socket.on('error', () => {}); const controller = new AbortController(); controllers.add(controller); - const task = this.forwardConnection(socket, sandboxId, name, targetHost, targetPort, controller.signal) + const task = this.forwardConnection( + socket, + sandboxId, + name, + opts.workspace, + targetHost, + targetPort, + controller.signal, + ) .catch((error: unknown) => { if (!closing) { try { @@ -1258,6 +1264,7 @@ export class SandboxClient { socket: net.Socket, sandboxId: string, name: string, + workspace: string | undefined, targetHost: string, targetPort: number, signal: AbortSignal, @@ -1266,7 +1273,7 @@ export class SandboxClient { const input = new Pushable>(); input.onDrain = () => socket.resume(); try { - const session = await this.grpc.createSshSession({ sandboxId }, { signal }); + const session = await this.grpc.createSshSession({ ...sandboxTarget(name, { workspace }) }, { signal }); // Defense-in-depth: the token feeds forwardTcp authorization, so hold it // to the same trust-boundary contract as createSshSession. A violation // tears down this one socket via the catch below. @@ -1276,7 +1283,7 @@ export class SandboxClient { payload: { case: 'init', value: { - sandboxId, + ...sandboxTarget(name, { workspace }), serviceId: `service-forward:${name}:${targetHost}:${targetPort}`, target: { case: 'tcp', @@ -1339,7 +1346,7 @@ export class SandboxClient { async createSshSession(name: string, options?: SandboxWorkspaceOptions | null): Promise { try { const sandbox = await this.get(name, options); - const resp = await this.grpc.createSshSession({ sandboxId: sandbox.id }); + const resp = await this.grpc.createSshSession({ ...sandboxTarget(name, options) }); // Reject any response outside the proto trust-boundary contract before // handing these values to the caller (they feed OpenSSH ProxyCommand). validateSshResponse(resp, sandbox.id); @@ -1373,10 +1380,9 @@ export class SandboxClient { ): Promise { try { const resp = await this.grpc.attachSandboxProvider({ - sandboxName: name, + ...sandboxTarget(name, options), providerName: provider, expectedResourceVersion: versionPin(options?.expectedResourceVersion), - workspaceScope: workspaceScope(options), }); return { sandbox: sandboxRef(resp.sandbox), changed: resp.attached }; } catch (e) { @@ -1391,10 +1397,9 @@ export class SandboxClient { ): Promise { try { const resp = await this.grpc.detachSandboxProvider({ - sandboxName: name, + ...sandboxTarget(name, options), providerName: provider, expectedResourceVersion: versionPin(options?.expectedResourceVersion), - workspaceScope: workspaceScope(options), }); return { sandbox: sandboxRef(resp.sandbox), changed: resp.detached }; } catch (e) { @@ -1405,8 +1410,7 @@ export class SandboxClient { async listProviders(name: string, options?: SandboxWorkspaceOptions | null): Promise { try { const resp = await this.grpc.listSandboxProviders({ - sandboxName: name, - workspaceScope: workspaceScope(options), + ...sandboxTarget(name, options), }); return resp.providers.map((p) => providerRef(p)); } catch (e) { @@ -1416,8 +1420,8 @@ export class SandboxClient { async getConfig(name: string, options?: SandboxCallOptions | null): Promise { try { - const sandbox = await this.get(name, options); - const resp = await this.grpc.getSandboxConfig({ sandboxId: sandbox.id }, requestCallOptions(options)); + await this.get(name, options); + const resp = await this.grpc.getSandboxConfig({ ...sandboxTarget(name, options) }, requestCallOptions(options)); return sandboxConfig(resp); } catch (e) { throw e instanceof SdkError ? e : fromConnect(e); @@ -1435,11 +1439,10 @@ export class SandboxClient { ): Promise { try { const resp = await this.grpc.updateConfig({ - name, + ...sandboxTarget(name, options), policy, global: false, expectedResourceVersion: versionPin(options?.expectedResourceVersion), - workspaceScope: workspaceScope(options), }); const result = updateConfigResult(resp); if (options?.wait) @@ -1460,11 +1463,10 @@ export class SandboxClient { ): Promise { try { const resp = await this.grpc.updateConfig({ - name, + ...sandboxTarget(name, options), settingKey: key, settingValue: value, global: false, - workspaceScope: workspaceScope(options), }); return updateConfigResult(resp); } catch (e) { diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index d986a45cc1..2aa50f88c5 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -798,6 +798,9 @@ openshell service get my-app web openshell service delete my-app web ``` +Use `openshell service list --all-workspaces` for a Platform Admin view across +workspaces. A sandbox name and `--all-workspaces` are mutually exclusive. + Prefer loopback binds unless the user explicitly needs LAN-visible local access. --- diff --git a/tasks/scripts/generate_python_proto.py b/tasks/scripts/generate_python_proto.py index 76664eb415..cefd2c375b 100644 --- a/tasks/scripts/generate_python_proto.py +++ b/tasks/scripts/generate_python_proto.py @@ -56,6 +56,12 @@ "from . import datamodel_pb2 as datamodel__pb2", ), ], + "python/openshell/_proto/sandbox_pb2.py": [ + ( + r"^import datamodel_pb2 as datamodel__pb2$", + "from . import datamodel_pb2 as datamodel__pb2", + ), + ], "python/openshell/_proto/sandbox_pb2_grpc.py": [ ( r"^import sandbox_pb2 as sandbox__pb2$",