diff --git a/.agents/skills/tui-development/SKILL.md b/.agents/skills/tui-development/SKILL.md index 5abc534f77..edd1397d56 100644 --- a/.agents/skills/tui-development/SKILL.md +++ b/.agents/skills/tui-development/SKILL.md @@ -469,14 +469,20 @@ Proto types come from `openshell-core` which generates them from `OUT_DIR` via ` ```rust use openshell_core::proto::openshell_client::OpenShellClient; -use openshell_core::proto::{ListSandboxesRequest, GetSandboxLogsRequest, ...}; +use openshell_core::proto::{ + all_workspaces_selector, workspace_selector, GetSandboxLogsRequest, + ListSandboxesRequest, ... +}; ``` ### Proto field gotchas - `DeleteSandboxRequest` uses the `name` field (not `id`): ```rust - let req = openshell_core::proto::DeleteSandboxRequest { name: sandbox_name }; + let req = openshell_core::proto::DeleteSandboxRequest { + name: sandbox_name, + workspace_scope: Some(workspace_selector(workspace)), + }; ``` - `WatchSandboxRequest` has extra fields beyond what you might need — always use `..Default::default()`: ```rust @@ -490,12 +496,24 @@ use openshell_core::proto::{ListSandboxesRequest, GetSandboxLogsRequest, ...}; }; ``` - `SandboxLogLine` proto fields: `sandbox_id`, `timestamp_ms`, `level`, `target`, `message`, `source`, `fields` (HashMap). -- `GetSandboxLogsRequest` fields: `sandbox_id`, `lines` (u32), `since_ms` (i64), `sources` (Vec), `min_level` (String), `workspace` (String). -- `ListSandboxesRequest` fields: `limit` (i64), `offset` (i64), `label_selector` (String), `workspace` (String), `all_workspaces` (bool). -- `ListProvidersRequest` fields: `limit` (i64), `offset` (i64), `workspace` (String), `all_workspaces` (bool). -- `ListWorkspacesRequest` fields: `limit` (i64), `offset` (i64), `label_selector` (String). -- `UpdateConfigRequest` fields: `name` (String, sandbox name or empty for global), `setting_key`, `setting_value`, `delete_setting` (bool), `global` (bool), `workspace`. -- Most resource requests include a `workspace` field that scopes the operation to the current workspace. +- Workspace-scoped request fields use `workspace_scope: Option`. + Select one workspace with `Some(workspace_selector(name))`. List requests that + explicitly support cross-workspace access also accept + `Some(all_workspaces_selector())`; do not use that marker on other requests. +- `GetSandboxLogsRequest` fields: `sandbox_id`, `lines` (u32), `since_ms` (i64), + `sources` (Vec), `min_level` (String), `workspace_scope`. +- `ListSandboxesRequest` fields: `limit` (u32), `offset` (u32), + `label_selector` (String), `workspace_scope`. +- `ListProvidersRequest` fields: `limit` (u32), `offset` (u32), + `workspace_scope`. +- `ListWorkspacesRequest` fields: `limit` (u32), `offset` (u32), + `label_selector` (String). +- `UpdateConfigRequest` fields include `name` (String, sandbox name or empty for + global), `setting_key`, `setting_value`, `delete_setting` (bool), `global` + (bool), and `workspace_scope`. Sandbox-scoped updates require a named selector; + gateway-global updates must leave `workspace_scope` as `None`. +- Most resource requests require an explicit named `workspace_scope`, including + the `default` workspace. An omitted selector is not an implicit default. ### gRPC timeouts diff --git a/architecture/gateway.md b/architecture/gateway.md index 2fbd46533e..59b2bd52ed 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -54,6 +54,16 @@ health, metrics, or tunnel routes. The plaintext service router also rejects browser requests whose Fetch Metadata, Origin, or Referer headers indicate a 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. + Docker and Podman report the local address through which their sandboxes can reach the gateway. When the primary listener covers that address, the gateway reuses it; sandbox JWT authentication and its RPC allowlist remain the callback @@ -318,8 +328,8 @@ public descriptor set generated by `openshell-core`; a fingerprint test in 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, 276 messages, and 12 enums -(`042034fe4d0000279ee4ed27e587ab8e530934b8d5c3aa36dc9769f81dfa6e51`). +methods, 278 messages, and 12 enums +(`c95ae90962c10fb28747db2b645adf4044562a3d208d84dfe4699d677e4364ee`). 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 c903156af0..e966eb690e 100644 --- a/crates/openshell-cli/src/commands/provider.rs +++ b/crates/openshell-cli/src/commands/provider.rs @@ -59,7 +59,7 @@ pub async fn sandbox_provider_list( let response = client .list_sandbox_providers(ListSandboxProvidersRequest { sandbox_name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -91,7 +91,7 @@ pub async fn sandbox_provider_attach( let sandbox = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -106,7 +106,7 @@ pub async fn sandbox_provider_attach( sandbox_name: name.to_string(), provider_name: provider.to_string(), expected_resource_version: resource_version, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -147,7 +147,7 @@ pub async fn sandbox_provider_detach( let sandbox = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -162,7 +162,7 @@ pub async fn sandbox_provider_detach( sandbox_name: name.to_string(), provider_name: provider.to_string(), expected_resource_version: resource_version, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -305,8 +305,7 @@ pub async fn ensure_required_providers( .list_providers(ListProvidersRequest { limit, offset, - workspace: workspace.to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -490,7 +489,7 @@ async fn auto_create_provider( profile_workspace: workspace.to_string(), credential_handles: HashMap::new(), }), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }; let response = client.create_provider(request).await.map_err(|status| { @@ -538,7 +537,7 @@ async fn auto_create_provider( profile_workspace: workspace.to_string(), credential_handles: HashMap::new(), }), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }; match client.create_provider(request).await { @@ -672,7 +671,7 @@ async fn rollback_provider_create_after_gcloud_adc_failure( match client .delete_provider(DeleteProviderRequest { name: provider_name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -1119,7 +1118,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) -> profile_workspace: profile_workspace.to_string(), credential_handles: HashMap::new(), }), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -1149,7 +1148,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) -> "refresh_token".to_string(), ], expires_at_ms: None, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -1167,7 +1166,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) -> .rotate_provider_credential(RotateProviderCredentialRequest { provider: provider_name.clone(), credential_key: adc_credential_key, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -1208,7 +1207,7 @@ pub async fn provider_get( let response = client .get_provider(GetProviderRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -1339,12 +1338,11 @@ pub async fn provider_list( .list_providers(ListProvidersRequest { limit, offset, - workspace: if all_workspaces { - String::new() + workspace_scope: Some(if all_workspaces { + openshell_core::proto::all_workspaces_selector() } else { - workspace.to_string() - }, - all_workspaces, + openshell_core::proto::workspace_selector(workspace) + }), }) .await .into_diagnostic()?; @@ -1713,7 +1711,7 @@ pub async fn provider_refresh_status( .get_provider_refresh_status(GetProviderRefreshStatusRequest { provider: name.to_string(), credential_key: credential_key.unwrap_or_default().to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -1794,7 +1792,7 @@ pub async fn provider_refresh_config( material, secret_material_keys, expires_at_ms: input.credential_expires_at_ms, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -1823,7 +1821,7 @@ pub async fn provider_rotate( .rotate_provider_credential(RotateProviderCredentialRequest { provider: name.to_string(), credential_key: credential_key.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -1860,7 +1858,7 @@ pub async fn provider_refresh_delete( .delete_provider_refresh(DeleteProviderRefreshRequest { provider: name.to_string(), credential_key: credential_key.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -2236,7 +2234,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { let existing = match client .get_provider(GetProviderRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -2319,7 +2317,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { credential_handles: HashMap::new(), }), credential_expires_at_ms, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -2349,7 +2347,7 @@ pub async fn provider_delete( let response = match client .delete_provider(DeleteProviderRequest { name: name.clone(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { diff --git a/crates/openshell-cli/src/completers.rs b/crates/openshell-cli/src/completers.rs index 9d3b88d594..0778babbf1 100644 --- a/crates/openshell-cli/src/completers.rs +++ b/crates/openshell-cli/src/completers.rs @@ -39,8 +39,9 @@ pub fn complete_sandbox_names(_prefix: &OsStr) -> Vec { limit: 200, offset: 0, label_selector: String::new(), - workspace: workspace_from_args(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + workspace_from_args(), + )), }) .await .ok()?; @@ -64,8 +65,9 @@ pub fn complete_provider_names(_prefix: &OsStr) -> Vec { .list_providers(ListProvidersRequest { limit: 200, offset: 0, - workspace: workspace_from_args(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + workspace_from_args(), + )), }) .await .ok()?; diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index fbad0d505f..3421f0f871 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -643,7 +643,7 @@ pub async fn sandbox_create( name: name.unwrap_or_default().to_string(), labels, annotations, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), await_main_process_attachment, workload_template_name: template.unwrap_or_default().to_string(), }; @@ -689,7 +689,7 @@ pub async fn sandbox_create( name: sandbox_name.clone(), setting_key: settings::PROPOSAL_APPROVAL_MODE_KEY.to_string(), setting_value: Some(setting), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), ..Default::default() }) .await @@ -1307,7 +1307,7 @@ async fn stage_rootfs_tar( // archive over its configured limit, before allocating anything. let slot = client .begin_rootfs_tar_staging(BeginRootfsTarStagingRequest { - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), file_name, size_bytes: source_meta.len(), }) @@ -1495,7 +1495,7 @@ pub async fn sandbox_get( let response = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -1657,7 +1657,7 @@ pub async fn sandbox_exec_grpc( let sandbox = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -1891,7 +1891,7 @@ async fn fetch_ready_sandbox_for_forward( let response = match client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -2249,12 +2249,11 @@ pub async fn sandbox_list( limit, offset, label_selector: label_selector.unwrap_or("").to_string(), - workspace: if all_workspaces { - String::new() + workspace_scope: Some(if all_workspaces { + openshell_core::proto::all_workspaces_selector() } else { - workspace.to_string() - }, - all_workspaces, + openshell_core::proto::workspace_selector(workspace) + }), }) .await .into_diagnostic()?; @@ -2490,7 +2489,7 @@ pub async fn sandbox_template_create( labels, resource_version: 0, annotations, - workspace: String::new(), + workspace: workspace.to_string(), deletion_timestamp_ms: 0, }), spec: Some(SandboxWorkloadTemplateSpec { @@ -2503,7 +2502,7 @@ pub async fn sandbox_template_create( desired_service_level, }), }), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -2568,7 +2567,7 @@ pub async fn sandbox_template_get( let response = client .get_sandbox_template(GetSandboxTemplateRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -2602,12 +2601,11 @@ pub async fn sandbox_template_list( .list_sandbox_templates(ListSandboxTemplatesRequest { limit, offset, - workspace: if all_workspaces { - String::new() + workspace_scope: Some(if all_workspaces { + openshell_core::proto::all_workspaces_selector() } else { - workspace.to_string() - }, - all_workspaces, + openshell_core::proto::workspace_selector(workspace) + }), label_selector: label_selector.unwrap_or_default().to_string(), }) .await @@ -2651,7 +2649,7 @@ pub async fn sandbox_template_delete( let response = client .delete_sandbox_template(DeleteSandboxTemplateRequest { name: name.clone(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -3036,8 +3034,7 @@ pub async fn sandbox_delete( limit: 1000, offset: 0, label_selector: String::new(), - workspace: workspace.to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -3069,7 +3066,7 @@ pub async fn sandbox_delete( let response = match client .delete_sandbox(DeleteSandboxRequest { name: name.clone(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await { @@ -3121,7 +3118,7 @@ pub async fn sandbox_stop( let sandbox = client .stop_sandbox(StopSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -3144,7 +3141,7 @@ pub async fn sandbox_start( let sandbox = client .start_sandbox(StartSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -3245,7 +3242,7 @@ pub async fn service_expose( service: service.to_string(), target_port: u32::from(target_port), domain: true, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .map_err(service_expose_status_error)? @@ -3295,12 +3292,11 @@ pub async fn service_list( sandbox: sandbox.unwrap_or_default().to_string(), limit, offset, - workspace: if all_workspaces { - String::new() + workspace_scope: Some(if all_workspaces { + openshell_core::proto::all_workspaces_selector() } else { - workspace.to_string() - }, - all_workspaces, + openshell_core::proto::workspace_selector(workspace) + }), }) .await .map_err(|status| service_status_error("list services", "sandbox:read", status))? @@ -3340,7 +3336,7 @@ pub async fn service_get( .get_service(GetServiceRequest { sandbox: sandbox.to_string(), service: service.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .map_err(|status| service_status_error("get service", "sandbox:read", status))? @@ -3362,7 +3358,7 @@ pub async fn service_delete( .delete_service(DeleteServiceRequest { sandbox: sandbox.to_string(), service: service.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .map_err(|status| service_status_error("delete service", "sandbox:write", status))? @@ -4139,7 +4135,7 @@ pub async fn sandbox_policy_set_global( yes: bool, wait: bool, _timeout_secs: u64, - workspace: &str, + _workspace: &str, tls: &TlsOptions, ) -> Result<()> { if wait { @@ -4159,7 +4155,6 @@ pub async fn sandbox_policy_set_global( name: String::new(), policy: Some(policy), global: true, - workspace: workspace.to_string(), ..Default::default() }) .await @@ -4190,7 +4185,7 @@ pub async fn sandbox_settings_get( let sandbox = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -4348,7 +4343,7 @@ pub async fn gateway_setting_set( key: &str, value: &str, yes: bool, - workspace: &str, + _workspace: &str, tls: &TlsOptions, ) -> Result<()> { let setting_value = parse_cli_setting_value(key, value)?; @@ -4361,7 +4356,6 @@ pub async fn gateway_setting_set( setting_key: key.to_string(), setting_value: Some(setting_value), global: true, - workspace: workspace.to_string(), ..Default::default() }) .await @@ -4394,7 +4388,7 @@ pub async fn sandbox_setting_set( name: name.to_string(), setting_key: key.to_string(), setting_value: Some(setting_value), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), ..Default::default() }) .await @@ -4416,7 +4410,7 @@ pub async fn gateway_setting_delete( server: &str, key: &str, yes: bool, - workspace: &str, + _workspace: &str, tls: &TlsOptions, ) -> Result<()> { confirm_global_setting_delete(key, yes)?; @@ -4428,7 +4422,6 @@ pub async fn gateway_setting_delete( setting_key: key.to_string(), delete_setting: true, global: true, - workspace: workspace.to_string(), ..Default::default() }) .await @@ -4461,7 +4454,7 @@ pub async fn sandbox_setting_delete( name: name.to_string(), setting_key: key.to_string(), delete_setting: true, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), ..Default::default() }) .await @@ -4507,7 +4500,7 @@ pub async fn sandbox_policy_set( name: name.to_string(), version: 0, global: false, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .ok() @@ -4518,7 +4511,7 @@ pub async fn sandbox_policy_set( .update_config(UpdateConfigRequest { name: name.to_string(), policy: Some(policy), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), ..Default::default() }) .await @@ -4566,7 +4559,7 @@ pub async fn sandbox_policy_set( name: name.to_string(), version: resp.version, global: false, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -4643,7 +4636,7 @@ pub async fn sandbox_policy_update( let sandbox = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -4692,7 +4685,7 @@ pub async fn sandbox_policy_update( .update_config(UpdateConfigRequest { name: name.to_string(), merge_operations: plan.merge_operations, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), ..Default::default() }) .await @@ -4740,7 +4733,7 @@ pub async fn sandbox_policy_update( name: name.to_string(), version: response.version, global: false, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -4848,7 +4841,7 @@ where name: name.to_string(), version, global: false, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -4930,7 +4923,7 @@ where let sandbox = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -5032,7 +5025,7 @@ pub async fn sandbox_policy_get_global( version: u32, view: PolicyGetView, output: &str, - workspace: &str, + _workspace: &str, tls: &TlsOptions, ) -> Result<()> { let mut client = grpc_client(server, tls).await?; @@ -5042,7 +5035,7 @@ pub async fn sandbox_policy_get_global( name: String::new(), version, global: true, - workspace: workspace.to_string(), + workspace_scope: None, }) .await .into_diagnostic()?; @@ -5183,7 +5176,7 @@ pub async fn sandbox_policy_list( limit, offset: 0, global: false, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -5207,7 +5200,7 @@ pub async fn sandbox_policy_list_global( server: &str, limit: u32, output: &str, - workspace: &str, + _workspace: &str, tls: &TlsOptions, ) -> Result<()> { let mut client = grpc_client(server, tls).await?; @@ -5218,7 +5211,7 @@ pub async fn sandbox_policy_list_global( limit, offset: 0, global: true, - workspace: workspace.to_string(), + workspace_scope: None, }) .await .into_diagnostic()?; @@ -5310,7 +5303,7 @@ pub async fn sandbox_logs( let sandbox = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -5375,7 +5368,7 @@ pub async fn sandbox_logs( since_ms, sources: source_filter, min_level: level.to_uppercase(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -5451,7 +5444,7 @@ pub async fn sandbox_draft_get( .get_draft_policy(GetDraftPolicyRequest { name: name.to_string(), status_filter: status_filter.unwrap_or("").to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -5559,7 +5552,7 @@ pub async fn sandbox_draft_approve( .get_draft_policy(GetDraftPolicyRequest { name: name.to_string(), status_filter: String::new(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -5574,7 +5567,7 @@ pub async fn sandbox_draft_approve( .approve_draft_chunk(ApproveDraftChunkRequest { name: name.to_string(), chunk_id: chunk_id.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), review_token, }) .await @@ -5607,7 +5600,7 @@ pub async fn sandbox_draft_reject( name: name.to_string(), chunk_id: chunk_id.to_string(), reason: reason.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -5630,7 +5623,7 @@ pub async fn sandbox_draft_approve_all( .get_draft_policy(GetDraftPolicyRequest { name: name.to_string(), status_filter: "pending".to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? @@ -5647,7 +5640,7 @@ pub async fn sandbox_draft_approve_all( .approve_all_draft_chunks(ApproveAllDraftChunksRequest { name: name.to_string(), include_security_flagged, - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), approvals, }) .await @@ -5677,7 +5670,7 @@ pub async fn sandbox_draft_clear( let response = client .clear_draft_chunks(ClearDraftChunksRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; @@ -5704,7 +5697,7 @@ pub async fn sandbox_draft_history( let response = client .get_draft_history(GetDraftHistoryRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()?; diff --git a/crates/openshell-cli/src/ssh.rs b/crates/openshell-cli/src/ssh.rs index 126cea652d..c0937db484 100644 --- a/crates/openshell-cli/src/ssh.rs +++ b/crates/openshell-cli/src/ssh.rs @@ -92,7 +92,7 @@ async fn ssh_session_config( let sandbox = client .get_sandbox(GetSandboxRequest { name: name.to_string(), - workspace: workspace.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await .into_diagnostic()? diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 2d1b3f25d3..912835faa3 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -2920,7 +2920,7 @@ async fn provider_create_supports_nvidia_type_with_nvidia_api_key() { let response = client .get_provider(GetProviderRequest { name: "my-nvidia".to_string(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }) .await .expect("get provider should succeed") diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index d9e3cdad98..6ed9a74339 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -43,6 +43,19 @@ use tokio_stream::wrappers::TcpListenerStream; use tonic::transport::{Certificate as TlsCertificate, Identity, Server, ServerTlsConfig}; use tonic::{Response, Status}; +fn selected_workspace( + scope: &Option, +) -> Option<&str> { + match scope.as_ref()?.selection.as_ref()? { + openshell_core::proto::datamodel::v1::workspace_selector::Selection::Workspace( + workspace, + ) => Some(workspace), + openshell_core::proto::datamodel::v1::workspace_selector::Selection::AllWorkspaces(_) => { + None + } + } +} + #[derive(Clone, Default)] struct SandboxState { deleted_names: Arc>>>, @@ -217,7 +230,9 @@ impl OpenShell for TestOpenShell { .unwrap_or_default(), resource_version: 1, annotations: HashMap::new(), - workspace: request.workspace.clone(), + workspace: selected_workspace(&request.workspace_scope) + .unwrap_or("default") + .to_string(), deletion_timestamp_ms: 0, }); self.state @@ -249,7 +264,9 @@ impl OpenShell for TestOpenShell { labels: HashMap::new(), resource_version: 1, annotations: HashMap::new(), - workspace: request.workspace, + workspace: selected_workspace(&request.workspace_scope) + .unwrap_or("default") + .to_string(), deletion_timestamp_ms: 0, }), spec: None, @@ -1774,7 +1791,7 @@ async fn sandbox_create_with_template_sends_workload_template_name() { } #[tokio::test] -async fn sandbox_template_create_sends_workload_template_resource() { +async fn sandbox_template_create_sends_non_default_workspace_in_scope_and_metadata() { let server = run_server().await; let fake_ssh_dir = tempfile::tempdir().unwrap(); let xdg_dir = tempfile::tempdir().unwrap(); @@ -1795,7 +1812,7 @@ async fn sandbox_template_create_sends_workload_template_resource() { HashMap::from([("owner".to_string(), "platform".to_string())]), HashMap::from([("FEATURE_FLAG".to_string(), "on".to_string())]), "table", - "default", + "team-a", &tls, ) .await @@ -1805,10 +1822,11 @@ async fn sandbox_template_create_sends_workload_template_resource() { let request = requests .first() .expect("template create request should be recorded"); - assert_eq!(request.workspace, "default"); + assert_eq!(selected_workspace(&request.workspace_scope), Some("team-a")); let template = request.template.as_ref().expect("template should be sent"); let metadata = template.metadata.as_ref().expect("metadata should be sent"); assert_eq!(metadata.name, "gpu-kata"); + assert_eq!(metadata.workspace, "team-a"); assert_eq!(metadata.labels.get("team"), Some(&"runtime".to_string())); assert_eq!( metadata.annotations.get("owner"), @@ -1877,15 +1895,20 @@ async fn sandbox_template_list_and_delete_send_workspace_requests() { assert_eq!(list_request.limit, 25); assert_eq!(list_request.offset, 5); assert_eq!(list_request.label_selector, "team=runtime"); - assert_eq!(list_request.workspace, "default"); - assert!(!list_request.all_workspaces); + assert_eq!( + selected_workspace(&list_request.workspace_scope), + Some("default") + ); let delete_requests = template_delete_requests(&server).await; let delete_request = delete_requests .first() .expect("template delete request should be recorded"); assert_eq!(delete_request.name, "gpu-kata"); - assert_eq!(delete_request.workspace, "default"); + assert_eq!( + selected_workspace(&delete_request.workspace_scope), + Some("default") + ); } #[tokio::test] diff --git a/crates/openshell-core/src/grpc_client.rs b/crates/openshell-core/src/grpc_client.rs index 315ff0b72f..38b91e8501 100644 --- a/crates/openshell-core/src/grpc_client.rs +++ b/crates/openshell-core/src/grpc_client.rs @@ -28,6 +28,7 @@ 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}; @@ -752,7 +753,7 @@ async fn sync_policy_with_client( .update_config(UpdateConfigRequest { name: sandbox.to_string(), policy: Some(policy.clone()), - workspace: workspace.to_string(), + workspace_scope: Some(workspace_selector(workspace)), ..Default::default() }) .await @@ -1180,7 +1181,7 @@ impl CachedOpenShellClient { .get_draft_policy(GetDraftPolicyRequest { name: sandbox_name.to_string(), status_filter: status_filter.to_string(), - workspace: self.workspace(), + workspace_scope: Some(workspace_selector(self.workspace())), }) .await .into_diagnostic()?; diff --git a/crates/openshell-core/src/proto/mod.rs b/crates/openshell-core/src/proto/mod.rs index cd4b24afe9..d0b976ff24 100644 --- a/crates/openshell-core/src/proto/mod.rs +++ b/crates/openshell-core/src/proto/mod.rs @@ -73,3 +73,19 @@ pub use middleware::v1::*; pub use openshell::*; pub use sandbox::v1::*; pub use test::ObjectForTest; + +/// Build a selector for one explicitly named workspace. +pub fn workspace_selector(workspace: impl Into) -> WorkspaceSelector { + WorkspaceSelector { + selection: Some(workspace_selector::Selection::Workspace(workspace.into())), + } +} + +/// Build a selector for every workspace supported by a cross-workspace request. +pub fn all_workspaces_selector() -> WorkspaceSelector { + WorkspaceSelector { + selection: Some(workspace_selector::Selection::AllWorkspaces( + AllWorkspaces {}, + )), + } +} diff --git a/crates/openshell-gateway-interceptors/src/proto_json.rs b/crates/openshell-gateway-interceptors/src/proto_json.rs index 70f6667321..dde37190e6 100644 --- a/crates/openshell-gateway-interceptors/src/proto_json.rs +++ b/crates/openshell-gateway-interceptors/src/proto_json.rs @@ -291,7 +291,7 @@ mod tests { use openshell_core::proto::{ CreateProviderRequest, CreateSandboxRequest, GpuResourceRequirements, Provider, - SandboxSpec, UpdateConfigRequest, + SandboxSpec, UpdateConfigRequest, workspace_selector, }; use prost::Message as _; use prost_types::{ @@ -315,7 +315,7 @@ mod tests { name: "demo".to_string(), labels: HashMap::from([("team".to_string(), "agent".to_string())]), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }; @@ -345,7 +345,7 @@ mod tests { config: HashMap::from([("region".to_string(), "us-west".to_string())]), ..Provider::default() }), - workspace: String::new(), + workspace_scope: Some(workspace_selector("default")), }; let encoded = request.encode_to_vec(); @@ -400,6 +400,7 @@ mod tests { environment: HashMap::from([("FEATURE_FLAG".to_string(), "on".to_string())]), ..SandboxSpec::default() }), + workspace_scope: Some(workspace_selector("default")), ..CreateSandboxRequest::default() }; @@ -454,6 +455,7 @@ mod tests { "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 cd0f59ea3c..0bd1ba2b47 100644 --- a/crates/openshell-gateway-interceptors/src/runtime.rs +++ b/crates/openshell-gateway-interceptors/src/runtime.rs @@ -764,7 +764,7 @@ mod tests { config: HashMap::from([("region".to_string(), "old".to_string())]), ..Provider::default() }), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }; let json = codec .decode_message_to_json("openshell.v1.CreateProviderRequest", &request) @@ -1017,6 +1017,7 @@ 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 { @@ -1074,7 +1075,7 @@ mod tests { name: "demo".to_string(), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }; diff --git a/crates/openshell-sdk/README.md b/crates/openshell-sdk/README.md index c773de627d..dc57d5ec11 100644 --- a/crates/openshell-sdk/README.md +++ b/crates/openshell-sdk/README.md @@ -46,6 +46,7 @@ mTLS (client certificates) is not supported. `health`, `create_sandbox`, `get_sandbox`, `list_sandboxes`, `delete_sandbox`, `create_sandbox_from_template`, `create_sandbox_template`, `get_sandbox_template`, `list_sandbox_templates`, `delete_sandbox_template`, +`list_sandboxes_all_workspaces`, `list_sandbox_templates_all_workspaces`, `wait_ready`, `wait_deleted`, and `exec`. Curated types (`SandboxSpec`, `SandboxRef`, `Health`, `ListOptions`, `SandboxTemplateListOptions`, `ExecOptions`, `SandboxPhase`) use SDK-shaped enums rather than raw proto @@ -54,6 +55,10 @@ integers where practical. Reusable template resources are exposed as portable workload shape and driver config. Failures map to a typed `SdkError` with a discriminable kind. +Curated calls without a workspace argument explicitly select the `default` +workspace. Cross-workspace listing uses the separate `*_all_workspaces` +methods and requires Platform Admin access. + ```rust use openshell_sdk::{ ClientConfig, OpenShellClient, SandboxTemplateCreateSpec, diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index cd48c41558..4713f0ed1c 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -169,7 +169,7 @@ impl OpenShellClient { .unary(|mut grpc| { let request = proto::CreateSandboxTemplateRequest { template: Some(template.clone()), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.create_sandbox_template(request).await } }) @@ -183,7 +183,7 @@ impl OpenShellClient { .unary(|mut grpc| { let request = proto::GetSandboxTemplateRequest { name: name.to_string(), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.get_sandbox_template(request).await } }) @@ -191,7 +191,7 @@ impl OpenShellClient { sandbox_template_from_response(response.template) } - /// List reusable sandbox templates in the default workspace or across all workspaces. + /// List reusable sandbox templates in the default workspace. pub async fn list_sandbox_templates( &self, opts: SandboxTemplateListOptions, @@ -201,9 +201,27 @@ impl OpenShellClient { let request = proto::ListSandboxTemplatesRequest { limit: opts.limit, offset: opts.offset, - workspace: String::new(), - all_workspaces: opts.all_workspaces, label_selector: opts.label_selector.clone(), + workspace_scope: Some(proto::workspace_selector("default")), + }; + async move { grpc.list_sandbox_templates(request).await } + }) + .await?; + Ok(response.templates) + } + + /// List reusable sandbox templates across all workspaces. + pub async fn list_sandbox_templates_all_workspaces( + &self, + opts: SandboxTemplateListOptions, + ) -> Result> { + let response = self + .unary(|mut grpc| { + let request = proto::ListSandboxTemplatesRequest { + limit: opts.limit, + offset: opts.offset, + label_selector: opts.label_selector.clone(), + workspace_scope: Some(proto::all_workspaces_selector()), }; async move { grpc.list_sandbox_templates(request).await } }) @@ -217,7 +235,7 @@ impl OpenShellClient { .unary(|mut grpc| { let request = proto::DeleteSandboxTemplateRequest { name: name.to_string(), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.delete_sandbox_template(request).await } }) @@ -231,7 +249,7 @@ impl OpenShellClient { .unary(|mut grpc| { let request = proto::GetSandboxRequest { name: name.to_string(), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.get_sandbox(request).await } }) @@ -247,8 +265,7 @@ impl OpenShellClient { limit: opts.limit, offset: opts.offset, label_selector: opts.label_selector.clone().unwrap_or_default(), - workspace: String::new(), - all_workspaces: false, + workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.list_sandboxes(request).await } }) @@ -271,7 +288,7 @@ impl OpenShellClient { .unary(|mut grpc| { let request = proto::DeleteSandboxRequest { name: name.to_string(), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.delete_sandbox(request).await } }) @@ -285,7 +302,7 @@ impl OpenShellClient { .unary(|mut grpc| { let request = proto::StopSandboxRequest { name: name.to_string(), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.stop_sandbox(request).await } }) @@ -299,7 +316,7 @@ impl OpenShellClient { .unary(|mut grpc| { let request = proto::StartSandboxRequest { name: name.to_string(), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), }; async move { grpc.start_sandbox(request).await } }) @@ -373,8 +390,7 @@ impl OpenShellClient { limit: opts.limit, offset: opts.offset, label_selector: opts.label_selector.clone().unwrap_or_default(), - workspace: String::new(), - all_workspaces: true, + workspace_scope: Some(proto::all_workspaces_selector()), }; async move { grpc.list_sandboxes(request).await } }) @@ -618,7 +634,7 @@ impl WorkspaceScopedClient { /// Create a new sandbox in this workspace. pub async fn create_sandbox(&self, spec: SandboxSpec) -> Result { let mut request = create_sandbox_request(spec); - request.workspace = self.workspace.clone(); + request.workspace_scope = Some(proto::workspace_selector(&self.workspace)); let response = self .client .unary(|mut grpc| { @@ -635,7 +651,7 @@ impl WorkspaceScopedClient { spec: SandboxTemplateCreateSpec, ) -> Result { let mut request = create_sandbox_from_template_request(spec); - request.workspace = self.workspace.clone(); + request.workspace_scope = Some(proto::workspace_selector(&self.workspace)); let response = self .client .unary(|mut grpc| { @@ -656,7 +672,7 @@ impl WorkspaceScopedClient { .unary(|mut grpc| { let request = proto::CreateSandboxTemplateRequest { template: Some(template.clone()), - workspace: self.workspace.clone(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.create_sandbox_template(request).await } }) @@ -671,7 +687,7 @@ impl WorkspaceScopedClient { .unary(|mut grpc| { let request = proto::GetSandboxTemplateRequest { name: name.to_string(), - workspace: self.workspace.clone(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.get_sandbox_template(request).await } }) @@ -679,7 +695,7 @@ impl WorkspaceScopedClient { sandbox_template_from_response(response.template) } - /// List reusable sandbox templates in this workspace, or across all workspaces. + /// List reusable sandbox templates in this workspace. pub async fn list_sandbox_templates( &self, opts: SandboxTemplateListOptions, @@ -690,13 +706,8 @@ impl WorkspaceScopedClient { let request = proto::ListSandboxTemplatesRequest { limit: opts.limit, offset: opts.offset, - workspace: if opts.all_workspaces { - String::new() - } else { - self.workspace.clone() - }, - all_workspaces: opts.all_workspaces, label_selector: opts.label_selector.clone(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.list_sandbox_templates(request).await } }) @@ -711,7 +722,7 @@ impl WorkspaceScopedClient { .unary(|mut grpc| { let request = proto::DeleteSandboxTemplateRequest { name: name.to_string(), - workspace: self.workspace.clone(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.delete_sandbox_template(request).await } }) @@ -726,7 +737,7 @@ impl WorkspaceScopedClient { .unary(|mut grpc| { let request = proto::GetSandboxRequest { name: name.to_string(), - workspace: self.workspace.clone(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.get_sandbox(request).await } }) @@ -743,8 +754,7 @@ impl WorkspaceScopedClient { limit: opts.limit, offset: opts.offset, label_selector: opts.label_selector.clone().unwrap_or_default(), - workspace: self.workspace.clone(), - all_workspaces: false, + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.list_sandboxes(request).await } }) @@ -763,7 +773,7 @@ impl WorkspaceScopedClient { .unary(|mut grpc| { let request = proto::DeleteSandboxRequest { name: name.to_string(), - workspace: self.workspace.clone(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.delete_sandbox(request).await } }) @@ -778,7 +788,7 @@ impl WorkspaceScopedClient { .unary(|mut grpc| { let request = proto::StopSandboxRequest { name: name.to_string(), - workspace: self.workspace.clone(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.stop_sandbox(request).await } }) @@ -793,7 +803,7 @@ impl WorkspaceScopedClient { .unary(|mut grpc| { let request = proto::StartSandboxRequest { name: name.to_string(), - workspace: self.workspace.clone(), + workspace_scope: Some(proto::workspace_selector(&self.workspace)), }; async move { grpc.start_sandbox(request).await } }) @@ -987,7 +997,7 @@ fn create_sandbox_request(spec: SandboxSpec) -> proto::CreateSandboxRequest { name: name.unwrap_or_default(), labels, annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), } @@ -1016,7 +1026,7 @@ fn create_sandbox_from_template_request( name: name.unwrap_or_default(), labels, annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(proto::workspace_selector("default")), workload_template_name: template_name, await_main_process_attachment: false, } diff --git a/crates/openshell-sdk/src/types.rs b/crates/openshell-sdk/src/types.rs index db2944474b..12b5a72cb9 100644 --- a/crates/openshell-sdk/src/types.rs +++ b/crates/openshell-sdk/src/types.rs @@ -167,8 +167,6 @@ pub struct SandboxTemplateListOptions { pub offset: u32, /// Optional label selector in `key=value,key2=value2` form. pub label_selector: String, - /// List templates across all workspaces. - pub all_workspaces: bool, } /// Reference to a sandbox owned by the gateway. diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 89cc68bf0f..5fd2133b5c 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -23,6 +23,22 @@ use tokio::sync::Mutex; use tokio_stream::wrappers::TcpListenerStream; use tonic::{Response, Status}; +fn selected_workspace(scope: &Option) -> Option<&str> { + match scope.as_ref()?.selection.as_ref()? { + proto::datamodel::v1::workspace_selector::Selection::Workspace(workspace) => { + Some(workspace) + } + proto::datamodel::v1::workspace_selector::Selection::AllWorkspaces(_) => None, + } +} + +fn selects_all_workspaces(scope: &Option) -> bool { + matches!( + scope.as_ref().and_then(|scope| scope.selection.as_ref()), + Some(proto::datamodel::v1::workspace_selector::Selection::AllWorkspaces(_)) + ) +} + /// Captured fixture state — what the mock observed and the canned replies it /// returned. One per test so assertions are scoped. #[derive(Default)] @@ -243,11 +259,7 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let request = request.into_inner(); - let workspace = if request.workspace.is_empty() { - "default" - } else { - &request.workspace - }; + let workspace = selected_workspace(&request.workspace_scope).unwrap_or("default"); let template = workload_template_proto(&request.name, workspace); *self.state.last_template_get.lock().await = Some(request); Ok(Response::new(proto::SandboxTemplateResponse { @@ -287,7 +299,7 @@ impl OpenShell for TestOpenShell { let sandbox = sandbox_with_phase_ws( &request.name, proto::SandboxPhase::Stopped, - &request.workspace, + selected_workspace(&request.workspace_scope).unwrap_or("default"), ); *self.state.last_stop.lock().await = Some(request); Ok(Response::new(proto::SandboxResponse { @@ -303,7 +315,7 @@ impl OpenShell for TestOpenShell { let sandbox = sandbox_with_phase_ws( &request.name, proto::SandboxPhase::Starting, - &request.workspace, + selected_workspace(&request.workspace_scope).unwrap_or("default"), ); *self.state.last_start.lock().await = Some(request); Ok(Response::new(proto::SandboxResponse { @@ -318,7 +330,8 @@ impl OpenShell for TestOpenShell { let req = request.into_inner(); let name = req.name; *self.state.last_get_name.lock().await = Some(name.clone()); - *self.state.last_get_workspace.lock().await = Some(req.workspace.clone()); + *self.state.last_get_workspace.lock().await = + selected_workspace(&req.workspace_scope).map(str::to_string); let count = self.state.get_calls.fetch_add(1, Ordering::SeqCst); if self.state.get_returns_not_found { @@ -387,7 +400,8 @@ impl OpenShell for TestOpenShell { ) -> Result, Status> { let req = request.into_inner(); *self.state.last_delete_name.lock().await = Some(req.name); - *self.state.last_delete_workspace.lock().await = Some(req.workspace); + *self.state.last_delete_workspace.lock().await = + selected_workspace(&req.workspace_scope).map(str::to_string); Ok(Response::new(proto::DeleteSandboxResponse { deleted: true, })) @@ -965,7 +979,10 @@ async fn sandbox_template_crud_uses_default_workspace() { assert_eq!(created.metadata.as_ref().unwrap().name, "python"); let observed_create = state.last_template_create.lock().await.clone().unwrap(); - assert!(observed_create.workspace.is_empty()); + assert_eq!( + selected_workspace(&observed_create.workspace_scope), + Some("default") + ); assert_eq!( observed_create .template @@ -980,14 +997,16 @@ async fn sandbox_template_crud_uses_default_workspace() { assert_eq!(fetched.metadata.as_ref().unwrap().name, "python"); let observed_get = state.last_template_get.lock().await.clone().unwrap(); assert_eq!(observed_get.name, "python"); - assert!(observed_get.workspace.is_empty()); + assert_eq!( + selected_workspace(&observed_get.workspace_scope), + Some("default") + ); let listed = client - .list_sandbox_templates(SandboxTemplateListOptions { + .list_sandbox_templates_all_workspaces(SandboxTemplateListOptions { limit: 10, offset: 2, label_selector: String::new(), - all_workspaces: true, }) .await .unwrap(); @@ -995,14 +1014,16 @@ async fn sandbox_template_crud_uses_default_workspace() { let observed_list = state.last_template_list.lock().await.clone().unwrap(); assert_eq!(observed_list.limit, 10); assert_eq!(observed_list.offset, 2); - assert!(observed_list.workspace.is_empty()); - assert!(observed_list.all_workspaces); + assert!(selects_all_workspaces(&observed_list.workspace_scope)); let deleted = client.delete_sandbox_template("python").await.unwrap(); assert!(deleted); let observed_delete = state.last_template_delete.lock().await.clone().unwrap(); assert_eq!(observed_delete.name, "python"); - assert!(observed_delete.workspace.is_empty()); + assert_eq!( + selected_workspace(&observed_delete.workspace_scope), + Some("default") + ); } #[tokio::test] @@ -1084,7 +1105,7 @@ async fn stop_and_start_map_requests_and_phases() { assert_eq!(stopped.phase, SandboxPhase::Stopped); let stop = state.last_stop.lock().await.clone().unwrap(); assert_eq!(stop.name, "sleepy"); - assert!(stop.workspace.is_empty()); + assert_eq!(selected_workspace(&stop.workspace_scope), Some("default")); let started = client .workspace("team-a") @@ -1094,7 +1115,7 @@ async fn stop_and_start_map_requests_and_phases() { assert_eq!(started.phase, SandboxPhase::Starting); let start = state.last_start.lock().await.clone().unwrap(); assert_eq!(start.name, "sleepy"); - assert_eq!(start.workspace, "team-a"); + assert_eq!(selected_workspace(&start.workspace_scope), Some("team-a")); } #[tokio::test] @@ -1337,7 +1358,10 @@ async fn workspace_scoped_create_passes_workspace() { assert_eq!(result.name, "my-box"); let observed = state.last_create.lock().await.clone().unwrap(); - assert_eq!(observed.workspace, "staging"); + assert_eq!( + selected_workspace(&observed.workspace_scope), + Some("staging") + ); } #[tokio::test] @@ -1362,7 +1386,10 @@ async fn workspace_scoped_create_from_template_passes_workspace() { assert_eq!(sandbox.name, "from-template"); let observed = state.last_create.lock().await.clone().unwrap(); - assert_eq!(observed.workspace, "staging"); + assert_eq!( + selected_workspace(&observed.workspace_scope), + Some("staging") + ); assert_eq!(observed.workload_template_name, "python"); assert_eq!(observed.spec.unwrap().policy.unwrap().version, 2); } @@ -1395,8 +1422,7 @@ async fn workspace_scoped_list_passes_workspace() { assert_eq!(items.len(), 2); let observed = state.last_list_request.lock().await.clone().unwrap(); - assert_eq!(observed.workspace, "dev"); - assert!(!observed.all_workspaces); + assert_eq!(selected_workspace(&observed.workspace_scope), Some("dev")); } #[tokio::test] @@ -1410,12 +1436,18 @@ async fn workspace_scoped_sandbox_template_crud_passes_workspace() { .await .unwrap(); let observed_create = state.last_template_create.lock().await.clone().unwrap(); - assert_eq!(observed_create.workspace, "staging"); + assert_eq!( + selected_workspace(&observed_create.workspace_scope), + Some("staging") + ); ws.get_sandbox_template("python").await.unwrap(); let observed_get = state.last_template_get.lock().await.clone().unwrap(); assert_eq!(observed_get.name, "python"); - assert_eq!(observed_get.workspace, "staging"); + assert_eq!( + selected_workspace(&observed_get.workspace_scope), + Some("staging") + ); let listed = ws .list_sandbox_templates(SandboxTemplateListOptions::default()) @@ -1423,24 +1455,26 @@ async fn workspace_scoped_sandbox_template_crud_passes_workspace() { .unwrap(); assert_eq!(listed.len(), 2); let observed_list = state.last_template_list.lock().await.clone().unwrap(); - assert_eq!(observed_list.workspace, "staging"); - assert!(!observed_list.all_workspaces); + assert_eq!( + selected_workspace(&observed_list.workspace_scope), + Some("staging") + ); - ws.list_sandbox_templates(SandboxTemplateListOptions { - all_workspaces: true, - ..Default::default() - }) - .await - .unwrap(); + client + .list_sandbox_templates_all_workspaces(SandboxTemplateListOptions::default()) + .await + .unwrap(); let observed_all = state.last_template_list.lock().await.clone().unwrap(); - assert!(observed_all.workspace.is_empty()); - assert!(observed_all.all_workspaces); + assert!(selects_all_workspaces(&observed_all.workspace_scope)); let deleted = ws.delete_sandbox_template("python").await.unwrap(); assert!(deleted); let observed_delete = state.last_template_delete.lock().await.clone().unwrap(); assert_eq!(observed_delete.name, "python"); - assert_eq!(observed_delete.workspace, "staging"); + assert_eq!( + selected_workspace(&observed_delete.workspace_scope), + Some("staging") + ); } #[tokio::test] @@ -1470,8 +1504,7 @@ async fn list_sandboxes_all_workspaces_sets_flag() { assert_eq!(items.len(), 2); let observed = state.last_list_request.lock().await.clone().unwrap(); - assert!(observed.all_workspaces); - assert!(observed.workspace.is_empty()); + assert!(selects_all_workspaces(&observed.workspace_scope)); } // ---- Workspace CRUD tests ---- diff --git a/crates/openshell-server/src/auth/workspace_authz.rs b/crates/openshell-server/src/auth/workspace_authz.rs index e23d2287a3..2958d656c7 100644 --- a/crates/openshell-server/src/auth/workspace_authz.rs +++ b/crates/openshell-server/src/auth/workspace_authz.rs @@ -9,7 +9,10 @@ //! and workspace-level role. use super::principal::Principal; -use openshell_core::proto::WorkspaceRole as ProtoWorkspaceRole; +use openshell_core::proto::{ + WorkspaceRole as ProtoWorkspaceRole, WorkspaceSelector, + workspace_selector::Selection as WorkspaceSelection, +}; use tonic::Status; use crate::persistence::Store; @@ -39,7 +42,7 @@ impl MinWorkspaceRole { /// Result of a successful workspace authorization check. #[derive(Debug)] pub struct AuthorizedWorkspace { - /// Resolved workspace name (empty string normalized to `"default"`). + /// Explicit workspace name selected by the caller. pub workspace: String, /// How the caller was authorized. pub grant: AuthGrant, @@ -56,6 +59,75 @@ pub enum AuthGrant { Sandbox, } +/// Authorized scope for a request that supports one workspace or all workspaces. +#[derive(Debug)] +pub enum AuthorizedWorkspaceScope { + /// One explicitly named workspace. + Workspace(AuthorizedWorkspace), + /// All workspaces, authorized for a platform administrator. + AllWorkspaces, +} + +/// Authorize the required named selector on a single-workspace request. +#[allow(clippy::result_large_err)] +pub async fn authorize_workspace_selector( + store: &Store, + admin_role: &str, + principal: &Principal, + selector: Option<&WorkspaceSelector>, + min_role: MinWorkspaceRole, +) -> Result { + let workspace = selected_workspace_name(selector)?; + authorize_workspace(store, admin_role, principal, workspace, min_role).await +} + +/// Authorize a selector on a request that explicitly supports all workspaces. +#[allow(clippy::result_large_err)] +pub async fn authorize_list_workspace_selector( + store: &Store, + admin_role: &str, + principal: &Principal, + selector: Option<&WorkspaceSelector>, + min_role: MinWorkspaceRole, +) -> Result { + match selected_workspace(selector)? { + WorkspaceSelection::Workspace(workspace) => { + authorize_workspace(store, admin_role, principal, workspace, min_role) + .await + .map(AuthorizedWorkspaceScope::Workspace) + } + WorkspaceSelection::AllWorkspaces(_) => { + require_platform_admin(admin_role, principal)?; + Ok(AuthorizedWorkspaceScope::AllWorkspaces) + } + } +} + +/// Return the explicitly selected workspace name, rejecting missing, empty, +/// or all-workspaces selections. +#[allow(clippy::result_large_err)] +pub fn selected_workspace_name(selector: Option<&WorkspaceSelector>) -> Result<&str, Status> { + match selected_workspace(selector)? { + WorkspaceSelection::Workspace(workspace) => Ok(workspace), + WorkspaceSelection::AllWorkspaces(_) => Err(Status::invalid_argument( + "all_workspaces is not supported by this request", + )), + } +} + +#[allow(clippy::result_large_err)] +fn selected_workspace(selector: Option<&WorkspaceSelector>) -> Result<&WorkspaceSelection, Status> { + let selection = selector + .and_then(|selector| selector.selection.as_ref()) + .ok_or_else(|| Status::invalid_argument("workspace_scope is required"))?; + + if let WorkspaceSelection::Workspace(workspace) = selection { + crate::grpc::workspace::validate_workspace_name(workspace)?; + } + + Ok(selection) +} + /// Authorize a workspace-scoped operation for a user principal. /// /// Checks workspace membership and role. Platform admins (callers whose @@ -72,7 +144,7 @@ pub async fn authorize_workspace( workspace: &str, min_role: MinWorkspaceRole, ) -> Result { - let workspace = normalize_workspace(workspace); + let workspace = workspace.to_string(); match principal { Principal::User(user) => { @@ -186,14 +258,6 @@ fn role_satisfies(member_role: ProtoWorkspaceRole, min_role: MinWorkspaceRole) - } } -fn normalize_workspace(workspace: &str) -> String { - if workspace.is_empty() { - "default".to_string() - } else { - workspace.to_string() - } -} - #[cfg(test)] mod tests { use super::*; @@ -401,20 +465,75 @@ mod tests { } #[tokio::test] - async fn empty_workspace_normalizes_to_default() { + async fn empty_named_selector_is_rejected() { let store = test_store().await; add_member(&store, "default", "user-d", ProtoWorkspaceRole::User).await; let principal = user_principal("user-d", &["openshell-user"]); - let result = authorize_workspace( + let result = authorize_workspace_selector( &store, "openshell-admin", &principal, - "", + Some(&openshell_core::proto::workspace_selector("")), MinWorkspaceRole::User, ) .await; - assert!(result.is_ok()); - assert_eq!(result.unwrap().workspace, "default"); + let err = result.unwrap_err(); + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert_eq!(err.message(), "workspace name is required"); + } + + #[test] + fn missing_and_unset_selectors_are_rejected() { + let missing = selected_workspace_name(None).unwrap_err(); + assert_eq!(missing.code(), tonic::Code::InvalidArgument); + assert_eq!(missing.message(), "workspace_scope is required"); + + let unset = selected_workspace_name(Some(&WorkspaceSelector::default())).unwrap_err(); + assert_eq!(unset.code(), tonic::Code::InvalidArgument); + assert_eq!(unset.message(), "workspace_scope is required"); + } + + #[test] + fn all_workspaces_is_rejected_for_single_workspace_requests() { + let err = selected_workspace_name(Some(&openshell_core::proto::all_workspaces_selector())) + .unwrap_err(); + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert_eq!( + err.message(), + "all_workspaces is not supported by this request" + ); + } + + #[tokio::test] + async fn all_workspaces_requires_platform_admin() { + let store = test_store().await; + let selector = openshell_core::proto::all_workspaces_selector(); + let principal = user_principal("workspace-user", &["openshell-user"]); + let err = authorize_list_workspace_selector( + &store, + "openshell-admin", + &principal, + Some(&selector), + MinWorkspaceRole::User, + ) + .await + .unwrap_err(); + assert_eq!(err.code(), tonic::Code::PermissionDenied); + + let admin = user_principal("platform-admin", &["openshell-admin"]); + let authorized = authorize_list_workspace_selector( + &store, + "openshell-admin", + &admin, + Some(&selector), + MinWorkspaceRole::User, + ) + .await + .unwrap(); + assert!(matches!( + authorized, + AuthorizedWorkspaceScope::AllWorkspaces + )); } #[tokio::test] diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 4940cd1aa3..4f2855dd0b 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -13,7 +13,8 @@ use crate::ServerState; use crate::auth::principal::Principal; use crate::auth::workspace_authz::{ - MinWorkspaceRole, authorize_sandbox_workspace, authorize_workspace, require_platform_admin, + MinWorkspaceRole, authorize_sandbox_workspace, authorize_workspace_selector, + require_platform_admin, selected_workspace_name, }; use crate::persistence::{ DraftChunkRecord, ObjectId, ObjectName, ObjectType, ObjectWorkspace, PolicyRecord, Store, @@ -3240,6 +3241,11 @@ async fn handle_update_config_inner( let req = request.into_inner(); validate_annotations(&req.annotations, "annotations")?; let workspace = if req.global { + if req.workspace_scope.is_some() { + return Err(Status::invalid_argument( + "workspace_scope must be omitted when global is true", + )); + } require_platform_admin(&state.admin_role, principal)?; String::new() } else { @@ -3248,15 +3254,16 @@ async fn handle_update_config_inner( } else { MinWorkspaceRole::Admin }; + let workspace = selected_workspace_name(req.workspace_scope.as_ref())?; authorize_sandbox_workspace( &state.store, &state.admin_role, principal, - &req.workspace, + workspace, min_role, ) .await?; - super::workspace::resolve_workspace(state.store.as_ref(), &req.workspace) + super::workspace::resolve_workspace(state.store.as_ref(), workspace) .await? .name }; @@ -3922,14 +3929,19 @@ pub(super) async fn handle_get_sandbox_policy_status( let principal = super::extract_principal(&request)?; let req = request.into_inner(); let workspace = if req.global { + if req.workspace_scope.is_some() { + return Err(Status::invalid_argument( + "workspace_scope must be omitted when global is true", + )); + } require_platform_admin(&state.admin_role, &principal)?; String::new() } else { - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -3990,14 +4002,19 @@ pub(super) async fn handle_list_sandbox_policies( let principal = super::extract_principal(&request)?; let req = request.into_inner(); let workspace = if req.global { + if req.workspace_scope.is_some() { + return Err(Status::invalid_argument( + "workspace_scope must be omitted when global is true", + )); + } require_platform_admin(&state.admin_role, &principal)?; String::new() } else { - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -4129,8 +4146,22 @@ pub(super) async fn handle_get_sandbox_logs( if req.sandbox_id.is_empty() { return Err(Status::invalid_argument("sandbox_id is required")); } - let _sandbox = + let authz = authorize_workspace_selector( + &state.store, + &state.admin_role, + &principal, + 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 lines = if req.lines == 0 { 2000 } else { req.lines }; let tail = state.tracing_log_bus.tail(&req.sandbox_id, lines as usize); @@ -4661,15 +4692,16 @@ 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, &principal, - &req.workspace, + workspace_name, MinWorkspaceRole::User, ) .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &req.workspace) + let workspace = super::workspace::resolve_workspace(state.store.as_ref(), workspace_name) .await? .name; if req.name.is_empty() { @@ -4742,11 +4774,11 @@ async fn handle_approve_draft_chunk_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -4897,11 +4929,11 @@ async fn handle_reject_draft_chunk_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -5007,11 +5039,11 @@ 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( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -5315,11 +5347,11 @@ 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( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -5396,11 +5428,11 @@ async fn handle_undo_draft_chunk_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -5493,11 +5525,11 @@ 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( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -5541,11 +5573,11 @@ 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( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -7538,6 +7570,7 @@ mod tests { with_user(Request::new(GetSandboxPolicyStatusRequest { name: "stored-invalid-history".to_string(), version: 2, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -7555,6 +7588,7 @@ mod tests { with_user(Request::new(ListSandboxPoliciesRequest { name: "stored-invalid-history".to_string(), limit: 10, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -7917,6 +7951,7 @@ mod tests { with_user(Request::new(UpdateConfigRequest { name: sandbox_name, policy: Some(mcp_policy_with_versions(&["2025-11-25"])), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -8023,6 +8058,7 @@ mod tests { with_user(Request::new(UpdateConfigRequest { name: sandbox_name.to_string(), policy: Some(candidate.clone()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -8081,6 +8117,7 @@ mod tests { with_user(Request::new(UpdateConfigRequest { name: sandbox_name.to_string(), policy: Some(candidate.clone()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -8409,6 +8446,7 @@ mod tests { let req = UpdateConfigRequest { name: "sandbox-1".to_string(), policy: Some(ProtoSandboxPolicy::default()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }; assert!(validate_sandbox_caller_update(&req).is_ok()); @@ -8431,6 +8469,7 @@ mod tests { name: "sandbox-1".to_string(), 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(); @@ -8511,7 +8550,9 @@ mod tests { &state, with_user(Request::new(GetSandboxLogsRequest { sandbox_id: "sandbox-b-id".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..GetSandboxLogsRequest::default() })), ) @@ -8597,6 +8638,47 @@ mod tests { ); } + #[tokio::test] + async fn global_policy_requests_reject_workspace_selectors() { + let state = test_server_state().await; + + let update_error = handle_update_config( + &state, + authed_request(UpdateConfigRequest { + global: true, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + ..Default::default() + }), + ) + .await + .unwrap_err(); + assert_eq!(update_error.code(), Code::InvalidArgument); + + let get_error = handle_get_sandbox_policy_status( + &state, + authed_request(GetSandboxPolicyStatusRequest { + global: true, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + ..Default::default() + }), + ) + .await + .unwrap_err(); + assert_eq!(get_error.code(), Code::InvalidArgument); + + let list_error = handle_list_sandbox_policies( + &state, + authed_request(ListSandboxPoliciesRequest { + global: true, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + ..Default::default() + }), + ) + .await + .unwrap_err(); + assert_eq!(list_error.code(), Code::InvalidArgument); + } + #[tokio::test] async fn update_config_rejects_missing_principal() { let state = test_server_state().await; @@ -8888,7 +8970,9 @@ mod tests { Request::new(GetDraftPolicyRequest { name: "sandbox-b".to_string(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), "sb-a", ); @@ -8905,6 +8989,7 @@ mod tests { Request::new(UpdateConfigRequest { name: "missing-sandbox".to_string(), policy: Some(ProtoSandboxPolicy::default()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), "sb-a", @@ -8940,7 +9025,9 @@ mod tests { Request::new(GetDraftPolicyRequest { name: "missing-sandbox".to_string(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), "sb-a", ); @@ -9996,7 +10083,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "ambiguous-update".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(test_ambiguous_policy()), ..Default::default() })), @@ -10033,7 +10122,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "unattached-binding".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(test_policy_with_credential_binding( "cloud", "api.cloud.example", @@ -10078,7 +10169,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "double-binding".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(test_policy_with_credential_binding( "cloud", "api.cloud.example", @@ -10146,7 +10239,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "endpointless-gating".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(l4.clone()), ..Default::default() })), @@ -10169,7 +10264,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "endpointless-gating".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(tls_skip.clone()), ..Default::default() })), @@ -10183,7 +10280,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "endpointless-gating".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), merge_operations: vec![add_bound_rule(&l4)], ..Default::default() })), @@ -10197,7 +10296,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "endpointless-gating".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), merge_operations: vec![add_bound_rule(&tls_skip)], ..Default::default() })), @@ -10228,7 +10329,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "endpointless-gating".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), merge_operations: vec![add_bound_rule(&opted_in)], ..Default::default() })), @@ -10261,7 +10364,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "signing-no-source".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(test_sigv4_policy("s3.amazonaws.com", None)), ..Default::default() })), @@ -10307,7 +10412,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "signing-unbound-aws".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(test_sigv4_policy("s3.amazonaws.com", None)), ..Default::default() })), @@ -10344,7 +10451,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "signing-bound-aws".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(test_sigv4_policy("s3.amazonaws.com", Some("aws-prod"))), ..Default::default() })), @@ -10388,7 +10497,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "signing-profile-endpoint".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(policy), ..Default::default() })), @@ -10418,7 +10529,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "signing-profile-mismatch".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(test_sigv4_policy("api.example.com", None)), ..Default::default() })), @@ -10526,7 +10639,9 @@ mod tests { sandbox_name: "provider-ambiguity".to_string(), provider_name: "candidate-provider".to_string(), expected_resource_version: 0, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11153,7 +11268,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "policy-binding".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(next_policy.clone()), ..Default::default() })), @@ -11208,7 +11325,9 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "policy-binding".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), policy: Some(unbound_policy), ..Default::default() })), @@ -11994,7 +12113,9 @@ mod tests { sandbox_name: "attach-lifecycle".to_string(), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -12032,7 +12153,9 @@ mod tests { sandbox_name: "attach-lifecycle".to_string(), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12164,7 +12287,9 @@ mod tests { sandbox_name: "attach-lifecycle".to_string(), provider_name: "work-custom".to_string(), expected_resource_version: 0, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -12205,7 +12330,9 @@ mod tests { sandbox_name: "attach-lifecycle".to_string(), provider_name: "work-custom".to_string(), expected_resource_version: 0, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12512,7 +12639,9 @@ mod tests { &state, with_user(Request::new(ApproveAllDraftChunksRequest { name: sandbox_name.to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), approvals: chunks .iter() .map(|chunk| openshell_core::proto::DraftChunkApproval { @@ -12639,7 +12768,9 @@ mod tests { &state, with_user(Request::new(ApproveAllDraftChunksRequest { name: sandbox_name.to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), approvals: chunks .iter() .map(|chunk| openshell_core::proto::DraftChunkApproval { @@ -12841,7 +12972,9 @@ mod tests { with_user(Request::new(ApproveAllDraftChunksRequest { name: sandbox_name.to_string(), include_security_flagged: false, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), approvals: vec![openshell_core::proto::DraftChunkApproval { chunk_id: chunk_id.clone(), review_token: chunk.review_token.clone(), @@ -12869,7 +13002,9 @@ mod tests { with_user(Request::new(ApproveAllDraftChunksRequest { name: sandbox_name.to_string(), include_security_flagged: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), approvals: vec![openshell_core::proto::DraftChunkApproval { chunk_id: chunk_id.clone(), review_token: chunk.review_token.clone(), @@ -12947,7 +13082,9 @@ mod tests { name: sandbox_name.to_string(), chunk_id: chunk_id.clone(), proposed_rule: Some(private_rule), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -12966,7 +13103,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.to_string(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -12983,7 +13122,9 @@ mod tests { with_user(Request::new(ApproveAllDraftChunksRequest { name: sandbox_name.to_string(), include_security_flagged: false, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -13049,7 +13190,9 @@ mod tests { with_user(Request::new(ApproveAllDraftChunksRequest { name: sandbox_name.to_string(), include_security_flagged: false, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -13119,7 +13262,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.to_string(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -13233,7 +13378,9 @@ mod tests { name: sandbox_name.to_string(), chunk_id: chunk_id.clone(), proposed_rule: Some(finding_rule), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -13366,7 +13513,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.clone(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -13386,7 +13535,9 @@ mod tests { authed_request(ApproveDraftChunkRequest { name: sandbox_name.clone(), chunk_id: chunk_id.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), review_token, }), ) @@ -13400,7 +13551,9 @@ mod tests { &state, authed_request(GetDraftHistoryRequest { name: sandbox_name.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13418,7 +13571,9 @@ mod tests { limit: 10, offset: 0, global: false, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13432,7 +13587,9 @@ mod tests { authed_request(UndoDraftChunkRequest { name: sandbox_name.clone(), chunk_id: chunk_id.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13446,7 +13603,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.clone(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -13459,7 +13618,9 @@ mod tests { &state, authed_request(GetDraftHistoryRequest { name: sandbox_name.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13475,7 +13636,9 @@ mod tests { limit: 10, offset: 0, global: false, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13489,7 +13652,9 @@ mod tests { &state, authed_request(ClearDraftChunksRequest { name: sandbox_name.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13502,7 +13667,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.clone(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -13514,7 +13681,9 @@ mod tests { &state, authed_request(GetDraftHistoryRequest { name: sandbox_name, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13591,7 +13760,9 @@ mod tests { name: sandbox_name.clone(), chunk_id: chunk_id.clone(), reason: guidance.to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13602,7 +13773,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -13709,7 +13882,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -13822,7 +13997,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.clone(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -13898,7 +14075,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -14028,7 +14207,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -14132,7 +14313,9 @@ mod tests { &state, with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -14255,7 +14438,9 @@ mod tests { &state, with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -14386,7 +14571,9 @@ mod tests { with_user(Request::new(ApproveDraftChunkRequest { name: sandbox_name, chunk_id: chunk_id.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), review_token: before.review_token.clone(), })), ) @@ -14570,7 +14757,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -14674,7 +14863,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -14771,7 +14962,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -14859,7 +15052,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -14951,7 +15146,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -15046,7 +15243,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -15220,7 +15419,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.to_string(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -15293,7 +15494,9 @@ mod tests { with_user(Request::new(ApproveDraftChunkRequest { name: sandbox_name.to_string(), chunk_id: chunk.id.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -15404,7 +15607,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -15504,7 +15709,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -15593,7 +15800,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -15753,7 +15962,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.clone(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -15783,7 +15994,9 @@ mod tests { authed_request(ApproveDraftChunkRequest { name: sandbox_name, chunk_id, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), review_token: chunk.review_token.clone(), }), ) @@ -15971,7 +16184,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -16122,7 +16337,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.clone(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -16144,7 +16361,9 @@ mod tests { name: sandbox_name, chunk_id: second.accepted_chunk_ids[0].clone(), reason: "redraft test".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -16228,7 +16447,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -16344,7 +16565,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.clone(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -16365,7 +16588,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name.clone(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -16608,7 +16833,9 @@ mod tests { name: sandbox_name.clone(), chunk_id: chunk_id.clone(), reason: "scope too broad".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -16619,7 +16846,9 @@ mod tests { authed_request(ApproveDraftChunkRequest { name: sandbox_name.clone(), chunk_id: chunk_id.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), review_token, }), ) @@ -16631,7 +16860,9 @@ mod tests { authed_request(UndoDraftChunkRequest { name: sandbox_name.clone(), chunk_id: chunk_id.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -16642,7 +16873,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -16754,7 +16987,9 @@ mod tests { with_user(Request::new(GetDraftPolicyRequest { name: sandbox_a.object_name().to_string(), status_filter: String::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), })), ) .await @@ -16769,7 +17004,9 @@ mod tests { authed_request(ApproveDraftChunkRequest { name: other_name.clone(), chunk_id: chunk_id.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), review_token: String::new(), }), ) @@ -16783,7 +17020,9 @@ mod tests { name: other_name.clone(), chunk_id: chunk_id.clone(), reason: "wrong sandbox".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -16796,7 +17035,9 @@ mod tests { name: other_name.clone(), chunk_id: chunk_id.clone(), proposed_rule: Some(proposed_rule.clone()), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -16808,7 +17049,9 @@ mod tests { authed_request(ApproveDraftChunkRequest { name: sandbox_a.object_name().to_string(), chunk_id: chunk_id.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), review_token, }), ) @@ -16820,7 +17063,9 @@ mod tests { authed_request(UndoDraftChunkRequest { name: other_name, chunk_id, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -18926,7 +19171,9 @@ mod tests { merge_operations: vec![], expected_resource_version: current_version, annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -19022,7 +19269,9 @@ mod tests { merge_operations: vec![], expected_resource_version: current_version, annotations: annotations.clone(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -19095,6 +19344,7 @@ mod tests { "openshell.nvidia.com/policy-signature".to_string(), "same-hash-signature".to_string(), )]), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -19178,6 +19428,7 @@ mod tests { name: "idempotent-provenance".to_string(), policy: Some(policy.clone()), annotations: annotations.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19190,6 +19441,7 @@ mod tests { name: "idempotent-provenance".to_string(), policy: Some(policy), annotations: annotations.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19243,6 +19495,7 @@ mod tests { with_user(Request::new(UpdateConfigRequest { name: "preserve-full".to_string(), policy: Some(updated), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19306,6 +19559,7 @@ mod tests { }, )), }], + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19378,6 +19632,7 @@ mod tests { )), }], annotations: provenance.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19439,6 +19694,7 @@ mod tests { name: "preserve-backfill".to_string(), policy: Some(ProtoSandboxPolicy::default()), expected_resource_version: current_version, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -19513,7 +19769,9 @@ mod tests { name: sandbox_name.to_string(), policy: Some(unsafe_replacement), expected_resource_version: current_version, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -19589,7 +19847,9 @@ mod tests { name: sandbox_name.to_string(), policy: Some(mcp_policy_with_versions(versions)), expected_resource_version: current_version, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -19671,7 +19931,9 @@ mod tests { name: sandbox_name.clone(), policy: Some(policy), expected_resource_version: current_version, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -19760,7 +20022,9 @@ mod tests { "2025-03-26", ])), expected_resource_version: current_version, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() })), ) @@ -19835,6 +20099,7 @@ mod tests { name: "invalid-annotation".to_string(), 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() })), ) @@ -19867,6 +20132,7 @@ mod tests { "_provider_work_github", "api.github.com", )), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() })), ) @@ -19933,6 +20199,7 @@ mod tests { name: "sync-strip".to_string(), policy: Some(synced_policy), expected_resource_version: current_version, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), "sb-sync-strip", @@ -20034,7 +20301,9 @@ mod tests { merge_operations: vec![], expected_resource_version: 99, // stale version annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -20133,7 +20402,9 @@ mod tests { merge_operations: vec![], expected_resource_version: initial_version, annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -20215,7 +20486,7 @@ mod tests { let err = handle_get_sandbox_policy_status( &state, non_member_request(GetSandboxPolicyStatusRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20231,7 +20502,7 @@ mod tests { let err = handle_list_sandbox_policies( &state, non_member_request(ListSandboxPoliciesRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20247,7 +20518,7 @@ mod tests { let err = handle_update_config( &state, non_member_request(UpdateConfigRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20263,7 +20534,7 @@ mod tests { let err = handle_get_draft_policy( &state, non_member_request(GetDraftPolicyRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20279,7 +20550,7 @@ mod tests { let err = handle_approve_draft_chunk( &state, non_member_request(ApproveDraftChunkRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20295,7 +20566,7 @@ mod tests { let err = handle_reject_draft_chunk( &state, non_member_request(RejectDraftChunkRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20311,7 +20582,7 @@ mod tests { let err = handle_approve_all_draft_chunks( &state, non_member_request(ApproveAllDraftChunksRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20327,7 +20598,7 @@ mod tests { let err = handle_edit_draft_chunk( &state, non_member_request(EditDraftChunkRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20343,7 +20614,7 @@ mod tests { let err = handle_undo_draft_chunk( &state, non_member_request(UndoDraftChunkRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20359,7 +20630,7 @@ mod tests { let err = handle_clear_draft_chunks( &state, non_member_request(ClearDraftChunksRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20375,7 +20646,7 @@ mod tests { let err = handle_get_draft_history( &state, non_member_request(GetDraftHistoryRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -20389,9 +20660,8 @@ mod tests { ); } - /// ID-based policy 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). + /// ID-only policy handlers hide cross-workspace resources, while requests + /// with an explicit workspace selector authorize that selector first. #[tokio::test] async fn id_based_policy_handlers_hide_cross_workspace_sandboxes() { let mut state = test_server_state().await; @@ -20440,6 +20710,7 @@ mod tests { &state, non_member_request(GetSandboxLogsRequest { sandbox_id: "sandbox-other".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -20447,8 +20718,8 @@ mod tests { .unwrap_err(); assert_eq!( err.code(), - Code::NotFound, - "handle_get_sandbox_logs must return NotFound, not PermissionDenied" + Code::PermissionDenied, + "handle_get_sandbox_logs must authorize the selected workspace before lookup" ); } diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 0039872afd..46eb9ce9be 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -2350,7 +2350,10 @@ use std::sync::{Arc, LazyLock, RwLock}; use tonic::{Request, Response}; use crate::auth::principal::Principal; -use crate::auth::workspace_authz::{MinWorkspaceRole, authorize_workspace, require_platform_admin}; +use crate::auth::workspace_authz::{ + AuthorizedWorkspaceScope, MinWorkspaceRole, authorize_list_workspace_selector, + authorize_workspace, authorize_workspace_selector, require_platform_admin, +}; use openshell_core::oauth::{ self, TokenExchangeParams, effective_client_assertion_type, effective_token_type, }; @@ -2468,11 +2471,11 @@ pub(super) async fn handle_create_provider( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -2542,11 +2545,11 @@ pub(super) async fn handle_get_provider( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -2566,15 +2569,17 @@ pub(super) async fn handle_list_providers( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - if request.all_workspaces && !request.workspace.is_empty() { - return Err(Status::invalid_argument( - "all_workspaces and workspace are mutually exclusive", - )); - } let limit = clamp_limit(request.limit, 100, MAX_PAGE_SIZE); - let providers = if request.all_workspaces { - require_platform_admin(&state.admin_role, &principal)?; + let scope = authorize_list_workspace_selector( + &state.store, + &state.admin_role, + &principal, + request.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; + let providers = if matches!(scope, AuthorizedWorkspaceScope::AllWorkspaces) { let all: Vec = state .store .list_all_messages(limit, request.offset) @@ -2582,14 +2587,9 @@ pub(super) async fn handle_list_providers( .map_err(|e| Status::internal(format!("list providers failed: {e}")))?; all.into_iter().map(redact_provider_credentials).collect() } else { - let authz = authorize_workspace( - &state.store, - &state.admin_role, - &principal, - &request.workspace, - MinWorkspaceRole::User, - ) - .await?; + let AuthorizedWorkspaceScope::Workspace(authz) = scope else { + unreachable!("all-workspaces scope handled above") + }; let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) .await? .name; @@ -3697,11 +3697,11 @@ pub(super) async fn handle_update_provider( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -4228,11 +4228,11 @@ pub(super) async fn handle_get_provider_refresh_status( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -4281,11 +4281,11 @@ pub(super) async fn handle_configure_provider_refresh( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -4678,11 +4678,11 @@ pub(super) async fn handle_rotate_provider_credential( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -4748,11 +4748,11 @@ pub(super) async fn handle_delete_provider_refresh( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -4826,11 +4826,11 @@ pub(super) async fn handle_delete_provider( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -6568,7 +6568,9 @@ mod tests { sandbox_name: "sandbox-custom".to_string(), provider_name: "custom-provider".to_string(), expected_resource_version: 0, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6663,7 +6665,9 @@ mod tests { // credential storage by omitting this advisory list. secret_material_keys: Vec::new(), expires_at_ms: Some(expires_at_ms), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6678,7 +6682,9 @@ mod tests { authed_request(GetProviderRefreshStatusRequest { provider: "msgraph".to_string(), credential_key: "MS_GRAPH_ACCESS_TOKEN".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6745,7 +6751,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: Some(expires_at_ms), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6770,7 +6778,9 @@ mod tests { authed_request(DeleteProviderRefreshRequest { provider: "msgraph".to_string(), credential_key: "MS_GRAPH_ACCESS_TOKEN".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6784,7 +6794,9 @@ mod tests { authed_request(GetProviderRefreshStatusRequest { provider: "msgraph".to_string(), credential_key: "MS_GRAPH_ACCESS_TOKEN".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6836,7 +6848,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }; handle_configure_provider_refresh(&state, authed_request(request("original-secret"))) .await @@ -6956,7 +6970,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }; let (first_store_hit, release_first_store) = first_state.credentials.gate_next_store(); @@ -7081,7 +7097,9 @@ mod tests { material: HashMap::new(), secret_material_keys: Vec::new(), expires_at_ms: Some(expires_at_ms), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7156,7 +7174,9 @@ mod tests { authed_request(DeleteProviderRefreshRequest { provider: "provider-a".to_string(), credential_key: "REFRESH_TOKEN".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7225,7 +7245,9 @@ mod tests { ]), secret_material_keys: vec!["private_key".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7291,7 +7313,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: Some(refresh_expires_at_ms), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7331,7 +7355,9 @@ mod tests { authed_request(DeleteProviderRefreshRequest { provider: "msgraph".to_string(), credential_key: "MS_GRAPH_ACCESS_TOKEN".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7394,7 +7420,9 @@ mod tests { )]), secret_material_keys: Vec::new(), expires_at_ms: Some(refresh_expires_at_ms), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7437,7 +7465,9 @@ mod tests { authed_request(DeleteProviderRefreshRequest { provider: "aws-delete".to_string(), credential_key: "AWS_ACCESS_KEY_ID".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7615,7 +7645,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7695,7 +7727,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7714,7 +7748,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7777,7 +7813,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7794,7 +7832,9 @@ mod tests { material: HashMap::from([("tenant_id".to_string(), "tenant".to_string())]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7848,7 +7888,9 @@ mod tests { material: HashMap::new(), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -7971,7 +8013,9 @@ mod tests { &task_state, authed_request(CreateProviderRequest { provider: Some(provider), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -8405,7 +8449,9 @@ mod tests { "openai", "OPENAI_API_KEY", )), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -8422,7 +8468,9 @@ mod tests { &state, authed_request(CreateProviderRequest { provider: Some(provider_with_values("legacy-gitlab", "gitlab")), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -8451,7 +8499,9 @@ mod tests { profile_workspace: "default".to_string(), ..Default::default() }), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -8493,7 +8543,9 @@ mod tests { profile_workspace: "default".to_string(), ..Default::default() }), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -8549,7 +8601,9 @@ mod tests { "GITHUB_TOKEN", "test-token", )), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -8595,7 +8649,9 @@ mod tests { "OPENAI_API_KEY", "sk-test", )), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -8710,7 +8766,9 @@ mod tests { "subject_token", "test-token", )), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -8746,7 +8804,9 @@ mod tests { "OPENAI_API_KEY", )), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -9708,7 +9768,9 @@ mod tests { ]), secret_material_keys: vec!["client_secret".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }; handle_configure_provider_refresh(&state, authed_request(configure())) .await @@ -9773,7 +9835,9 @@ mod tests { ..Default::default() }), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11587,7 +11651,9 @@ mod tests { "OPENAI_API_KEY", "sk-test", )), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11611,7 +11677,9 @@ mod tests { authed_request(UpdateProviderRequest { provider: Some(update), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11633,7 +11701,9 @@ mod tests { "OPENAI_API_KEY", "sk-test", )), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11656,7 +11726,9 @@ mod tests { authed_request(UpdateProviderRequest { provider: Some(update), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11678,7 +11750,9 @@ mod tests { &state, authed_request(CreateProviderRequest { provider: Some(provider.clone()), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11707,7 +11781,9 @@ mod tests { authed_request(UpdateProviderRequest { provider: Some(updated_provider.clone()), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11744,7 +11820,9 @@ mod tests { &state, authed_request(CreateProviderRequest { provider: Some(provider.clone()), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11773,7 +11851,9 @@ mod tests { authed_request(UpdateProviderRequest { provider: Some(stale_provider), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11822,7 +11902,9 @@ mod tests { "OPENAI_API_KEY", "sk-first", )), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11846,7 +11928,9 @@ mod tests { authed_request(UpdateProviderRequest { provider: Some(stale_provider), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11886,7 +11970,9 @@ mod tests { &state, authed_request(CreateProviderRequest { provider: Some(provider.clone()), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -11918,7 +12004,9 @@ mod tests { authed_request(UpdateProviderRequest { provider: Some(updated), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12012,7 +12100,9 @@ mod tests { )]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12074,7 +12164,9 @@ mod tests { ]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12148,7 +12240,9 @@ mod tests { ]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12204,7 +12298,9 @@ mod tests { ]), secret_material_keys: vec!["aws_session_token".to_string()], expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12254,7 +12350,9 @@ mod tests { )]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12332,7 +12430,9 @@ mod tests { )]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12356,7 +12456,9 @@ mod tests { ..Default::default() }), credential_expires_at_ms: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12422,7 +12524,9 @@ mod tests { )]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12474,7 +12578,9 @@ mod tests { )]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12652,7 +12758,9 @@ mod tests { )]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12727,7 +12835,9 @@ mod tests { )]), secret_material_keys: Vec::new(), expires_at_ms: None, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }) }; @@ -12930,12 +13040,14 @@ mod tests { labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), - workspace: String::new(), + workspace: "default".to_string(), deletion_timestamp_ms: 0, }); p }), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12960,12 +13072,14 @@ mod tests { labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), - workspace: String::new(), + workspace: "default".to_string(), deletion_timestamp_ms: 0, }); p }), - workspace: "beta".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -12985,7 +13099,9 @@ mod tests { &state, authed_request(GetProviderRequest { name: "shared-name".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -12997,7 +13113,9 @@ mod tests { &state, authed_request(GetProviderRequest { name: "shared-name".to_string(), - workspace: "beta".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -13011,8 +13129,9 @@ mod tests { authed_request(ListProvidersRequest { limit: 100, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13026,8 +13145,9 @@ mod tests { authed_request(ListProvidersRequest { limit: 100, offset: 0, - workspace: "beta".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -13041,7 +13161,9 @@ mod tests { &state, authed_request(DeleteProviderRequest { name: "shared-name".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13054,8 +13176,9 @@ mod tests { authed_request(ListProvidersRequest { limit: 100, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13067,7 +13190,9 @@ mod tests { &state, authed_request(GetProviderRequest { name: "shared-name".to_string(), - workspace: "beta".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -13089,12 +13214,14 @@ mod tests { labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), - workspace: String::new(), + workspace: "default".to_string(), deletion_timestamp_ms: 0, }); p }), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -13105,28 +13232,13 @@ mod tests { authed_request(ListProvidersRequest { limit: 100, offset: 0, - workspace: String::new(), - all_workspaces: true, + workspace_scope: Some(openshell_core::proto::all_workspaces_selector()), }), ) .await .unwrap() .into_inner(); assert_eq!(listed.providers.len(), 2); - - // all_workspaces with non-empty workspace is rejected. - let err = handle_list_providers( - &state, - authed_request(ListProvidersRequest { - limit: 100, - offset: 0, - workspace: "default".to_string(), - all_workspaces: true, - }), - ) - .await - .unwrap_err(); - assert_eq!(err.code(), Code::InvalidArgument); } #[tokio::test] @@ -13938,7 +14050,7 @@ mod tests { let err = handle_create_provider( &state, non_member_request(CreateProviderRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -13953,7 +14065,7 @@ mod tests { let err = handle_get_provider( &state, non_member_request(GetProviderRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -13968,7 +14080,7 @@ mod tests { let err = handle_list_providers( &state, non_member_request(ListProvidersRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -13983,7 +14095,7 @@ mod tests { let err = handle_update_provider( &state, non_member_request(UpdateProviderRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -13998,7 +14110,7 @@ mod tests { let err = handle_get_provider_refresh_status( &state, non_member_request(GetProviderRefreshStatusRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -14013,7 +14125,7 @@ mod tests { let err = handle_configure_provider_refresh( &state, non_member_request(ConfigureProviderRefreshRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -14028,7 +14140,7 @@ mod tests { let err = handle_rotate_provider_credential( &state, non_member_request(RotateProviderCredentialRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -14043,7 +14155,7 @@ mod tests { let err = handle_delete_provider_refresh( &state, non_member_request(DeleteProviderRefreshRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -14058,7 +14170,7 @@ mod tests { let err = handle_delete_provider( &state, non_member_request(DeleteProviderRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index e0ff130ebc..17ae70d2fb 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -11,7 +11,8 @@ use crate::ServerState; use crate::auth::workspace_authz::{ - MinWorkspaceRole, authorize_sandbox_workspace, authorize_workspace, require_platform_admin, + AuthorizedWorkspaceScope, MinWorkspaceRole, authorize_list_workspace_selector, + authorize_sandbox_workspace, authorize_workspace_selector, }; use crate::persistence::{ObjectLabels, ObjectType, WriteCondition, generate_name}; use futures::future; @@ -193,11 +194,11 @@ pub(super) async fn handle_begin_rootfs_tar_staging( let principal = super::extract_principal(&request)?; let request = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -330,11 +331,11 @@ async fn handle_create_sandbox_inner( validate_create_sandbox_request_pre_io(&request, &workload_template_name)?; - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -665,11 +666,11 @@ pub(super) async fn handle_get_sandbox( if req.name.is_empty() { return Err(Status::invalid_argument("name is required")); } - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -695,15 +696,17 @@ pub(super) async fn handle_list_sandboxes( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - if request.all_workspaces && !request.workspace.is_empty() { - return Err(Status::invalid_argument( - "all_workspaces and workspace are mutually exclusive", - )); - } let limit = clamp_limit(request.limit, 100, MAX_PAGE_SIZE); - let sandboxes: Vec = if request.all_workspaces { - require_platform_admin(&state.admin_role, &principal)?; + let scope = authorize_list_workspace_selector( + &state.store, + &state.admin_role, + &principal, + request.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; + let sandboxes: Vec = if matches!(scope, AuthorizedWorkspaceScope::AllWorkspaces) { if request.label_selector.is_empty() { state .store @@ -719,14 +722,9 @@ pub(super) async fn handle_list_sandboxes( .map_err(|e| Status::internal(format!("list sandboxes failed: {e}")))? } } else { - let authz = authorize_workspace( - &state.store, - &state.admin_role, - &principal, - &request.workspace, - MinWorkspaceRole::User, - ) - .await?; + let AuthorizedWorkspaceScope::Workspace(authz) = scope else { + unreachable!("all-workspaces scope handled above") + }; let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) .await? .name; @@ -766,11 +764,11 @@ pub(super) async fn handle_create_sandbox_template( .template .ok_or_else(|| Status::invalid_argument("template is required"))?; let metadata = template.metadata.clone().unwrap_or_default(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -856,11 +854,11 @@ pub(super) async fn handle_get_sandbox_template( if req.name.is_empty() { return Err(Status::invalid_argument("name is required")); } - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -884,14 +882,16 @@ pub(super) async fn handle_list_sandbox_templates( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); - if request.all_workspaces && !request.workspace.is_empty() { - return Err(Status::invalid_argument( - "all_workspaces and workspace are mutually exclusive", - )); - } let limit = clamp_limit(request.limit, 100, MAX_PAGE_SIZE); - let templates = if request.all_workspaces { - require_platform_admin(&state.admin_role, &principal)?; + let scope = authorize_list_workspace_selector( + &state.store, + &state.admin_role, + &principal, + request.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; + let templates = if matches!(scope, AuthorizedWorkspaceScope::AllWorkspaces) { if request.label_selector.is_empty() { state .store @@ -911,14 +911,9 @@ pub(super) async fn handle_list_sandbox_templates( .map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))? } } else { - let authz = authorize_workspace( - &state.store, - &state.admin_role, - &principal, - &request.workspace, - MinWorkspaceRole::User, - ) - .await?; + let AuthorizedWorkspaceScope::Workspace(authz) = scope else { + unreachable!("all-workspaces scope handled above") + }; let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) .await? .name; @@ -956,11 +951,11 @@ pub(super) async fn handle_delete_sandbox_template( if req.name.is_empty() { return Err(Status::invalid_argument("name is required")); } - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -1042,11 +1037,11 @@ 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( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -1064,11 +1059,11 @@ 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( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -1213,11 +1208,11 @@ 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( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -1333,11 +1328,11 @@ async fn handle_delete_sandbox_inner( if name.is_empty() { return Err(Status::invalid_argument("name is required")); } - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -1381,11 +1376,11 @@ async fn handle_stop_sandbox_inner( if req.name.is_empty() { return Err(Status::invalid_argument("name is required")); } - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -1425,11 +1420,11 @@ async fn handle_start_sandbox_inner( if req.name.is_empty() { return Err(Status::invalid_argument("name is required")); } - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -3164,6 +3159,7 @@ mod tests { ..SandboxSpec::default() }), workload_template_name: "gpu-kata".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..CreateSandboxRequest::default() }; let created = Sandbox { @@ -3201,6 +3197,7 @@ mod tests { ..SandboxSpec::default() }), workload_template_name: "missing-template".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..CreateSandboxRequest::default() }; @@ -3633,7 +3630,9 @@ mod tests { &delete_state, authed_request(DeleteSandboxRequest { name: "reused-name".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -3692,7 +3691,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3733,7 +3732,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3771,7 +3770,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3822,7 +3821,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3847,7 +3846,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "work-github".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3894,7 +3893,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "work-gcp".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3932,7 +3931,7 @@ mod tests { &state, authed_request(ListSandboxProvidersRequest { sandbox_name: "work".to_string(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -3962,7 +3961,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "missing".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -4137,7 +4136,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }), @@ -4162,7 +4161,7 @@ mod tests { spec: Some(SandboxSpec::default()), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }), @@ -4199,7 +4198,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }), @@ -4306,7 +4305,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -4363,7 +4362,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -4462,7 +4461,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -4530,7 +4529,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }), ) @@ -4563,7 +4562,7 @@ mod tests { spec: Some(SandboxSpec::default()), labels: HashMap::new(), annotations: HashMap::from([(annotation_key.clone(), annotation_value.clone())]), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }), @@ -4585,7 +4584,7 @@ mod tests { &state, authed_request(GetSandboxRequest { name: "annotated".to_string(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -4624,7 +4623,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }), @@ -4649,7 +4648,7 @@ mod tests { &state, authed_request(GetSandboxRequest { name: "partial-id".to_string(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -4689,7 +4688,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }), @@ -4721,7 +4720,7 @@ mod tests { spec: Some(SandboxSpec::default()), labels: HashMap::from([("team".to_string(), "x".repeat(512))]), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }), @@ -4755,7 +4754,7 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), await_main_process_attachment: false, workload_template_name: String::new(), }), @@ -4790,7 +4789,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(test_workload_template("gpu-kata")), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -4809,7 +4810,9 @@ mod tests { &state, authed_request(GetSandboxTemplateRequest { name: "gpu-kata".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -4825,8 +4828,9 @@ mod tests { authed_request(ListSandboxTemplatesRequest { limit: 100, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), label_selector: String::new(), }), ) @@ -4841,7 +4845,9 @@ mod tests { &state, authed_request(DeleteSandboxTemplateRequest { name: "gpu-kata".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -4853,7 +4859,9 @@ mod tests { &state, authed_request(GetSandboxTemplateRequest { name: "gpu-kata".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -4875,7 +4883,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(gpu), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -4891,7 +4901,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(cpu), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -4902,8 +4914,9 @@ mod tests { authed_request(ListSandboxTemplatesRequest { limit: 100, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), label_selector: "team=runtime".to_string(), }), ) @@ -4924,7 +4937,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(test_workload_template(" gpu-kata ")), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -4938,8 +4953,9 @@ mod tests { authed_request(ListSandboxTemplatesRequest { limit: 100, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), label_selector: String::new(), }), ) @@ -4972,7 +4988,7 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(template), - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -4986,8 +5002,9 @@ mod tests { authed_request(ListSandboxTemplatesRequest { limit: 100, offset: 0, - workspace: "beta".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), label_selector: String::new(), }), ) @@ -5008,7 +5025,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(template), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -5145,7 +5164,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(test_workload_template("overflow")), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -5174,7 +5195,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(test_workload_template(&format!("overflow-{index}"))), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -5272,7 +5295,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(test_workload_template("gpu-kata")), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -5303,7 +5328,9 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), workload_template_name: "gpu-kata".to_string(), await_main_process_attachment: false, }), @@ -5372,7 +5399,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(template), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -5385,7 +5414,9 @@ mod tests { spec: Some(SandboxSpec::default()), labels: HashMap::new(), annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), workload_template_name: "default-image".to_string(), await_main_process_attachment: false, }), @@ -5419,7 +5450,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(template), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -5432,7 +5465,9 @@ mod tests { spec: Some(SandboxSpec::default()), labels: HashMap::new(), annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), workload_template_name: "default-gpu".to_string(), await_main_process_attachment: false, }), @@ -5469,7 +5504,9 @@ mod tests { spec: Some(SandboxSpec::default()), labels: HashMap::new(), annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), workload_template_name: "corrupt-template".to_string(), await_main_process_attachment: false, }), @@ -5488,7 +5525,9 @@ mod tests { &state, authed_request(CreateSandboxTemplateRequest { template: Some(test_workload_template("gpu-kata")), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -5504,7 +5543,9 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), workload_template_name: "gpu-kata".to_string(), await_main_process_attachment: false, }), @@ -5527,7 +5568,9 @@ mod tests { spec: Some(SandboxSpec::default()), labels: HashMap::new(), annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), workload_template_name: "Invalid_Template_Name".to_string(), await_main_process_attachment: false, }), @@ -5553,7 +5596,9 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), workload_template_name: "missing-template".to_string(), await_main_process_attachment: false, }), @@ -5579,7 +5624,9 @@ mod tests { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: "missing-workspace".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "missing-workspace".to_string(), + )), workload_template_name: String::new(), await_main_process_attachment: false, }), @@ -5616,7 +5663,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "provider-b".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5663,7 +5710,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "provider-31".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5718,7 +5765,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "provider-32".to_string(), expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5765,7 +5812,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: long_name, expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5792,7 +5839,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: long_name, expected_resource_version: 0, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -5991,7 +6038,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "github".to_string(), expected_resource_version: current_version, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6043,7 +6090,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "github".to_string(), expected_resource_version: 99, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6106,7 +6153,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "github".to_string(), expected_resource_version: current_version, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6158,7 +6205,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: "github".to_string(), expected_resource_version: 99, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6239,7 +6286,7 @@ mod tests { sandbox_name: "work".to_string(), provider_name: format!("provider-{i}"), expected_resource_version: initial_version, - workspace: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), }), ) .await @@ -6323,7 +6370,9 @@ mod tests { &state, authed_request(GetSandboxRequest { name: "shared-name".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6336,7 +6385,9 @@ mod tests { &state, authed_request(GetSandboxRequest { name: "shared-name".to_string(), - workspace: "beta".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -6351,8 +6402,9 @@ mod tests { limit: 100, offset: 0, label_selector: String::new(), - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6368,8 +6420,9 @@ mod tests { limit: 100, offset: 0, label_selector: String::new(), - workspace: "beta".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -6392,8 +6445,9 @@ mod tests { limit: 100, offset: 0, label_selector: String::new(), - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -6406,7 +6460,9 @@ mod tests { &state, authed_request(GetSandboxRequest { name: "shared-name".to_string(), - workspace: "beta".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -6434,29 +6490,13 @@ mod tests { limit: 100, offset: 0, label_selector: String::new(), - workspace: String::new(), - all_workspaces: true, + workspace_scope: Some(openshell_core::proto::all_workspaces_selector()), }), ) .await .unwrap() .into_inner(); assert_eq!(listed.sandboxes.len(), 2); - - // all_workspaces with non-empty workspace is rejected. - let err = handle_list_sandboxes( - &state, - authed_request(ListSandboxesRequest { - limit: 100, - offset: 0, - label_selector: String::new(), - workspace: "default".to_string(), - all_workspaces: true, - }), - ) - .await - .unwrap_err(); - assert_eq!(err.code(), tonic::Code::InvalidArgument); } /// Non-members must receive `PERMISSION_DENIED` — never `NOT_FOUND` — when @@ -6492,7 +6532,7 @@ mod tests { let err = handle_create_sandbox( &state, non_member_request(CreateSandboxRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), spec: Some(SandboxSpec::default()), ..Default::default() }), @@ -6510,7 +6550,7 @@ mod tests { let err = handle_get_sandbox( &state, non_member_request(GetSandboxRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), name: "any".into(), }), ) @@ -6526,7 +6566,7 @@ mod tests { let err = handle_list_sandboxes( &state, non_member_request(ListSandboxesRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -6542,7 +6582,7 @@ mod tests { let err = handle_list_sandbox_providers( &state, non_member_request(ListSandboxProvidersRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -6558,7 +6598,7 @@ mod tests { let err = handle_attach_sandbox_provider( &state, non_member_request(AttachSandboxProviderRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -6574,7 +6614,7 @@ mod tests { let err = handle_detach_sandbox_provider( &state, non_member_request(DetachSandboxProviderRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -6591,7 +6631,7 @@ mod tests { let err = handle_delete_sandbox( &state, non_member_request(DeleteSandboxRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), name: "any".into(), }), ) @@ -6607,7 +6647,7 @@ mod tests { handle_stop_sandbox( &state, non_member_request(StopSandboxRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), name: "any".into(), }), ) @@ -6615,7 +6655,7 @@ mod tests { handle_start_sandbox( &state, non_member_request(StartSandboxRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), name: "any".into(), }), ) diff --git a/crates/openshell-server/src/grpc/service.rs b/crates/openshell-server/src/grpc/service.rs index 790e26d618..34c7d62d3f 100644 --- a/crates/openshell-server/src/grpc/service.rs +++ b/crates/openshell-server/src/grpc/service.rs @@ -15,7 +15,10 @@ use tonic::{Request, Response, Status}; use uuid::Uuid; use crate::ServerState; -use crate::auth::workspace_authz::{MinWorkspaceRole, authorize_workspace, require_platform_admin}; +use crate::auth::workspace_authz::{ + AuthorizedWorkspaceScope, MinWorkspaceRole, authorize_list_workspace_selector, + authorize_workspace_selector, +}; use crate::persistence::{ObjectType, WriteCondition}; use crate::service_routing; @@ -28,11 +31,11 @@ pub(super) async fn handle_expose_service( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -147,11 +150,11 @@ pub(super) async fn handle_get_service( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -174,54 +177,53 @@ pub(super) async fn handle_list_services( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - if req.all_workspaces && !req.workspace.is_empty() { - return Err(Status::invalid_argument( - "all_workspaces and workspace are mutually exclusive", - )); - } if !req.sandbox.is_empty() { validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?; } let limit = super::clamp_limit(req.limit, 100, super::MAX_PAGE_SIZE); - let endpoints: Vec = if req.all_workspaces { - require_platform_admin(&state.admin_role, &principal)?; - if !req.sandbox.is_empty() { - return Err(Status::invalid_argument( - "sandbox filter is not supported with all_workspaces", - )); - } - state.store.list_all_messages(limit, req.offset).await - } else { - let authz = authorize_workspace( - &state.store, - &state.admin_role, - &principal, - &req.workspace, - MinWorkspaceRole::User, - ) - .await?; - let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) - .await? - .name; - if req.sandbox.is_empty() { - state - .store - .list_messages(&workspace, limit, req.offset) - .await + let scope = authorize_list_workspace_selector( + &state.store, + &state.admin_role, + &principal, + req.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; + let endpoints: Vec = + if matches!(scope, AuthorizedWorkspaceScope::AllWorkspaces) { + if !req.sandbox.is_empty() { + return Err(Status::invalid_argument( + "sandbox filter is not supported with all_workspaces", + )); + } + state.store.list_all_messages(limit, req.offset).await } else { - state - .store - .list_messages_with_selector( - &workspace, - &format!("sandbox={}", req.sandbox), - limit, - req.offset, - ) - .await + let AuthorizedWorkspaceScope::Workspace(authz) = scope else { + unreachable!("all-workspaces scope handled above") + }; + let workspace = + super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) + .await? + .name; + if req.sandbox.is_empty() { + state + .store + .list_messages(&workspace, limit, req.offset) + .await + } else { + state + .store + .list_messages_with_selector( + &workspace, + &format!("sandbox={}", req.sandbox), + limit, + req.offset, + ) + .await + } } - } - .map_err(|e| Status::internal(format!("list endpoints failed: {e}")))?; + .map_err(|e| Status::internal(format!("list endpoints failed: {e}")))?; let services = endpoints .into_iter() @@ -237,11 +239,11 @@ pub(super) async fn handle_delete_service( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); - let authz = authorize_workspace( + let authz = authorize_workspace_selector( &state.store, &state.admin_role, &principal, - &req.workspace, + req.workspace_scope.as_ref(), MinWorkspaceRole::User, ) .await?; @@ -413,7 +415,9 @@ mod tests { service: "web".to_string(), target_port: 8080, domain: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -427,8 +431,9 @@ mod tests { sandbox: "my-sandbox".to_string(), limit: 0, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -445,7 +450,9 @@ mod tests { authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), service: "web".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -458,7 +465,9 @@ mod tests { authed_request(DeleteServiceRequest { sandbox: "my-sandbox".to_string(), service: "web".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -471,7 +480,9 @@ mod tests { authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), service: "web".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -484,8 +495,9 @@ mod tests { sandbox: "my-sandbox".to_string(), limit: 0, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -509,7 +521,9 @@ mod tests { service: "web".to_string(), target_port: 8080, domain: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -524,7 +538,9 @@ mod tests { service: "web".to_string(), target_port: 9090, domain: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -549,8 +565,9 @@ mod tests { sandbox: "my-sandbox".to_string(), limit: 0, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -572,7 +589,9 @@ mod tests { service: "web".to_string(), target_port: 7070, domain: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -588,7 +607,9 @@ mod tests { service: "web".to_string(), target_port: 8080, domain: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -603,7 +624,9 @@ mod tests { service: "web".to_string(), target_port: 9090, domain: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -626,7 +649,9 @@ mod tests { authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), service: "web".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -687,7 +712,9 @@ mod tests { service: "web".to_string(), target_port: 8080, domain: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -700,7 +727,9 @@ mod tests { service: "web".to_string(), target_port: 9090, domain: true, - workspace: "beta".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -712,7 +741,9 @@ mod tests { authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), service: "web".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -726,7 +757,9 @@ mod tests { authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), service: "web".to_string(), - workspace: "beta".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -741,8 +774,9 @@ mod tests { sandbox: "my-sandbox".to_string(), limit: 100, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -760,8 +794,9 @@ mod tests { sandbox: "my-sandbox".to_string(), limit: 100, offset: 0, - workspace: "beta".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -779,7 +814,9 @@ mod tests { authed_request(DeleteServiceRequest { sandbox: "my-sandbox".to_string(), service: "web".to_string(), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -793,8 +830,9 @@ mod tests { sandbox: "my-sandbox".to_string(), limit: 100, offset: 0, - workspace: "default".to_string(), - all_workspaces: false, + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -807,7 +845,9 @@ mod tests { authed_request(GetServiceRequest { sandbox: "my-sandbox".to_string(), service: "web".to_string(), - workspace: "beta".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "beta".to_string(), + )), }), ) .await @@ -824,7 +864,9 @@ mod tests { service: "api".to_string(), target_port: 3000, domain: true, - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), }), ) .await @@ -836,29 +878,13 @@ mod tests { sandbox: String::new(), limit: 100, offset: 0, - workspace: String::new(), - all_workspaces: true, + workspace_scope: Some(openshell_core::proto::all_workspaces_selector()), }), ) .await .unwrap() .into_inner(); assert_eq!(listed.services.len(), 2); - - // all_workspaces with non-empty workspace is rejected. - let err = handle_list_services( - &state, - authed_request(ListServicesRequest { - sandbox: String::new(), - limit: 100, - offset: 0, - workspace: "default".to_string(), - all_workspaces: true, - }), - ) - .await - .unwrap_err(); - assert_eq!(err.code(), tonic::Code::InvalidArgument); } /// Non-member callers must receive `PERMISSION_DENIED` — not `NOT_FOUND` — @@ -889,7 +915,7 @@ mod tests { let err = handle_expose_service( &state, non_member_request(ExposeServiceRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -905,7 +931,7 @@ mod tests { let err = handle_get_service( &state, non_member_request(GetServiceRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -921,7 +947,7 @@ mod tests { let err = handle_list_services( &state, non_member_request(ListServicesRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) @@ -937,7 +963,7 @@ mod tests { let err = handle_delete_service( &state, non_member_request(DeleteServiceRequest { - workspace: "no-such-ws".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")), ..Default::default() }), ) diff --git a/crates/openshell-server/src/grpc/workspace.rs b/crates/openshell-server/src/grpc/workspace.rs index f360cb6c1d..546934a6ed 100644 --- a/crates/openshell-server/src/grpc/workspace.rs +++ b/crates/openshell-server/src/grpc/workspace.rs @@ -70,7 +70,7 @@ fn membership_filter_subject<'a>( } } -fn validate_workspace_name(name: &str) -> Result<(), Status> { +pub fn validate_workspace_name(name: &str) -> Result<(), Status> { if name.is_empty() { return Err(Status::invalid_argument("workspace name is required")); } diff --git a/crates/openshell-server/src/multiplex.rs b/crates/openshell-server/src/multiplex.rs index 21315f9830..f03aa4d07f 100644 --- a/crates/openshell-server/src/multiplex.rs +++ b/crates/openshell-server/src/multiplex.rs @@ -48,7 +48,7 @@ use crate::{ auth::identity::Identity, auth::oidc::{self, OidcAuthenticator}, auth::principal::{Principal, UserPrincipal}, - auth::workspace_authz::{MinWorkspaceRole, authorize_workspace}, + auth::workspace_authz::{MinWorkspaceRole, authorize_workspace_selector}, gateway_listener::GatewayListenerScope, http_router, service_http_router, }; @@ -552,11 +552,11 @@ async fn hydrate_update_provider_identity( let principal = principal.ok_or_else(|| tonic::Status::unauthenticated("authentication required"))?; - let authorized = authorize_workspace( + let authorized = authorize_workspace_selector( state.store.as_ref(), &state.admin_role, principal, - &request.workspace, + request.workspace_scope.as_ref(), MinWorkspaceRole::Admin, ) .await?; @@ -1740,7 +1740,9 @@ mod tests { )]), ..Default::default() }), - workspace: "default".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + "default".to_string(), + )), ..Default::default() }; let authed = crate::grpc::test_support::authed_request(()); diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 0d6d33c01f..cd5e065dee 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 = - "042034fe4d0000279ee4ed27e587ab8e530934b8d5c3aa36dc9769f81dfa6e51"; + "c95ae90962c10fb28747db2b645adf4044562a3d208d84dfe4699d677e4364ee"; const DURABLE_SCHEMA_SHA256: &str = "920a5243dfb37ce709f0f562a47d17791a5ede90fd7f662ed01542abd60a0dfb"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = @@ -487,7 +487,7 @@ mod tests { assert_eq!( (public_closure.messages.len(), public_closure.enums.len()), - (276, 12) + (278, 12) ); assert_eq!( (durable_closure.messages.len(), durable_closure.enums.len()), diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 174f9910d0..2ab19d89d7 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -40,6 +40,21 @@ const PROVIDER_PROFILE_PAGE_SIZE: u32 = 100; type ProviderProfileCache = HashMap<(String, String), openshell_core::proto::ProviderProfile>; +fn named_workspace_scope(workspace: impl Into) -> openshell_core::proto::WorkspaceSelector { + openshell_core::proto::workspace_selector(workspace) +} + +fn list_workspace_scope( + workspace: impl Into, + all_workspaces: bool, +) -> openshell_core::proto::WorkspaceSelector { + if all_workspaces { + openshell_core::proto::all_workspaces_selector() + } else { + openshell_core::proto::workspace_selector(workspace) + } +} + // Re-export for use by the CLI crate. pub use theme::ThemeMode; @@ -648,7 +663,7 @@ fn spawn_log_stream(app: &mut App, tx: mpsc::UnboundedSender) { since_ms: 0, sources: vec![], min_level: String::new(), - workspace, + workspace_scope: Some(named_workspace_scope(workspace)), }; match tokio::time::timeout(Duration::from_secs(5), client.get_sandbox_logs(req)).await { @@ -751,7 +766,7 @@ async fn handle_sandbox_delete(app: &mut App) { let req = openshell_core::proto::DeleteSandboxRequest { name: sandbox_name, - workspace: app.selected_sandbox_workspace(), + workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())), }; match app.client.delete_sandbox(req).await { Ok(_) => { @@ -784,7 +799,7 @@ async fn fetch_sandbox_detail(app: &mut App) { let req = openshell_core::proto::GetSandboxRequest { name: sandbox_name.clone(), - workspace: app.selected_sandbox_workspace(), + workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())), }; // Step 1: Fetch sandbox metadata (providers, sandbox ID). @@ -870,7 +885,7 @@ async fn handle_shell_connect( let sandbox_id = { let req = openshell_core::proto::GetSandboxRequest { name: sandbox_name.clone(), - workspace: app.selected_sandbox_workspace(), + 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)) => { @@ -1027,7 +1042,7 @@ async fn handle_exec_command( let sandbox_id = { let req = openshell_core::proto::GetSandboxRequest { name: sandbox_name.to_string(), - workspace: workspace.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)) => { @@ -1403,7 +1418,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { }), labels: HashMap::new(), annotations: HashMap::new(), - workspace: workspace.clone(), + workspace_scope: Some(named_workspace_scope(&workspace)), await_main_process_attachment: false, workload_template_name: String::new(), }; @@ -1446,7 +1461,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { let req = openshell_core::proto::GetSandboxRequest { name: sandbox_name.clone(), - workspace: workspace.clone(), + workspace_scope: Some(named_workspace_scope(&workspace)), }; // Retry on transient errors. if let Ok(resp) = client.get_sandbox(req).await @@ -1688,7 +1703,7 @@ fn spawn_create_provider(app: &App, tx: mpsc::UnboundedSender) { profile_workspace: workspace.clone(), credential_handles: HashMap::default(), }), - workspace: workspace.clone(), + workspace_scope: Some(named_workspace_scope(&workspace)), }; match client.create_provider(req).await { @@ -1729,7 +1744,10 @@ fn spawn_get_provider(app: &App, tx: mpsc::UnboundedSender) { let workspace = app.selected_provider_workspace(); tokio::spawn(async move { - let req = openshell_core::proto::GetProviderRequest { name, workspace }; + let req = openshell_core::proto::GetProviderRequest { + name, + workspace_scope: Some(named_workspace_scope(workspace)), + }; match tokio::time::timeout(Duration::from_secs(5), client.get_provider(req)).await { Ok(Ok(resp)) => { if let Some(provider) = resp.into_inner().provider { @@ -1803,7 +1821,7 @@ fn spawn_update_provider(app: &App, tx: mpsc::UnboundedSender) { credential_handles: HashMap::default(), }), credential_expires_at_ms: HashMap::default(), - workspace, + workspace_scope: Some(named_workspace_scope(workspace)), }; match tokio::time::timeout(Duration::from_secs(5), client.update_provider(req)).await { @@ -1832,7 +1850,10 @@ fn spawn_delete_provider(app: &App, tx: mpsc::UnboundedSender) { let workspace = app.selected_provider_workspace(); tokio::spawn(async move { - let req = openshell_core::proto::DeleteProviderRequest { name, workspace }; + let req = openshell_core::proto::DeleteProviderRequest { + name, + workspace_scope: Some(named_workspace_scope(workspace)), + }; match tokio::time::timeout(Duration::from_secs(5), client.delete_provider(req)).await { Ok(Ok(resp)) => { let _ = tx.send(Event::ProviderDeleteResult(Ok(resp.into_inner().deleted))); @@ -1875,7 +1896,7 @@ fn spawn_draft_approve(app: &App, tx: mpsc::UnboundedSender) { let req = openshell_core::proto::ApproveDraftChunkRequest { name, chunk_id, - workspace, + workspace_scope: Some(named_workspace_scope(workspace)), review_token, }; match tokio::time::timeout(Duration::from_secs(5), client.approve_draft_chunk(req)).await { @@ -1921,7 +1942,7 @@ fn spawn_draft_reject(app: &App, tx: mpsc::UnboundedSender) { name, chunk_id, reason: String::new(), - workspace, + workspace_scope: Some(named_workspace_scope(workspace)), }; match tokio::time::timeout(Duration::from_secs(5), client.reject_draft_chunk(req)).await { Ok(Ok(_)) => { @@ -1970,7 +1991,7 @@ fn spawn_draft_approve_all( let req = openshell_core::proto::ApproveAllDraftChunksRequest { name, include_security_flagged: false, - workspace, + workspace_scope: Some(named_workspace_scope(workspace)), approvals, }; match tokio::time::timeout( @@ -2094,12 +2115,10 @@ async fn refresh_providers(app: &mut App) { let req = openshell_core::proto::ListProvidersRequest { limit: 100, offset: 0, - workspace: if app.all_workspaces { - String::new() - } else { - app.current_workspace.clone() - }, - all_workspaces: app.all_workspaces, + workspace_scope: Some(list_workspace_scope( + &app.current_workspace, + app.all_workspaces, + )), }; let response = match tokio::time::timeout(Duration::from_secs(5), app.client.list_providers(req)).await { @@ -2247,7 +2266,7 @@ async fn refresh_global_settings(app: &mut App) { limit: 1, offset: 0, global: true, - workspace: String::new(), + workspace_scope: None, }; match tokio::time::timeout( Duration::from_secs(5), @@ -2327,7 +2346,6 @@ fn spawn_set_global_setting(app: &App, tx: mpsc::UnboundedSender) { setting_key: key, setting_value: Some(SettingValue { value: Some(value) }), global: true, - workspace: String::new(), ..Default::default() }; @@ -2361,7 +2379,6 @@ fn spawn_delete_global_setting(app: &App, tx: mpsc::UnboundedSender) { setting_key: key, delete_setting: true, global: true, - workspace: String::new(), ..Default::default() }; @@ -2429,7 +2446,7 @@ fn spawn_set_sandbox_setting(app: &App, tx: mpsc::UnboundedSender) { name, setting_key: key, setting_value: Some(SettingValue { value: Some(value) }), - workspace, + workspace_scope: Some(named_workspace_scope(workspace)), ..Default::default() }; @@ -2467,7 +2484,7 @@ fn spawn_delete_sandbox_setting(app: &App, tx: mpsc::UnboundedSender) { name, setting_key: key, delete_setting: true, - workspace, + workspace_scope: Some(named_workspace_scope(workspace)), ..Default::default() }; @@ -2511,12 +2528,10 @@ async fn refresh_sandboxes(app: &mut App) { limit: 100, offset: 0, label_selector: String::new(), - workspace: if app.all_workspaces { - String::new() - } else { - app.current_workspace.clone() - }, - all_workspaces: app.all_workspaces, + workspace_scope: Some(list_workspace_scope( + &app.current_workspace, + app.all_workspaces, + )), }; let result = tokio::time::timeout(Duration::from_secs(5), app.client.list_sandboxes(req)).await; match result { @@ -2665,7 +2680,7 @@ async fn refresh_draft_chunks(app: &mut App) { let req = openshell_core::proto::GetDraftPolicyRequest { name: sandbox_name, status_filter: String::new(), - workspace: app.selected_sandbox_workspace(), + workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())), }; if let Ok(Ok(resp)) = @@ -2696,7 +2711,7 @@ async fn refresh_sandbox_draft_counts(app: &mut App) { let req = openshell_core::proto::GetDraftPolicyRequest { name: name.clone(), status_filter: "pending".to_string(), - workspace: ws, + workspace_scope: Some(named_workspace_scope(ws)), }; if let Ok(Ok(resp)) = tokio::time::timeout(Duration::from_secs(2), app.client.get_draft_policy(req)).await diff --git a/docs/sandboxes/manage-workspaces.mdx b/docs/sandboxes/manage-workspaces.mdx index 2212eb93e7..003201ac21 100644 --- a/docs/sandboxes/manage-workspaces.mdx +++ b/docs/sandboxes/manage-workspaces.mdx @@ -149,8 +149,9 @@ export OPENSHELL_WORKSPACE=team-ml openshell sandbox list ``` -An empty workspace value resolves to `default`. It never means all -workspaces. +When `--workspace` is omitted, the CLI intentionally selects the `default` +workspace. An empty workspace name is invalid and never means either `default` +or all workspaces. Platform Admins can opt into cross-workspace list operations: @@ -160,6 +161,29 @@ openshell provider list --all-workspaces openshell service list --all-workspaces ``` +The public API represents this choice with a `WorkspaceSelector` oneof. Set +`workspace` to a non-empty name, including the literal `default`, or set the +`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. + +Clients migrating from the previous request fields should make the scope +explicit: + +| Previous request | Typed selector | +| --- | --- | +| `workspace: "team-ml"` | `workspace_scope.workspace: "team-ml"` | +| Empty or omitted `workspace` for the default | `workspace_scope.workspace: "default"` | +| `all_workspaces: true` | `workspace_scope.all_workspaces: {}` | +| `global: true` | Omit `workspace_scope` | + +The Rust and Python SDKs expose separate all-workspaces list methods. The Go +SDK uses `ListAll`, and the TypeScript SDK uses a discriminated option type, so +a caller cannot select a named workspace and all workspaces in one typed call. +The TUI starts in the `default` workspace and sends that named selector +explicitly. Its all-workspaces view sends the marker instead. + Provider profiles and policy also have explicit `--global` operations. Those operations target platform scope and require Platform Admin access. A Workspace Admin should use `--workspace` for workspace-scoped profiles and diff --git a/e2e/python/oidc/oidc_auth_test.py b/e2e/python/oidc/oidc_auth_test.py index 39cd076af0..06b5199a6f 100644 --- a/e2e/python/oidc/oidc_auth_test.py +++ b/e2e/python/oidc/oidc_auth_test.py @@ -51,11 +51,12 @@ def test_admin_can_create_provider(self) -> None: token = get_token("admin@test", "admin", scopes="openid openshell:all") stub, metadata = stub_with_token(token) req = openshell_pb2.CreateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name="e2e-oidc-admin-test"), type="claude", credentials={"ANTHROPIC_API_KEY": "test-value"}, - ) + ), ) try: stub.CreateProvider(req, metadata=metadata) @@ -67,7 +68,12 @@ def test_admin_can_create_provider(self) -> None: finally: with contextlib.suppress(grpc.RpcError): stub.DeleteProvider( - openshell_pb2.DeleteProviderRequest(name="e2e-oidc-admin-test"), + openshell_pb2.DeleteProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace="default" + ), + name="e2e-oidc-admin-test", + ), metadata=metadata, ) @@ -75,11 +81,12 @@ def test_user_cannot_create_provider(self) -> None: token = get_token("user@test", "user", scopes="openid openshell:all") stub, metadata = stub_with_token(token) req = openshell_pb2.CreateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name="e2e-oidc-user-blocked"), type="claude", credentials={"ANTHROPIC_API_KEY": "test-value"}, - ) + ), ) with pytest.raises(grpc.RpcError) as exc_info: stub.CreateProvider(req, metadata=metadata) @@ -103,7 +110,10 @@ def test_user_can_list_sandboxes(self) -> None: ) try: user_stub.ListSandboxes( - openshell_pb2.ListSandboxesRequest(), metadata=user_md + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default") + ), + metadata=user_md, ) finally: with contextlib.suppress(grpc.RpcError): @@ -118,7 +128,11 @@ def test_request_without_bearer_token_rejected(self) -> None: channel = grpc_channel() stub = openshell_pb2_grpc.OpenShellStub(channel) with pytest.raises(grpc.RpcError) as exc_info: - stub.ListSandboxes(openshell_pb2.ListSandboxesRequest()) + stub.ListSandboxes( + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default") + ) + ) assert exc_info.value.code() in ( grpc.StatusCode.UNAUTHENTICATED, grpc.StatusCode.PERMISSION_DENIED, @@ -151,7 +165,12 @@ def test_sandbox_scoped_token_can_list_sandboxes(self) -> None: "admin@test", "admin", scopes="openid sandbox:read sandbox:write" ) stub, metadata = stub_with_token(token) - stub.ListSandboxes(openshell_pb2.ListSandboxesRequest(), metadata=metadata) + stub.ListSandboxes( + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default") + ), + metadata=metadata, + ) def test_sandbox_scoped_token_cannot_list_providers(self) -> None: token = get_token( @@ -159,21 +178,41 @@ def test_sandbox_scoped_token_cannot_list_providers(self) -> None: ) stub, metadata = stub_with_token(token) with pytest.raises(grpc.RpcError) as exc_info: - stub.ListProviders(openshell_pb2.ListProvidersRequest(), metadata=metadata) + stub.ListProviders( + openshell_pb2.ListProvidersRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default") + ), + metadata=metadata, + ) assert exc_info.value.code() == grpc.StatusCode.PERMISSION_DENIED assert "provider:read" in exc_info.value.details() def test_openshell_all_grants_full_access(self) -> None: token = get_token("admin@test", "admin", scopes="openid openshell:all") stub, metadata = stub_with_token(token) - stub.ListSandboxes(openshell_pb2.ListSandboxesRequest(), metadata=metadata) - stub.ListProviders(openshell_pb2.ListProvidersRequest(), metadata=metadata) + stub.ListSandboxes( + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default") + ), + metadata=metadata, + ) + stub.ListProviders( + openshell_pb2.ListProvidersRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default") + ), + metadata=metadata, + ) def test_no_openshell_scopes_denied(self) -> None: token = get_token("admin@test", "admin") stub, metadata = stub_with_token(token) with pytest.raises(grpc.RpcError) as exc_info: - stub.ListSandboxes(openshell_pb2.ListSandboxesRequest(), metadata=metadata) + stub.ListSandboxes( + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default") + ), + metadata=metadata, + ) assert exc_info.value.code() == grpc.StatusCode.PERMISSION_DENIED diff --git a/e2e/python/oidc/workspace_authz_test.py b/e2e/python/oidc/workspace_authz_test.py index 9a904810cb..fe8ad4bcea 100644 --- a/e2e/python/oidc/workspace_authz_test.py +++ b/e2e/python/oidc/workspace_authz_test.py @@ -176,7 +176,7 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "CreateSandbox", lambda s, m: s.CreateSandbox( openshell_pb2.CreateSandboxRequest( - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), spec=openshell_pb2.SandboxSpec( template=openshell_pb2.SandboxTemplate(image="ubuntu:24.04") ), @@ -187,20 +187,29 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: ( "GetSandbox", lambda s, m: s.GetSandbox( - openshell_pb2.GetSandboxRequest(name="nonexistent", workspace=WS), + openshell_pb2.GetSandboxRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=m, ), ), ( "ListSandboxes", lambda s, m: s.ListSandboxes( - openshell_pb2.ListSandboxesRequest(workspace=WS), metadata=m + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS) + ), + metadata=m, ), ), ( "DeleteSandbox", lambda s, m: s.DeleteSandbox( - openshell_pb2.DeleteSandboxRequest(name="nonexistent", workspace=WS), + openshell_pb2.DeleteSandboxRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=m, ), ), @@ -208,7 +217,8 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "ListSandboxProviders", lambda s, m: s.ListSandboxProviders( openshell_pb2.ListSandboxProvidersRequest( - sandbox_name="nonexistent", workspace=WS + sandbox_name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -219,7 +229,7 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: openshell_pb2.AttachSandboxProviderRequest( sandbox_name="nonexistent", provider_name="nonexistent", - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -230,7 +240,7 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: openshell_pb2.DetachSandboxProviderRequest( sandbox_name="nonexistent", provider_name="nonexistent", - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -240,7 +250,7 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "CreateProvider", lambda s, m: s.CreateProvider( openshell_pb2.CreateProviderRequest( - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta( name="authz-test", workspace=WS @@ -255,21 +265,27 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: ( "GetProvider", lambda s, m: s.GetProvider( - openshell_pb2.GetProviderRequest(name="nonexistent", workspace=WS), + openshell_pb2.GetProviderRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=m, ), ), ( "ListProviders", lambda s, m: s.ListProviders( - openshell_pb2.ListProvidersRequest(workspace=WS), metadata=m + openshell_pb2.ListProvidersRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS) + ), + metadata=m, ), ), ( "UpdateProvider", lambda s, m: s.UpdateProvider( openshell_pb2.UpdateProviderRequest( - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta( name="nonexistent", workspace=WS @@ -284,7 +300,10 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: ( "DeleteProvider", lambda s, m: s.DeleteProvider( - openshell_pb2.DeleteProviderRequest(name="nonexistent", workspace=WS), + openshell_pb2.DeleteProviderRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=m, ), ), @@ -337,7 +356,8 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "GetProviderRefreshStatus", lambda s, m: s.GetProviderRefreshStatus( openshell_pb2.GetProviderRefreshStatusRequest( - provider="nonexistent", workspace=WS + provider="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -349,7 +369,7 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: provider="nonexistent", credential_key="k", strategy=openshell_pb2.PROVIDER_CREDENTIAL_REFRESH_STRATEGY_STATIC, - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -360,7 +380,7 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: openshell_pb2.RotateProviderCredentialRequest( provider="nonexistent", credential_key="k", - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -371,7 +391,7 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: openshell_pb2.DeleteProviderRefreshRequest( provider="nonexistent", credential_key="k", - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -384,7 +404,7 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: sandbox="nonexistent", service="svc", target_port=8080, - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -393,7 +413,9 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "GetService", lambda s, m: s.GetService( openshell_pb2.GetServiceRequest( - sandbox="nonexistent", service="svc", workspace=WS + sandbox="nonexistent", + service="svc", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -401,14 +423,19 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: ( "ListServices", lambda s, m: s.ListServices( - openshell_pb2.ListServicesRequest(workspace=WS), metadata=m + openshell_pb2.ListServicesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS) + ), + metadata=m, ), ), ( "DeleteService", lambda s, m: s.DeleteService( openshell_pb2.DeleteServiceRequest( - sandbox="nonexistent", service="svc", workspace=WS + sandbox="nonexistent", + service="svc", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -418,7 +445,8 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "GetSandboxPolicyStatus", lambda s, m: s.GetSandboxPolicyStatus( openshell_pb2.GetSandboxPolicyStatusRequest( - name="nonexistent", workspace=WS + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -427,7 +455,8 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "ListSandboxPolicies", lambda s, m: s.ListSandboxPolicies( openshell_pb2.ListSandboxPoliciesRequest( - name="nonexistent", workspace=WS + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -435,7 +464,10 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: ( "GetDraftPolicy", lambda s, m: s.GetDraftPolicy( - openshell_pb2.GetDraftPolicyRequest(name="nonexistent", workspace=WS), + openshell_pb2.GetDraftPolicyRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=m, ), ), @@ -443,7 +475,9 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "ApproveDraftChunk", lambda s, m: s.ApproveDraftChunk( openshell_pb2.ApproveDraftChunkRequest( - name="nonexistent", chunk_id="x", workspace=WS + name="nonexistent", + chunk_id="x", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -452,7 +486,9 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "RejectDraftChunk", lambda s, m: s.RejectDraftChunk( openshell_pb2.RejectDraftChunkRequest( - name="nonexistent", chunk_id="x", workspace=WS + name="nonexistent", + chunk_id="x", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -461,7 +497,8 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "ApproveAllDraftChunks", lambda s, m: s.ApproveAllDraftChunks( openshell_pb2.ApproveAllDraftChunksRequest( - name="nonexistent", workspace=WS + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -470,7 +507,9 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "EditDraftChunk", lambda s, m: s.EditDraftChunk( openshell_pb2.EditDraftChunkRequest( - name="nonexistent", chunk_id="x", workspace=WS + name="nonexistent", + chunk_id="x", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -479,7 +518,9 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: "UndoDraftChunk", lambda s, m: s.UndoDraftChunk( openshell_pb2.UndoDraftChunkRequest( - name="nonexistent", chunk_id="x", workspace=WS + name="nonexistent", + chunk_id="x", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=m, ), @@ -487,14 +528,20 @@ def _workspace_rpcs() -> list[tuple[str, Callable]]: ( "ClearDraftChunks", lambda s, m: s.ClearDraftChunks( - openshell_pb2.ClearDraftChunksRequest(name="nonexistent", workspace=WS), + openshell_pb2.ClearDraftChunksRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=m, ), ), ( "GetDraftHistory", lambda s, m: s.GetDraftHistory( - openshell_pb2.GetDraftHistoryRequest(name="nonexistent", workspace=WS), + openshell_pb2.GetDraftHistoryRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=m, ), ), @@ -558,18 +605,14 @@ def _global_policy_read_rpcs() -> list[tuple[str, Callable]]: ( "GetSandboxPolicyStatus", lambda s, m: s.GetSandboxPolicyStatus( - openshell_pb2.GetSandboxPolicyStatusRequest( - workspace="", **{"global": True} - ), + openshell_pb2.GetSandboxPolicyStatusRequest(**{"global": True}), metadata=m, ), ), ( "ListSandboxPolicies", lambda s, m: s.ListSandboxPolicies( - openshell_pb2.ListSandboxPoliciesRequest( - workspace="", **{"global": True} - ), + openshell_pb2.ListSandboxPoliciesRequest(**{"global": True}), metadata=m, ), ), @@ -626,7 +669,9 @@ def seed_provider(self, admin_ctx: Any, workspace: str) -> Any: with contextlib.suppress(grpc.RpcError): stub.CreateProvider( openshell_pb2.CreateProviderRequest( - workspace=workspace, + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace=workspace + ), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta( name=prov_name, workspace=workspace @@ -641,7 +686,10 @@ def seed_provider(self, admin_ctx: Any, workspace: str) -> Any: with contextlib.suppress(grpc.RpcError): stub.DeleteProvider( openshell_pb2.DeleteProviderRequest( - name=prov_name, workspace=workspace + name=prov_name, + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace=workspace + ), ), metadata=metadata, ) @@ -673,7 +721,10 @@ def test_non_member_rejected_update_config( stub, metadata, user_sub = user_ctx with pytest.raises(grpc.RpcError) as exc_info: stub.UpdateConfig( - openshell_pb2.UpdateConfigRequest(name="nonexistent", workspace=WS), + openshell_pb2.UpdateConfigRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=metadata, ) _assert_non_member_denial( @@ -713,7 +764,9 @@ def test_get_sandbox_logs_rejects_spoofed_workspace( response = admin_stub.CreateSandbox( openshell_pb2.CreateSandboxRequest( name=sandbox_name, - workspace=other_workspace, + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace=other_workspace + ), spec=openshell_pb2.SandboxSpec(), ), metadata=admin_md, @@ -724,7 +777,7 @@ def test_get_sandbox_logs_rejects_spoofed_workspace( user_stub.GetSandboxLogs( openshell_pb2.GetSandboxLogsRequest( sandbox_id=sandbox_id, - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=user_md, ) @@ -740,7 +793,9 @@ def test_get_sandbox_logs_rejects_spoofed_workspace( admin_stub.DeleteSandbox( openshell_pb2.DeleteSandboxRequest( name=sandbox_name, - workspace=other_workspace, + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace=other_workspace + ), ), metadata=admin_md, ) @@ -773,7 +828,6 @@ def test_global_update_rejected_for_default_workspace_admin( with pytest.raises(grpc.RpcError) as exc_info: user_stub.UpdateConfig( openshell_pb2.UpdateConfigRequest( - workspace="", setting_key="log_level", delete_setting=True, **{"global": True}, @@ -824,7 +878,9 @@ def test_platform_admin_list_sandboxes( ) -> None: stub, metadata = admin_ctx stub.ListSandboxes( - openshell_pb2.ListSandboxesRequest(workspace=WS), + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS) + ), metadata=metadata, ) @@ -835,7 +891,10 @@ def test_platform_admin_get_provider( ) -> None: stub, metadata = admin_ctx resp = stub.GetProvider( - openshell_pb2.GetProviderRequest(name=seed_provider, workspace=WS), + openshell_pb2.GetProviderRequest( + name=seed_provider, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=metadata, ) assert resp.provider.metadata.name == seed_provider @@ -846,7 +905,9 @@ def test_platform_admin_list_services( ) -> None: stub, metadata = admin_ctx stub.ListServices( - openshell_pb2.ListServicesRequest(workspace=WS), + openshell_pb2.ListServicesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS) + ), metadata=metadata, ) @@ -858,7 +919,10 @@ def test_platform_admin_get_draft_history( # May fail with NOT_FOUND for the sandbox name, but should not fail with PERMISSION_DENIED try: stub.GetDraftHistory( - openshell_pb2.GetDraftHistoryRequest(name="nonexistent", workspace=WS), + openshell_pb2.GetDraftHistoryRequest( + name="nonexistent", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=metadata, ) except grpc.RpcError as e: @@ -888,26 +952,35 @@ def test_user_member_read_operations( # ListSandboxes user_stub.ListSandboxes( - openshell_pb2.ListSandboxesRequest(workspace=WS), + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS) + ), metadata=user_md, ) # GetProvider resp = user_stub.GetProvider( - openshell_pb2.GetProviderRequest(name=seed_provider, workspace=WS), + openshell_pb2.GetProviderRequest( + name=seed_provider, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=user_md, ) assert resp.provider.metadata.name == seed_provider # ListProviders user_stub.ListProviders( - openshell_pb2.ListProvidersRequest(workspace=WS), + openshell_pb2.ListProvidersRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS) + ), metadata=user_md, ) # ListServices user_stub.ListServices( - openshell_pb2.ListServicesRequest(workspace=WS), + openshell_pb2.ListServicesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS) + ), metadata=user_md, ) @@ -937,7 +1010,7 @@ def test_user_member_admin_operations_denied( with pytest.raises(grpc.RpcError) as exc_info: user_stub.CreateProvider( openshell_pb2.CreateProviderRequest( - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta( name="user-blocked", workspace=WS @@ -978,7 +1051,7 @@ def test_user_member_admin_operations_denied( openshell_pb2.ApproveDraftChunkRequest( name="nonexistent", chunk_id="x", - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), ), metadata=user_md, ) @@ -1008,7 +1081,7 @@ def test_workspace_admin_can_create_provider( try: user_stub.CreateProvider( openshell_pb2.CreateProviderRequest( - workspace=WS, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=prov_name, workspace=WS), type="claude", @@ -1031,7 +1104,10 @@ def test_workspace_admin_can_create_provider( finally: with contextlib.suppress(grpc.RpcError): admin_stub.DeleteProvider( - openshell_pb2.DeleteProviderRequest(name=prov_name, workspace=WS), + openshell_pb2.DeleteProviderRequest( + name=prov_name, + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=WS), + ), metadata=admin_md, ) _remove_member(admin_stub, admin_md, WS, user_sub) @@ -1052,21 +1128,33 @@ def test_all_workspaces_rejected_for_workspace_admin( try: with pytest.raises(grpc.RpcError) as exc_info: user_stub.ListSandboxes( - openshell_pb2.ListSandboxesRequest(all_workspaces=True), + openshell_pb2.ListSandboxesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + all_workspaces=datamodel_pb2.AllWorkspaces() + ) + ), metadata=user_md, ) assert exc_info.value.code() == grpc.StatusCode.PERMISSION_DENIED with pytest.raises(grpc.RpcError) as exc_info: user_stub.ListProviders( - openshell_pb2.ListProvidersRequest(all_workspaces=True), + openshell_pb2.ListProvidersRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + all_workspaces=datamodel_pb2.AllWorkspaces() + ) + ), metadata=user_md, ) assert exc_info.value.code() == grpc.StatusCode.PERMISSION_DENIED with pytest.raises(grpc.RpcError) as exc_info: user_stub.ListServices( - openshell_pb2.ListServicesRequest(all_workspaces=True), + openshell_pb2.ListServicesRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + all_workspaces=datamodel_pb2.AllWorkspaces() + ) + ), metadata=user_md, ) assert exc_info.value.code() == grpc.StatusCode.PERMISSION_DENIED diff --git a/e2e/python/test_policy_validation.py b/e2e/python/test_policy_validation.py index 323804c4d9..7beb7e146f 100644 --- a/e2e/python/test_policy_validation.py +++ b/e2e/python/test_policy_validation.py @@ -69,7 +69,13 @@ def test_create_sandbox_rejects_root_user( stub = sandbox_client._stub with pytest.raises(grpc.RpcError) as exc_info: - stub.CreateSandbox(openshell_pb2.CreateSandboxRequest(name="", spec=spec)) + stub.CreateSandbox( + openshell_pb2.CreateSandboxRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), + name="", + spec=spec, + ) + ) assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT assert "root" in exc_info.value.details().lower() @@ -93,7 +99,13 @@ def test_create_sandbox_rejects_path_traversal( stub = sandbox_client._stub with pytest.raises(grpc.RpcError) as exc_info: - stub.CreateSandbox(openshell_pb2.CreateSandboxRequest(name="", spec=spec)) + stub.CreateSandbox( + openshell_pb2.CreateSandboxRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), + name="", + spec=spec, + ) + ) assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT assert "traversal" in exc_info.value.details().lower() @@ -117,7 +129,13 @@ def test_create_sandbox_rejects_overly_broad_paths( stub = sandbox_client._stub with pytest.raises(grpc.RpcError) as exc_info: - stub.CreateSandbox(openshell_pb2.CreateSandboxRequest(name="", spec=spec)) + stub.CreateSandbox( + openshell_pb2.CreateSandboxRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), + name="", + spec=spec, + ) + ) assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT assert "broad" in exc_info.value.details().lower() @@ -153,7 +171,7 @@ def test_create_sandbox_materializes_default_mcp_version( stored = sandbox_client._stub.GetSandbox( openshell_pb2.GetSandboxRequest( name=created.name, - workspace="default", + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), ) ) stored_endpoint = stored.sandbox.spec.policy.network_policies[ @@ -210,6 +228,9 @@ def test_update_policy_rejects_immutable_fields( with pytest.raises(grpc.RpcError) as exc_info: stub.UpdateConfig( openshell_pb2.UpdateConfigRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace="default" + ), name=sandbox_name, policy=unsafe_policy, ) diff --git a/e2e/python/test_sandbox_providers.py b/e2e/python/test_sandbox_providers.py index 3ef4276f13..0b3c9b0a83 100644 --- a/e2e/python/test_sandbox_providers.py +++ b/e2e/python/test_sandbox_providers.py @@ -81,12 +81,13 @@ def provider( _delete_provider(stub, name) stub.CreateProvider( openshell_pb2.CreateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type=provider_type, credentials=credentials, profile_workspace=profile_workspace, - ) + ), ) ) try: @@ -98,7 +99,12 @@ def provider( def _delete_provider(stub: object, name: str) -> None: """Delete a provider, ignoring not-found errors.""" try: - stub.DeleteProvider(openshell_pb2.DeleteProviderRequest(name=name)) + stub.DeleteProvider( + openshell_pb2.DeleteProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), + name=name, + ) + ) except grpc.RpcError as exc: if hasattr(exc, "code") and exc.code() == grpc.StatusCode.NOT_FOUND: pass @@ -337,13 +343,14 @@ def test_profileless_provider_creation_is_rejected( with pytest.raises(grpc.RpcError) as exc_info: sandbox_client._stub.CreateProvider( openshell_pb2.CreateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta( name="e2e-test-profileless-provider" ), type="generic", credentials={"CUSTOM_SERVICE_TOKEN": "token-generic-123"}, - ) + ), ) ) assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT @@ -496,6 +503,9 @@ def wait_for_token(sb: Sandbox, expected: str) -> None: try: stub.AttachSandboxProvider( openshell_pb2.AttachSandboxProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace="default" + ), sandbox_name=sb.sandbox.name, provider_name=provider_name, ) @@ -507,6 +517,9 @@ def wait_for_token(sb: Sandbox, expected: str) -> None: stub.DetachSandboxProvider( openshell_pb2.DetachSandboxProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace="default" + ), sandbox_name=sb.sandbox.name, provider_name=provider_name, ) @@ -516,6 +529,9 @@ def wait_for_token(sb: Sandbox, expected: str) -> None: try: stub.DetachSandboxProvider( openshell_pb2.DetachSandboxProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace="default" + ), sandbox_name=sb.sandbox.name, provider_name=provider_name, ) @@ -750,7 +766,12 @@ def test_credentials_not_in_persisted_spec_environment( with sandbox(spec=spec, delete_on_exit=True) as sb: fetched = sandbox_client._stub.GetSandbox( - openshell_pb2.GetSandboxRequest(name=sb.sandbox.name) + openshell_pb2.GetSandboxRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace="default" + ), + name=sb.sandbox.name, + ) ) persisted_env = dict(fetched.sandbox.spec.environment) assert "ANTHROPIC_API_KEY" not in persisted_env, ( @@ -774,6 +795,7 @@ def test_update_provider_preserves_unset_credentials_and_config( try: stub.CreateProvider( openshell_pb2.CreateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type="codex", @@ -783,21 +805,27 @@ def test_update_provider_preserves_unset_credentials_and_config( "CODEX_AUTH_ACCOUNT_ID": "account-id", }, config={"BASE_URL": "https://example.com"}, - ) + ), ) ) stub.UpdateProvider( openshell_pb2.UpdateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type="", credentials={"CODEX_AUTH_ACCESS_TOKEN": "rotated-a"}, - ) + ), ) ) - got = stub.GetProvider(openshell_pb2.GetProviderRequest(name=name)) + got = stub.GetProvider( + openshell_pb2.GetProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), + name=name, + ) + ) p = got.provider # Credential keys are preserved but values are redacted. assert len(p.credentials) > 0, "credential keys should be preserved" @@ -823,25 +851,32 @@ def test_update_provider_empty_maps_preserves_all( try: stub.CreateProvider( openshell_pb2.CreateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type="openai", credentials={"OPENAI_API_KEY": "secret"}, config={"URL": "https://api.example.com"}, - ) + ), ) ) stub.UpdateProvider( openshell_pb2.UpdateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type="", - ) + ), ) ) - got = stub.GetProvider(openshell_pb2.GetProviderRequest(name=name)) + got = stub.GetProvider( + openshell_pb2.GetProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), + name=name, + ) + ) p = got.provider # Credential keys are preserved but values are redacted. assert len(p.credentials) > 0, "credential keys should be preserved" @@ -865,26 +900,33 @@ def test_update_provider_merges_config_preserves_credentials( try: stub.CreateProvider( openshell_pb2.CreateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type="openai", credentials={"OPENAI_API_KEY": "original-key"}, config={"ENDPOINT": "https://old.example.com"}, - ) + ), ) ) stub.UpdateProvider( openshell_pb2.UpdateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type="", config={"ENDPOINT": "https://new.example.com"}, - ) + ), ) ) - got = stub.GetProvider(openshell_pb2.GetProviderRequest(name=name)) + got = stub.GetProvider( + openshell_pb2.GetProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), + name=name, + ) + ) p = got.provider # Credential keys are preserved but values are redacted. assert len(p.credentials) > 0, "credential keys should be preserved" @@ -908,21 +950,25 @@ def test_update_provider_rejects_type_change( try: stub.CreateProvider( openshell_pb2.CreateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type="openai", credentials={"OPENAI_API_KEY": "val"}, - ) + ), ) ) with pytest.raises(grpc.RpcError) as exc_info: stub.UpdateProvider( openshell_pb2.UpdateProviderRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector( + workspace="default" + ), provider=datamodel_pb2.Provider( metadata=datamodel_pb2.ObjectMeta(name=name), type="nvidia", - ) + ), ) ) assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT diff --git a/examples/governance-interceptor/src/main.rs b/examples/governance-interceptor/src/main.rs index 207727622a..eefeb695fa 100644 --- a/examples/governance-interceptor/src/main.rs +++ b/examples/governance-interceptor/src/main.rs @@ -1233,8 +1233,7 @@ async fn propagate_policy_to_running_sandboxes( limit, offset, label_selector: String::new(), - workspace: String::new(), - all_workspaces: true, + workspace_scope: Some(openshell_core::proto::all_workspaces_selector()), }) .await .map_err(|status| format!("list sandboxes failed: {status}"))? @@ -1257,6 +1256,7 @@ async fn propagate_policy_to_running_sandboxes( 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 e1dfde4038..25d9ef2f69 100644 --- a/examples/governance-interceptor/src/smoke_client.rs +++ b/examples/governance-interceptor/src/smoke_client.rs @@ -85,6 +85,7 @@ async fn main() -> Result<(), Box> { .update_config(UpdateConfigRequest { name: sandbox_name.clone(), policy: Some(widened_policy), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), ..Default::default() }) .await; diff --git a/proto/datamodel.proto b/proto/datamodel.proto index b990f05768..d39ee75500 100644 --- a/proto/datamodel.proto +++ b/proto/datamodel.proto @@ -7,6 +7,26 @@ package openshell.datamodel.v1; import "options.proto"; +// Selects the workspace scope for a public API request. +// +// Requests that operate on one workspace require a non-empty `workspace`. +// Cross-workspace list requests additionally accept `all_workspaces`. The +// containing request documents which selections it supports; an omitted +// selector is invalid for workspace-scoped operations. +message WorkspaceSelector { + oneof selection { + // One explicitly named workspace. Use `default` to select the gateway's + // default workspace; an empty name is invalid. + string workspace = 1; + // All workspaces the caller is authorized to access. Only supported by + // requests that explicitly document cross-workspace behavior. + AllWorkspaces all_workspaces = 2; + } +} + +// Marker for the all-workspaces selector variant. +message AllWorkspaces {} + // Kubernetes-style metadata shared by all top-level OpenShell domain objects. // // This structure provides consistent metadata (identity, labels, annotations, diff --git a/proto/openshell.proto b/proto/openshell.proto index 36f1026d84..580cb2ce2c 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1117,6 +1117,8 @@ message PlatformEvent { // Create sandbox request. message CreateSandboxRequest { + reserved 5; + reserved "workspace"; SandboxSpec spec = 1; // Optional user-supplied sandbox name. When empty the server generates one. string name = 2; @@ -1124,43 +1126,49 @@ message CreateSandboxRequest { map labels = 3; // Optional annotations for the sandbox (non-selector metadata). map annotations = 4; - // Workspace for the sandbox. Empty defaults to "default". - string workspace = 5; // One-shot launch hint indicating that the creating client will attach to // the canonical main process. The supervisor keeps the terminal transport // alive until that attachment connects and closes naturally. bool await_main_process_attachment = 6; // Workspace-scoped SandboxWorkloadTemplate name to resolve at creation time. string workload_template_name = 7; + // Explicit workspace for the sandbox. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 8; } message CreateSandboxTemplateRequest { + reserved 2; + reserved "workspace"; SandboxWorkloadTemplate template = 1; - // Workspace for the template. Empty defaults to "default". - string workspace = 2; + // Explicit workspace for the template. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } message GetSandboxTemplateRequest { + reserved 2; + reserved "workspace"; string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } message ListSandboxTemplatesRequest { + reserved 3, 4; + reserved "workspace", "all_workspaces"; uint32 limit = 1; uint32 offset = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; - // List across all workspaces. Mutually exclusive with workspace. - bool all_workspaces = 4; // Optional label selector in key=value comma-separated form. string label_selector = 5; + // Explicit named or all-workspaces scope. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; } message DeleteSandboxTemplateRequest { + reserved 2; + reserved "workspace"; string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } message SandboxTemplateResponse { @@ -1177,15 +1185,17 @@ message DeleteSandboxTemplateResponse { // Request a gateway-owned staging slot for a local rootfs tar archive. message BeginRootfsTarStagingRequest { - // Workspace that will own the sandbox created from this archive. Empty - // defaults to "default", matching CreateSandboxRequest.workspace. - string workspace = 1; + reserved 1; + reserved "workspace"; // Base file name of the local archive. The gateway uses it only to name the // staged file; path separators and traversal components are rejected. string file_name = 2; // Size of the local archive in bytes, checked against the driver limit // before the gateway allocates a slot. uint64 size_bytes = 3; + // Explicit workspace that will own the sandbox created from this archive. + // The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } // Gateway-issued staging slot. @@ -1204,34 +1214,40 @@ message BeginRootfsTarStagingResponse { // Get sandbox request. message GetSandboxRequest { + reserved 2; + reserved "workspace"; // Sandbox name (canonical lookup key). string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // List sandboxes request. message ListSandboxesRequest { + reserved 4, 5; + reserved "workspace", "all_workspaces"; uint32 limit = 1; uint32 offset = 2; // Optional label selector for filtering (format: "key1=value1,key2=value2"). string label_selector = 3; - // Workspace scope. Empty defaults to "default". - string workspace = 4; - // List across all workspaces. Mutually exclusive with workspace. - bool all_workspaces = 5; + // Explicit named or all-workspaces scope. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; } // List providers attached to a sandbox request. message ListSandboxProvidersRequest { + reserved 2; + reserved "workspace"; // Sandbox name (canonical lookup key). string sandbox_name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Attach provider to sandbox request. message AttachSandboxProviderRequest { + reserved 4; + reserved "workspace"; // Sandbox name (canonical lookup key). string sandbox_name = 1; // Provider name to attach. @@ -1241,12 +1257,14 @@ 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; - // Workspace scope. Empty defaults to "default". - string workspace = 4; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } // Detach provider from sandbox request. message DetachSandboxProviderRequest { + reserved 4; + reserved "workspace"; // Sandbox name (canonical lookup key). string sandbox_name = 1; // Provider name to detach. @@ -1256,32 +1274,38 @@ 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; - // Workspace scope. Empty defaults to "default". - string workspace = 4; + // 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; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Stop sandbox request. message StopSandboxRequest { + reserved 2; + reserved "workspace"; // Sandbox name (canonical lookup key). string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Start sandbox request. message StartSandboxRequest { + reserved 2; + reserved "workspace"; // Sandbox name (canonical lookup key). string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Sandbox response. @@ -1360,6 +1384,8 @@ message CreateSshSessionResponse { // Request to expose an HTTP service running inside a sandbox. message ExposeServiceRequest { + reserved 5; + reserved "workspace"; // Sandbox name. string sandbox = 1; // Service name within the sandbox. @@ -1368,32 +1394,34 @@ message ExposeServiceRequest { uint32 target_port = 3; // Whether to print/use the browser-facing service URL. bool domain = 4; - // Workspace scope. Empty defaults to "default". - string workspace = 5; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; } // Request to fetch an exposed sandbox service endpoint. message GetServiceRequest { + reserved 3; + reserved "workspace"; // Sandbox name. string sandbox = 1; // Service name within the sandbox. Empty selects the unnamed endpoint. string service = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } // Request to list exposed sandbox service endpoints. message ListServicesRequest { + reserved 4, 5; + reserved "workspace", "all_workspaces"; // Optional sandbox name. Empty lists endpoints for all sandboxes. string sandbox = 1; // Page size. Zero uses the server default. uint32 limit = 2; // Page offset. uint32 offset = 3; - // Workspace scope. Empty defaults to "default". - string workspace = 4; - // List across all workspaces. Mutually exclusive with workspace. - bool all_workspaces = 5; + // Explicit named or all-workspaces scope. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; } // Response containing exposed sandbox service endpoints. @@ -1403,12 +1431,14 @@ message ListServicesResponse { // Request to delete an exposed sandbox service endpoint. message DeleteServiceRequest { + reserved 3; + reserved "workspace"; // Sandbox name. string sandbox = 1; // Service name within the sandbox. Empty selects the unnamed endpoint. string service = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } // Response for deleting an exposed sandbox service endpoint. @@ -1645,43 +1675,51 @@ message SandboxStreamWarning { // Create provider request. message CreateProviderRequest { + reserved 2; + reserved "workspace"; openshell.datamodel.v1.Provider provider = 1; - // Workspace for the provider. Empty defaults to "default". - string workspace = 2; + // Explicit workspace for the provider. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Get provider request. message GetProviderRequest { + reserved 2; + reserved "workspace"; string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // List providers request. message ListProvidersRequest { + reserved 3, 4; + reserved "workspace", "all_workspaces"; uint32 limit = 1; uint32 offset = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; - // List across all workspaces. Mutually exclusive with workspace. - bool all_workspaces = 4; + // Explicit named or all-workspaces scope. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } // Update provider request. message UpdateProviderRequest { + reserved 3; + reserved "workspace"; openshell.datamodel.v1.Provider provider = 1; // Optional per-credential expiry timestamps to merge into the provider. // A zero value removes the expiry for that credential. map credential_expires_at_ms = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } // Delete provider request. message DeleteProviderRequest { + reserved 2; + reserved "workspace"; string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } // Provider response. @@ -1886,10 +1924,12 @@ message ProviderProfileDiscovery { } message GetProviderRefreshStatusRequest { + reserved 3; + reserved "workspace"; string provider = 1; string credential_key = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } message GetProviderRefreshStatusResponse { @@ -1897,6 +1937,8 @@ message GetProviderRefreshStatusResponse { } message ConfigureProviderRefreshRequest { + reserved 7; + reserved "workspace"; string provider = 1; string credential_key = 2; ProviderCredentialRefreshStrategy strategy = 3; @@ -1906,8 +1948,8 @@ message ConfigureProviderRefreshRequest { // the authoritative provider profile and refresh strategy. repeated string secret_material_keys = 5; optional int64 expires_at_ms = 6; - // Workspace scope. Empty defaults to "default". - string workspace = 7; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 8; } message ConfigureProviderRefreshResponse { @@ -1915,10 +1957,12 @@ message ConfigureProviderRefreshResponse { } message RotateProviderCredentialRequest { + reserved 3; + reserved "workspace"; string provider = 1; string credential_key = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } message RotateProviderCredentialResponse { @@ -1926,10 +1970,12 @@ message RotateProviderCredentialResponse { } message DeleteProviderRefreshRequest { + reserved 3; + reserved "workspace"; string provider = 1; string credential_key = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } message DeleteProviderRefreshResponse { @@ -2133,6 +2179,8 @@ 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; @@ -2169,8 +2217,9 @@ message UpdateConfigRequest { // sandbox metadata as a convenience projection. For setting-only updates, it // only merges them into sandbox metadata. map annotations = 9; - // Workspace scope. Empty defaults to "default". Ignored for global-scoped updates. - string workspace = 10; + // Explicit workspace scope for sandbox-scoped updates. Omit only when + // `global` is true; the all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 11; } message PolicyMergeOperation { @@ -2232,14 +2281,17 @@ 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; // 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; - // Workspace scope. Empty defaults to "default". Ignored when global is true. - string workspace = 4; + // Explicit workspace scope for sandbox-scoped queries. Omit only when + // `global` is true; the all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } // Get sandbox policy status response. @@ -2252,14 +2304,17 @@ 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; uint32 limit = 2; uint32 offset = 3; // List global policy revisions instead of sandbox-scoped ones. bool global = 4; - // Workspace scope. Empty defaults to "default". Ignored when global is true. - string workspace = 5; + // Explicit workspace scope for sandbox-scoped queries. Omit only when + // `global` is true; the all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; } // List sandbox policies response. @@ -2330,6 +2385,8 @@ enum PolicyStatus { // Get sandbox logs request (one-shot fetch). message GetSandboxLogsRequest { + reserved 6; + reserved "workspace"; // Sandbox id. string sandbox_id = 1; // Maximum number of log lines to return. 0 means use default (2000). @@ -2340,8 +2397,8 @@ message GetSandboxLogsRequest { repeated string sources = 4; // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. string min_level = 5; - // Workspace scope. Empty defaults to "default". - string workspace = 6; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 7; } // Batch of log lines pushed from sandbox to server. @@ -2694,12 +2751,14 @@ message SubmitPolicyAnalysisResponse { // Get draft policy for a sandbox. message GetDraftPolicyRequest { + reserved 3; + reserved "workspace"; // Sandbox name. string name = 1; // Optional status filter: "pending", "approved", "rejected", or "" for all. string status_filter = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } message GetDraftPolicyResponse { @@ -2715,15 +2774,17 @@ message GetDraftPolicyResponse { // Approve a single draft chunk. message ApproveDraftChunkRequest { + reserved 3; + reserved "workspace"; // Sandbox name. string name = 1; // Chunk ID to approve. string chunk_id = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; // 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. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } message ApproveDraftChunkResponse { @@ -2735,14 +2796,16 @@ message ApproveDraftChunkResponse { // Reject a single draft chunk. message RejectDraftChunkRequest { + reserved 4; + reserved "workspace"; // Sandbox name. string name = 1; // Chunk ID to reject. string chunk_id = 2; // Optional reason for rejection (fed to LLM context in future analysis). string reason = 3; - // Workspace scope. Empty defaults to "default". - string workspace = 4; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } message RejectDraftChunkResponse {} @@ -2754,15 +2817,17 @@ message DraftChunkApproval { } message ApproveAllDraftChunksRequest { + reserved 3; + reserved "workspace"; // Sandbox name. string name = 1; // Include chunks with security_notes (default false: skips them). bool include_security_flagged = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; // 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. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } message ApproveAllDraftChunksResponse { @@ -2779,26 +2844,30 @@ message ApproveAllDraftChunksResponse { // Edit a pending chunk in-place. message EditDraftChunkRequest { + reserved 4; + reserved "workspace"; // Sandbox name. string name = 1; // Chunk ID to edit. string chunk_id = 2; // The modified rule (replaces existing proposed_rule). openshell.sandbox.v1.NetworkPolicyRule proposed_rule = 3; - // Workspace scope. Empty defaults to "default". - string workspace = 4; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; } message EditDraftChunkResponse {} // Reverse an approval (remove merged rule from active policy). message UndoDraftChunkRequest { + reserved 3; + reserved "workspace"; // Sandbox name. string name = 1; // Chunk ID to undo. string chunk_id = 2; - // Workspace scope. Empty defaults to "default". - string workspace = 3; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; } message UndoDraftChunkResponse { @@ -2810,10 +2879,12 @@ message UndoDraftChunkResponse { // Clear all pending draft chunks for a sandbox. message ClearDraftChunksRequest { + reserved 2; + reserved "workspace"; // Sandbox name. string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } message ClearDraftChunksResponse { @@ -2823,10 +2894,12 @@ message ClearDraftChunksResponse { // Get decision history for a sandbox's draft policy. message GetDraftHistoryRequest { + reserved 2; + reserved "workspace"; // Sandbox name. string name = 1; - // Workspace scope. Empty defaults to "default". - string workspace = 2; + // Explicit workspace scope. The all-workspaces selection is invalid. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; } message DraftHistoryEntry { diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index c34a2c05fe..343d75edcc 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -37,6 +37,16 @@ _OAUTH_MAX_RESPONSE_BYTES = 1 << 20 +def _workspace_scope(workspace: str) -> datamodel_pb2.WorkspaceSelector: + if not workspace: + raise ValueError("workspace must be non-empty") + return datamodel_pb2.WorkspaceSelector(workspace=workspace) + + +def _all_workspaces_scope() -> datamodel_pb2.WorkspaceSelector: + return datamodel_pb2.WorkspaceSelector(all_workspaces=datamodel_pb2.AllWorkspaces()) + + class _ClientCallDetails(_ClientCallDetailsBase, grpc.ClientCallDetails): pass @@ -722,7 +732,7 @@ def create( spec=request_spec, name=name or "", labels=dict(labels) if labels else {}, - workspace=workspace, + workspace_scope=_workspace_scope(workspace), ), timeout=self._timeout, ) @@ -748,7 +758,7 @@ def create_from_template( spec=request_spec, name=name or "", labels=dict(labels) if labels else {}, - workspace=workspace, + workspace_scope=_workspace_scope(workspace), workload_template_name=template_name, ), timeout=self._timeout, @@ -795,7 +805,9 @@ 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=workspace), + openshell_pb2.GetSandboxRequest( + name=sandbox_name, workspace_scope=_workspace_scope(workspace) + ), timeout=self._timeout, ) return _sandbox_ref(response.sandbox) @@ -812,7 +824,7 @@ def list( label_selector: str | None = None, ) -> builtins.list[SandboxRef]: request = openshell_pb2.ListSandboxesRequest( - workspace=workspace, + workspace_scope=_workspace_scope(workspace), limit=limit, offset=offset, label_selector=label_selector or "", @@ -828,7 +840,7 @@ def list_for_all_workspaces( label_selector: str | None = None, ) -> builtins.list[SandboxRef]: request = openshell_pb2.ListSandboxesRequest( - all_workspaces=True, + workspace_scope=_all_workspaces_scope(), limit=limit, offset=offset, label_selector=label_selector or "", @@ -872,21 +884,27 @@ 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=workspace), + openshell_pb2.DeleteSandboxRequest( + name=sandbox_name, workspace_scope=_workspace_scope(workspace) + ), timeout=self._timeout, ) return bool(response.deleted) def stop(self, sandbox_name: str, *, workspace: str) -> SandboxRef: response = self._stub.StopSandbox( - openshell_pb2.StopSandboxRequest(name=sandbox_name, workspace=workspace), + openshell_pb2.StopSandboxRequest( + name=sandbox_name, workspace_scope=_workspace_scope(workspace) + ), timeout=self._timeout, ) return _sandbox_ref(response.sandbox) def start(self, sandbox_name: str, *, workspace: str) -> SandboxRef: response = self._stub.StartSandbox( - openshell_pb2.StartSandboxRequest(name=sandbox_name, workspace=workspace), + openshell_pb2.StartSandboxRequest( + name=sandbox_name, workspace_scope=_workspace_scope(workspace) + ), timeout=self._timeout, ) return _sandbox_ref(response.sandbox) @@ -1138,7 +1156,7 @@ def create( response = self._stub.CreateSandboxTemplate( openshell_pb2.CreateSandboxTemplateRequest( - workspace=workspace, + workspace_scope=_workspace_scope(workspace), template=template, ), timeout=self._timeout, @@ -1152,7 +1170,9 @@ def get( workspace: str, ) -> openshell_pb2.SandboxWorkloadTemplate: response = self._stub.GetSandboxTemplate( - openshell_pb2.GetSandboxTemplateRequest(name=name, workspace=workspace), + openshell_pb2.GetSandboxTemplateRequest( + name=name, workspace_scope=_workspace_scope(workspace) + ), timeout=self._timeout, ) return response.template @@ -1167,7 +1187,7 @@ def list( ) -> builtins.list[openshell_pb2.SandboxWorkloadTemplate]: response = self._stub.ListSandboxTemplates( openshell_pb2.ListSandboxTemplatesRequest( - workspace=workspace, + workspace_scope=_workspace_scope(workspace), limit=limit, offset=offset, label_selector=label_selector, @@ -1185,7 +1205,7 @@ def list_for_all_workspaces( ) -> builtins.list[openshell_pb2.SandboxWorkloadTemplate]: response = self._stub.ListSandboxTemplates( openshell_pb2.ListSandboxTemplatesRequest( - all_workspaces=True, + workspace_scope=_all_workspaces_scope(), limit=limit, offset=offset, label_selector=label_selector, @@ -1196,7 +1216,9 @@ def list_for_all_workspaces( def delete(self, name: str, *, workspace: str) -> bool: response = self._stub.DeleteSandboxTemplate( - openshell_pb2.DeleteSandboxTemplateRequest(name=name, workspace=workspace), + openshell_pb2.DeleteSandboxTemplateRequest( + name=name, workspace_scope=_workspace_scope(workspace) + ), timeout=self._timeout, ) return bool(response.deleted) diff --git a/python/openshell/sandbox_test.py b/python/openshell/sandbox_test.py index fd9e2bcb5b..772a327239 100644 --- a/python/openshell/sandbox_test.py +++ b/python/openshell/sandbox_test.py @@ -42,6 +42,17 @@ ) +def _request_workspace(request: Any) -> str | None: + scope = request.workspace_scope + if scope.WhichOneof("selection") == "workspace": + return cast("str", scope.workspace) + return None + + +def _request_selects_all_workspaces(request: Any) -> bool: + return request.workspace_scope.WhichOneof("selection") == "all_workspaces" + + def _client_credentials_fixture() -> dict[str, Any]: return json.loads( ( @@ -1981,7 +1992,9 @@ def GetSandbox( _ = timeout return SimpleNamespace( sandbox=_make_sandbox_proto( - "sandbox-1", request.name, workspace=request.workspace or "default" + "sandbox-1", + request.name, + workspace=_request_workspace(request) or "default", ) ) @@ -2006,7 +2019,7 @@ def StopSandbox( "sandbox-1", request.name, phase=openshell_pb2.SANDBOX_PHASE_STOPPED, - workspace=request.workspace, + workspace=_request_workspace(request) or "default", ) ) @@ -2022,7 +2035,7 @@ def StartSandbox( "sandbox-1", request.name, phase=openshell_pb2.SANDBOX_PHASE_STARTING, - workspace=request.workspace, + workspace=_request_workspace(request) or "default", ) ) @@ -2038,7 +2051,7 @@ def CreateSandbox( "sandbox-1", request.name or "generated", dict(request.labels), - workspace=request.workspace or "default", + workspace=_request_workspace(request) or "default", ) ) @@ -2071,7 +2084,7 @@ def GetSandboxTemplate( return SimpleNamespace( template=_make_workload_template_proto( request.name, - workspace=request.workspace or "default", + workspace=_request_workspace(request) or "default", ) ) @@ -2217,7 +2230,7 @@ def test_sandbox_template_create_builds_template_from_public_fields() -> None: assert created.metadata.name == "gpu-kata" assert stub.create_template_request is not None - assert stub.create_template_request.workspace == "default" + assert _request_workspace(stub.create_template_request) == "default" template = stub.create_template_request.template assert template.metadata.name == "gpu-kata" assert dict(template.metadata.labels) == {"team": "runtime"} @@ -2361,7 +2374,7 @@ def test_sandbox_template_client_crud_forwards_requests() -> None: assert created.metadata.name == "gpu-kata" assert stub.create_template_request is not None - assert stub.create_template_request.workspace == "default" + assert _request_workspace(stub.create_template_request) == "default" assert ( stub.create_template_request.template.spec.workload.image == "ghcr.io/test/gpu-kata:latest" @@ -2377,34 +2390,34 @@ def test_sandbox_template_client_crud_forwards_requests() -> None: assert got.metadata.name == "gpu-kata" assert stub.get_template_request is not None assert stub.get_template_request.name == "gpu-kata" - assert stub.get_template_request.workspace == "default" + assert _request_workspace(stub.get_template_request) == "default" listed = client.list( workspace="default", limit=50, offset=10, label_selector="team=runtime" ) assert len(listed) == 1 assert stub.list_template_request is not None - assert stub.list_template_request.workspace == "default" + assert _request_workspace(stub.list_template_request) == "default" assert stub.list_template_request.limit == 50 assert stub.list_template_request.offset == 10 assert stub.list_template_request.label_selector == "team=runtime" - assert not stub.list_template_request.all_workspaces + assert not _request_selects_all_workspaces(stub.list_template_request) assert client.delete("gpu-kata", workspace="default") is True assert stub.delete_template_request is not None assert stub.delete_template_request.name == "gpu-kata" - assert stub.delete_template_request.workspace == "default" + assert _request_workspace(stub.delete_template_request) == "default" -def test_sandbox_template_list_for_all_workspaces_clears_workspace() -> None: +def test_sandbox_template_list_for_all_workspaces_selects_all() -> None: stub = _FakeSandboxStub() client = _template_client_with_fake_stub(stub) client.list_for_all_workspaces(limit=100, offset=5, label_selector="team=runtime") assert stub.list_template_request is not None - assert stub.list_template_request.all_workspaces - assert stub.list_template_request.workspace == "" + assert _request_selects_all_workspaces(stub.list_template_request) + assert _request_workspace(stub.list_template_request) is None assert stub.list_template_request.limit == 100 assert stub.list_template_request.offset == 5 assert stub.list_template_request.label_selector == "team=runtime" @@ -2417,13 +2430,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 stub.stop_request.workspace == "team-a" + 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 stub.start_request.workspace == "team-a" + assert _request_workspace(stub.start_request) == "team-a" assert starting.phase == openshell_pb2.SANDBOX_PHASE_STARTING @@ -2449,7 +2462,7 @@ def GetSandbox( "sandbox-1", request.name, phase=phase, - workspace=request.workspace, + workspace=_request_workspace(request) or "default", ) ) @@ -2471,7 +2484,7 @@ def test_create_without_args_sends_empty_metadata() -> None: assert stub.create_request is not None assert stub.create_request.name == "" assert dict(stub.create_request.labels) == {} - assert stub.create_request.workspace == "default" + assert _request_workspace(stub.create_request) == "default" def test_create_copies_caller_labels() -> None: @@ -2508,7 +2521,7 @@ def test_list_forwards_label_selector() -> None: assert stub.list_request is not None assert stub.list_request.label_selector == "aiq=deep-research" - assert stub.list_request.workspace == "default" + assert _request_workspace(stub.list_request) == "default" def test_list_without_selector_sends_empty_string() -> None: @@ -2725,7 +2738,7 @@ def test_create_passes_workspace_to_proto() -> None: ref = client.create(workspace="staging", name="job-1") assert stub.create_request is not None - assert stub.create_request.workspace == "staging" + assert _request_workspace(stub.create_request) == "staging" assert ref.workspace == "staging" @@ -2736,7 +2749,7 @@ def test_get_passes_workspace_to_proto() -> None: ref = client.get("job-1", workspace="production") assert stub.get_request is not None - assert stub.get_request.workspace == "production" + assert _request_workspace(stub.get_request) == "production" assert ref.workspace == "production" @@ -2748,7 +2761,7 @@ def test_delete_passes_workspace_to_proto() -> None: assert result is True assert stub.delete_request is not None - assert stub.delete_request.workspace == "staging" + assert _request_workspace(stub.delete_request) == "staging" def test_list_for_all_workspaces_sets_flag() -> None: @@ -2758,8 +2771,8 @@ def test_list_for_all_workspaces_sets_flag() -> None: client.list_for_all_workspaces() assert stub.list_request is not None - assert stub.list_request.all_workspaces is True - assert stub.list_request.workspace == "" + assert _request_selects_all_workspaces(stub.list_request) + assert _request_workspace(stub.list_request) is None def test_list_with_workspace_passes_workspace() -> None: @@ -2769,8 +2782,8 @@ def test_list_with_workspace_passes_workspace() -> None: client.list(workspace="staging") assert stub.list_request is not None - assert stub.list_request.workspace == "staging" - assert stub.list_request.all_workspaces is False + assert _request_workspace(stub.list_request) == "staging" + assert not _request_selects_all_workspaces(stub.list_request) def test_sandbox_ref_includes_workspace_from_proto() -> None: @@ -2809,4 +2822,4 @@ def test_sandbox_session_delete_passes_workspace() -> None: session.delete() assert stub.delete_request is not None - assert stub.delete_request.workspace == "staging" + assert _request_workspace(stub.delete_request) == "staging" diff --git a/rfc/0011-multi-player-design/README.md b/rfc/0011-multi-player-design/README.md index 32a2f584e1..87fb5f558a 100644 --- a/rfc/0011-multi-player-design/README.md +++ b/rfc/0011-multi-player-design/README.md @@ -312,8 +312,8 @@ Within a workspace, access varies by resource type: boundary — within a workspace, all members share the same trust domain and the same provider credentials, so there is no security benefit to restricting sandbox access by owner. Platform Admins can list across workspaces using - `all_workspaces = true` on list RPCs (see Cross-Workspace List Operations - below). + the `all_workspaces` `WorkspaceSelector` variant on list RPCs (see + Cross-Workspace List Operations below). - **Providers.** Users can list and reference providers by name within their workspace but cannot create, update, or delete them, and cannot see raw @@ -507,7 +507,8 @@ resolves it, and the runtime consumes it directly. that depends on a request field. Handlers may strengthen, but never weaken, the declared baseline for these cases: -- `all_workspaces: true` requires Platform Admin on cross-workspace list RPCs. +- The `all_workspaces` selector requires Platform Admin on cross-workspace list + RPCs. - `global: true` requires Platform Admin for global configuration and policy reads or writes. - An empty provider-profile workspace selects platform scope and requires @@ -861,16 +862,16 @@ analogous to `kubectl get pods --all-namespaces`. This is an explicit opt-in on list RPCs, not the default behavior. **RPC mechanism.** Workspace-scoped list RPCs (`ListSandboxes`, -`ListProviders`, `ListServices`) gain an `all_workspaces` boolean field. When -`all_workspaces = true`, the handler bypasses workspace scoping and returns -results from all workspaces. The caller must have the Platform Admin global role; -workspace-scoped roles cannot set `all_workspaces`. Results include the -`workspace` field in each resource's `ObjectMeta` so the caller can distinguish -provenance. +`ListSandboxTemplates`, `ListProviders`, `ListServices`) use a +`WorkspaceSelector` oneof with either a non-empty named workspace or an +`all_workspaces` marker. The all-workspaces variant bypasses workspace scoping +and returns results from all workspaces. The caller must have the Platform +Admin global role; workspace-scoped roles cannot select all workspaces. Results +include the `workspace` field in each resource's `ObjectMeta` so the caller can +distinguish provenance. -This is distinct from passing an empty workspace string. Empty workspace is -resolved to `"default"` by the gateway's `resolve_workspace()` logic for -backwards compatibility — it does not mean "all workspaces." +An omitted selector, an unset selector, and an empty named workspace are +invalid. The default workspace is selected explicitly with the name `default`. **Store query.** The `all_workspaces` handler path uses the same `list_by_type(object_type, limit, offset)` store method as the internal @@ -1081,8 +1082,8 @@ etc.). Unlike the CLI, the SDK does not default to `"default"` — programmatic callers must always specify the target workspace. This is a deliberate design choice: agents and automation scripts should be explicit about which workspace they operate on, and a silent default could mask workspace-routing bugs. -Passing `workspace=None` to `list()` uses `all_workspaces=True` for -cross-workspace queries. +Cross-workspace queries use the separate `list_for_all_workspaces()` method so +named and all-workspaces scopes cannot conflict. ## Implementation plan @@ -1149,8 +1150,9 @@ foundations. The work can be phased to deliver value incrementally: workspace through `StoredProviderCredentialRefreshState` so the provider refresh worker can unambiguously resolve workspace-scoped providers — with multiple workspaces, `provider_name` alone is insufficient because different - workspaces can have same-named providers. Add `all_workspaces` field to - workspace-scoped list RPCs for Platform Admin cross-workspace visibility. + workspaces can have same-named providers. Add the typed `WorkspaceSelector` + to workspace-scoped request RPCs and its `all_workspaces` variant to list + RPCs for Platform Admin cross-workspace visibility. Add `ObjectWorkspace::requires_workspace()` trait method and validation in store write helpers (`put_message`, `put_scoped_message`) that returns an error when a workspace-scoped resource is persisted with an empty workspace. @@ -1270,10 +1272,10 @@ depend only on Phase 1. - **Cross-workspace store query authorization.** The `list_by_type` store method has no access-control gate — it is a persistence-layer primitive. Authorization for cross-workspace queries is enforced at the gRPC handler level (Platform - Admin check for `all_workspaces` on list RPCs) and by code-level access - control for internal operations (only the reconciler, start, and refresh - worker call it). This relies on internal code discipline rather than an - enforced store-level boundary. A future extension could add a store-level + Admin check for the `all_workspaces` selector on list RPCs) and by code-level + access control for internal operations (only the reconciler, start, and + refresh worker call it). This relies on internal code discipline rather than + an enforced store-level boundary. A future extension could add a store-level caller identity parameter if defense-in-depth is desired. - **Remote compute driver channel security.** The `RemoteComputeDriver` gRPC diff --git a/sdk/go/docs/src/api/providers.md b/sdk/go/docs/src/api/providers.md index 9eec23f675..87c82379a1 100644 --- a/sdk/go/docs/src/api/providers.md +++ b/sdk/go/docs/src/api/providers.md @@ -59,6 +59,9 @@ providers, err = client.Providers().List(ctx, "default", v1.ListOptions{ Limit: 10, Offset: 0, }) + +// Platform Admin only: list across all workspaces +allProviders, err := client.Providers().ListAll(ctx) ``` ## Update diff --git a/sdk/go/docs/src/api/sandbox-templates.md b/sdk/go/docs/src/api/sandbox-templates.md index d1dcc86e47..03e8ae43e8 100644 --- a/sdk/go/docs/src/api/sandbox-templates.md +++ b/sdk/go/docs/src/api/sandbox-templates.md @@ -97,8 +97,8 @@ templates, err := client.SandboxTemplates().List(ctx, "default", v1.ListOptions{ Offset: 0, }) -allTemplates, err := client.SandboxTemplates().List(ctx, "", v1.ListOptions{ - AllWorkspaces: true, +allTemplates, err := client.SandboxTemplates().ListAll(ctx, v1.ListOptions{ + Limit: 50, }) ``` diff --git a/sdk/go/docs/src/api/sandboxes.md b/sdk/go/docs/src/api/sandboxes.md index 698982fe13..af026a17cb 100644 --- a/sdk/go/docs/src/api/sandboxes.md +++ b/sdk/go/docs/src/api/sandboxes.md @@ -69,6 +69,9 @@ sandboxes, err := client.Sandboxes().List(ctx, "default", v1.ListOptions{ Offset: 0, LabelSelector: "team=platform", }) + +// Platform Admin only: list across all workspaces +allSandboxes, err := client.Sandboxes().ListAll(ctx) ``` ## Delete diff --git a/sdk/go/docs/src/api/services.md b/sdk/go/docs/src/api/services.md index 6d3195e1ad..0c44567d2f 100644 --- a/sdk/go/docs/src/api/services.md +++ b/sdk/go/docs/src/api/services.md @@ -30,6 +30,9 @@ if err != nil { for _, svc := range services { fmt.Printf(" %s -> port %d (%s)\n", svc.ServiceName, svc.TargetPort, svc.URL) } + +// Platform Admin only: list services across all workspaces +allServices, err := client.Services().ListAll(ctx) ``` ## Delete diff --git a/sdk/go/openshell/v1/config_client.go b/sdk/go/openshell/v1/config_client.go index e7086b9b6c..b2cb3fd372 100644 --- a/sdk/go/openshell/v1/config_client.go +++ b/sdk/go/openshell/v1/config_client.go @@ -58,7 +58,9 @@ func (c *configClient) Update(ctx context.Context, workspace string, update *Con if convErr != nil { return nil, &StatusError{Code: ErrorInvalidArgument, Message: convErr.Error()} } - req.Workspace = workspace + if !req.GetGlobal() { + req.WorkspaceScope = namedWorkspaceScope(workspace) + } resp, err := c.client.UpdateConfig(ctx, req) if err != nil { return nil, converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/exec_client_test.go b/sdk/go/openshell/v1/exec_client_test.go index 1c649ac0b7..53acc1e8bc 100644 --- a/sdk/go/openshell/v1/exec_client_test.go +++ b/sdk/go/openshell/v1/exec_client_test.go @@ -40,6 +40,9 @@ func (r *stubSandboxResolver) Create(context.Context, string, string, *SandboxSp func (r *stubSandboxResolver) List(context.Context, string, ...ListOptions) ([]*Sandbox, error) { panic("not implemented") } +func (r *stubSandboxResolver) ListAll(context.Context, ...ListOptions) ([]*Sandbox, error) { + panic("not implemented") +} func (r *stubSandboxResolver) Delete(context.Context, string, string) error { panic("not implemented") } diff --git a/sdk/go/openshell/v1/fake/provider.go b/sdk/go/openshell/v1/fake/provider.go index 1f5ce4d831..7ba0345880 100644 --- a/sdk/go/openshell/v1/fake/provider.go +++ b/sdk/go/openshell/v1/fake/provider.go @@ -113,16 +113,20 @@ func (c *fakeProviderClient) Get(_ context.Context, workspace, name string) (*ty // List returns all providers. ListOptions are accepted for interface // compatibility but filtering is not implemented. -func (c *fakeProviderClient) List(_ context.Context, workspace string, opts ...v1.ListOptions) ([]*types.Provider, error) { +func (c *fakeProviderClient) List(_ context.Context, workspace string, _ ...v1.ListOptions) ([]*types.Provider, error) { if c.closedFunc() { return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} } - if len(opts) > 0 && opts[0].AllWorkspaces { - return c.store.ListAll(), nil - } return c.store.List(workspace), nil } +func (c *fakeProviderClient) ListAll(_ context.Context, _ ...v1.ListOptions) ([]*types.Provider, error) { + if c.closedFunc() { + return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} + } + return c.store.ListAll(), nil +} + // Update replaces an existing provider's data. ResourceVersion is // incremented automatically. func (c *fakeProviderClient) Update(_ context.Context, workspace string, provider *types.Provider) (*types.Provider, error) { diff --git a/sdk/go/openshell/v1/fake/sandbox.go b/sdk/go/openshell/v1/fake/sandbox.go index 5de3062d96..8191594a5c 100644 --- a/sdk/go/openshell/v1/fake/sandbox.go +++ b/sdk/go/openshell/v1/fake/sandbox.go @@ -447,16 +447,20 @@ func (c *fakeSandboxClient) Get(_ context.Context, workspace, name string) (*typ // List returns all sandboxes. ListOptions are accepted for interface // compatibility but filtering is not implemented. -func (c *fakeSandboxClient) List(_ context.Context, workspace string, opts ...v1.ListOptions) ([]*types.Sandbox, error) { +func (c *fakeSandboxClient) List(_ context.Context, workspace string, _ ...v1.ListOptions) ([]*types.Sandbox, error) { if c.closedFunc() { return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} } - if len(opts) > 0 && opts[0].AllWorkspaces { - return c.store.ListAll(), nil - } return c.store.List(workspace), nil } +func (c *fakeSandboxClient) ListAll(_ context.Context, _ ...v1.ListOptions) ([]*types.Sandbox, error) { + if c.closedFunc() { + return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} + } + return c.store.ListAll(), nil +} + // Stop transitions a sandbox to the Stopped phase. func (c *fakeSandboxClient) Stop(_ context.Context, workspace, name string) (*types.Sandbox, error) { if c.closedFunc() { diff --git a/sdk/go/openshell/v1/fake/sandbox_template.go b/sdk/go/openshell/v1/fake/sandbox_template.go index 8dbf68cc12..b105de7c13 100644 --- a/sdk/go/openshell/v1/fake/sandbox_template.go +++ b/sdk/go/openshell/v1/fake/sandbox_template.go @@ -106,6 +106,14 @@ func (c *fakeSandboxTemplateClient) Get(_ context.Context, workspace, name strin } func (c *fakeSandboxTemplateClient) List(_ context.Context, workspace string, opts ...v1.ListOptions) ([]*types.SandboxWorkloadTemplate, error) { + return c.list(workspace, false, opts...) +} + +func (c *fakeSandboxTemplateClient) ListAll(_ context.Context, opts ...v1.ListOptions) ([]*types.SandboxWorkloadTemplate, error) { + return c.list("", true, opts...) +} + +func (c *fakeSandboxTemplateClient) list(workspace string, allWorkspaces bool, opts ...v1.ListOptions) ([]*types.SandboxWorkloadTemplate, error) { if c.closedFunc() { return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} } @@ -120,7 +128,7 @@ func (c *fakeSandboxTemplateClient) List(_ context.Context, workspace string, op } } var templates []*types.SandboxWorkloadTemplate - if options.AllWorkspaces { + if allWorkspaces { templates = c.store.ListAll() } else { templates = c.store.List(workspace) diff --git a/sdk/go/openshell/v1/fake/sandbox_template_test.go b/sdk/go/openshell/v1/fake/sandbox_template_test.go index 06a5187227..6caa0da684 100644 --- a/sdk/go/openshell/v1/fake/sandbox_template_test.go +++ b/sdk/go/openshell/v1/fake/sandbox_template_test.go @@ -108,7 +108,7 @@ func TestSandboxTemplate_ListAllWorkspaces(t *testing.T) { _, _ = tc.Create(ctx, "default", testSandboxWorkloadTemplate("default-template")) _, _ = tc.Create(ctx, "team-a", testSandboxWorkloadTemplate("team-template")) - listed, err := tc.List(ctx, "default", types.ListOptions{AllWorkspaces: true}) + listed, err := tc.ListAll(ctx) require.NoError(t, err) assert.Len(t, listed, 2) } diff --git a/sdk/go/openshell/v1/fake/service.go b/sdk/go/openshell/v1/fake/service.go index 8b0c4e1482..4b3029012f 100644 --- a/sdk/go/openshell/v1/fake/service.go +++ b/sdk/go/openshell/v1/fake/service.go @@ -45,6 +45,14 @@ func (c *fakeServiceClient) List(_ context.Context, _, _ string, _ ...v1.ListOpt return nil, &types.StatusError{Code: types.ErrorUnimplemented, Message: "List is not supported by the fake client"} } +// ListAll returns Unimplemented. +func (c *fakeServiceClient) ListAll(_ context.Context, _ ...v1.ListOptions) ([]*types.ServiceEndpoint, error) { + if c.closedFunc() { + return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} + } + return nil, &types.StatusError{Code: types.ErrorUnimplemented, Message: "ListAll is not supported by the fake client"} +} + // Delete returns Unimplemented. func (c *fakeServiceClient) Delete(_ context.Context, _, _, _ string) error { if c.closedFunc() { diff --git a/sdk/go/openshell/v1/policy_client.go b/sdk/go/openshell/v1/policy_client.go index f002caf9d9..12801af02a 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(), - Workspace: workspace, + Name: sandboxName, + StatusFilter: cfg.StatusFilter(), + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -35,10 +35,10 @@ 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, - Workspace: workspace, - ReviewToken: reviewToken, + Name: sandboxName, + ChunkId: chunkID, + WorkspaceScope: namedWorkspaceScope(workspace), + ReviewToken: reviewToken, }) if err != nil { return nil, converter.FromGRPCError(err) @@ -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, - ChunkId: chunkID, - Reason: reason, - Workspace: workspace, + Name: sandboxName, + ChunkId: chunkID, + Reason: reason, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return converter.FromGRPCError(err) @@ -71,7 +71,7 @@ func (p *policyClient) ApproveAllDraftChunks(ctx context.Context, workspace, san resp, err := p.client.ApproveAllDraftChunks(ctx, &pb.ApproveAllDraftChunksRequest{ Name: sandboxName, IncludeSecurityFlagged: cfg.IncludeSecurityFlagged(), - Workspace: workspace, + WorkspaceScope: namedWorkspaceScope(workspace), Approvals: approvals, }) if err != nil { @@ -82,8 +82,8 @@ 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, - Workspace: workspace, + Name: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -93,8 +93,8 @@ 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, - Workspace: workspace, + Name: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -114,12 +114,15 @@ 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) - resp, err := p.client.GetSandboxPolicyStatus(ctx, &pb.GetSandboxPolicyStatusRequest{ - Name: sandboxName, - Version: cfg.Version(), - Workspace: workspace, - Global: cfg.Global(), - }) + req := &pb.GetSandboxPolicyStatusRequest{ + Name: sandboxName, + Version: cfg.Version(), + Global: cfg.Global(), + } + if !cfg.Global() { + req.WorkspaceScope = namedWorkspaceScope(workspace) + } + resp, err := p.client.GetSandboxPolicyStatus(ctx, req) if err != nil { return nil, converter.FromGRPCError(err) } @@ -128,12 +131,15 @@ func (p *policyClient) GetStatus(ctx context.Context, workspace, sandboxName str func (p *policyClient) List(ctx context.Context, workspace string, opts ...ListPolicyOption) ([]SandboxPolicyRevision, error) { cfg := types.ApplyListPolicyOptions(opts) - resp, err := p.client.ListSandboxPolicies(ctx, &pb.ListSandboxPoliciesRequest{ - Workspace: workspace, - Limit: cfg.Limit(), - Offset: cfg.Offset(), - Global: cfg.Global(), - }) + req := &pb.ListSandboxPoliciesRequest{ + Limit: cfg.Limit(), + Offset: cfg.Offset(), + Global: cfg.Global(), + } + if !cfg.Global() { + req.WorkspaceScope = namedWorkspaceScope(workspace) + } + resp, err := p.client.ListSandboxPolicies(ctx, req) if err != nil { return nil, converter.FromGRPCError(err) } @@ -152,10 +158,10 @@ func (p *policyClient) List(ctx context.Context, workspace string, opts ...ListP func (p *policyClient) EditDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string, proposedRule *NetworkPolicyRule) error { _, err := p.client.EditDraftChunk(ctx, &pb.EditDraftChunkRequest{ - Name: sandboxName, - ChunkId: chunkID, - ProposedRule: converter.NetworkPolicyRuleToProto(proposedRule), - Workspace: workspace, + Name: sandboxName, + ChunkId: chunkID, + ProposedRule: converter.NetworkPolicyRuleToProto(proposedRule), + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return converter.FromGRPCError(err) @@ -165,9 +171,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, - Workspace: workspace, + Name: sandboxName, + ChunkId: chunkID, + WorkspaceScope: namedWorkspaceScope(workspace), }) 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 d572ca8e33..a0e16f5a40 100644 --- a/sdk/go/openshell/v1/policy_client_test.go +++ b/sdk/go/openshell/v1/policy_client_test.go @@ -650,7 +650,7 @@ func TestPolicyGetStatus_WithGlobal(t *testing.T) { mock.mu.Lock() assert.True(t, mock.lastStatusReq.GetGlobal()) assert.Empty(t, mock.lastStatusReq.GetName()) - assert.Empty(t, mock.lastStatusReq.GetWorkspace()) + assert.Nil(t, mock.lastStatusReq.GetWorkspaceScope()) mock.mu.Unlock() } @@ -676,7 +676,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.Equal(t, "some-workspace", mock.lastStatusReq.GetWorkspace()) + assert.Nil(t, mock.lastStatusReq.GetWorkspaceScope()) mock.mu.Unlock() } @@ -731,7 +731,7 @@ func TestPolicyGetStatus_WithoutGlobal_PreservesExistingBehavior(t *testing.T) { // Verify global flag is false by default. mock.mu.Lock() assert.False(t, mock.lastStatusReq.GetGlobal()) - assert.Equal(t, "default", mock.lastStatusReq.GetWorkspace()) + assert.Equal(t, "default", mock.lastStatusReq.GetWorkspaceScope().GetWorkspace()) assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetName()) mock.mu.Unlock() } @@ -780,7 +780,7 @@ func TestPolicyList(t *testing.T) { // Verify request was forwarded (no pagination options). mock.mu.Lock() - assert.Equal(t, "default", mock.lastListReq.GetWorkspace()) + assert.Equal(t, "default", mock.lastListReq.GetWorkspaceScope().GetWorkspace()) assert.Equal(t, uint32(0), mock.lastListReq.GetLimit()) assert.Equal(t, uint32(0), mock.lastListReq.GetOffset()) mock.mu.Unlock() @@ -855,7 +855,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.Empty(t, mock.lastListReq.GetWorkspace()) + assert.Nil(t, mock.lastListReq.GetWorkspaceScope()) mock.mu.Unlock() } @@ -877,7 +877,7 @@ func TestPolicyList_WithGlobalIgnoresWorkspace(t *testing.T) { mock.mu.Lock() assert.True(t, mock.lastListReq.GetGlobal()) - assert.Equal(t, "some-workspace", mock.lastListReq.GetWorkspace()) + assert.Nil(t, mock.lastListReq.GetWorkspaceScope()) mock.mu.Unlock() } @@ -928,7 +928,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, "default", mock.lastListReq.GetWorkspace()) + assert.Equal(t, "default", mock.lastListReq.GetWorkspaceScope().GetWorkspace()) mock.mu.Unlock() } diff --git a/sdk/go/openshell/v1/provider.go b/sdk/go/openshell/v1/provider.go index f1ab67c482..42788f8855 100644 --- a/sdk/go/openshell/v1/provider.go +++ b/sdk/go/openshell/v1/provider.go @@ -21,6 +21,7 @@ type ProviderInterface interface { Create(ctx context.Context, workspace string, provider *Provider) (*Provider, error) Get(ctx context.Context, workspace, name string) (*Provider, error) List(ctx context.Context, workspace string, opts ...ListOptions) ([]*Provider, error) + ListAll(ctx context.Context, opts ...ListOptions) ([]*Provider, error) Update(ctx context.Context, workspace string, provider *Provider) (*Provider, error) Delete(ctx context.Context, workspace, name string) error Ensure(ctx context.Context, workspace string, provider *Provider) (*Provider, error) diff --git a/sdk/go/openshell/v1/provider_client.go b/sdk/go/openshell/v1/provider_client.go index 19784b6346..237325a1e3 100644 --- a/sdk/go/openshell/v1/provider_client.go +++ b/sdk/go/openshell/v1/provider_client.go @@ -35,8 +35,8 @@ func (p *providerClient) Refresh() RefreshInterface { func (p *providerClient) Create(ctx context.Context, workspace string, provider *Provider) (*Provider, error) { resp, err := p.client.CreateProvider(ctx, &pb.CreateProviderRequest{ - Provider: converter.ProviderToProto(provider), - Workspace: workspace, + Provider: converter.ProviderToProto(provider), + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -46,8 +46,8 @@ func (p *providerClient) Create(ctx context.Context, workspace string, provider func (p *providerClient) Get(ctx context.Context, workspace, name string) (*Provider, error) { resp, err := p.client.GetProvider(ctx, &pb.GetProviderRequest{ - Name: name, - Workspace: workspace, + Name: name, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -57,8 +57,16 @@ func (p *providerClient) Get(ctx context.Context, workspace, name string) (*Prov func (p *providerClient) List(ctx context.Context, workspace string, opts ...ListOptions) ([]*Provider, error) { req := &pb.ListProvidersRequest{ - Workspace: workspace, + WorkspaceScope: namedWorkspaceScope(workspace), } + return p.list(ctx, req, opts...) +} + +func (p *providerClient) ListAll(ctx context.Context, opts ...ListOptions) ([]*Provider, error) { + return p.list(ctx, &pb.ListProvidersRequest{WorkspaceScope: allWorkspacesScope()}, opts...) +} + +func (p *providerClient) list(ctx context.Context, req *pb.ListProvidersRequest, opts ...ListOptions) ([]*Provider, error) { if len(opts) > 0 { if opts[0].Limit < 0 { return nil, &StatusError{Code: ErrorInvalidArgument, Message: "limit must not be negative"} @@ -68,7 +76,6 @@ func (p *providerClient) List(ctx context.Context, workspace string, opts ...Lis } req.Limit = uint32(opts[0].Limit) req.Offset = uint32(opts[0].Offset) - req.AllWorkspaces = opts[0].AllWorkspaces } resp, err := p.client.ListProviders(ctx, req) @@ -86,8 +93,8 @@ func (p *providerClient) List(ctx context.Context, workspace string, opts ...Lis func (p *providerClient) Update(ctx context.Context, workspace string, provider *Provider) (*Provider, error) { proto := converter.ProviderToProto(provider) req := &pb.UpdateProviderRequest{ - Provider: proto, - Workspace: workspace, + Provider: proto, + WorkspaceScope: namedWorkspaceScope(workspace), } if proto != nil { req.CredentialExpiresAtMs = proto.CredentialExpiresAtMs @@ -102,8 +109,8 @@ func (p *providerClient) Update(ctx context.Context, workspace string, provider func (p *providerClient) Delete(ctx context.Context, workspace, name string) error { _, err := p.client.DeleteProvider(ctx, &pb.DeleteProviderRequest{ - Name: name, - Workspace: workspace, + Name: name, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/refresh_client.go b/sdk/go/openshell/v1/refresh_client.go index ec98316a93..dc182da844 100644 --- a/sdk/go/openshell/v1/refresh_client.go +++ b/sdk/go/openshell/v1/refresh_client.go @@ -21,9 +21,9 @@ func newRefreshClient(conn grpc.ClientConnInterface) *refreshClient { func (r *refreshClient) GetStatus(ctx context.Context, workspace, provider, credentialKey string) ([]*RefreshStatus, error) { resp, err := r.client.GetProviderRefreshStatus(ctx, &pb.GetProviderRefreshStatusRequest{ - Provider: provider, - CredentialKey: credentialKey, - Workspace: workspace, + Provider: provider, + CredentialKey: credentialKey, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -38,7 +38,7 @@ func (r *refreshClient) GetStatus(ctx context.Context, workspace, provider, cred func (r *refreshClient) Configure(ctx context.Context, workspace string, config *RefreshConfig) (*RefreshStatus, error) { req := converter.RefreshConfigToProto(config) - req.Workspace = workspace + req.WorkspaceScope = namedWorkspaceScope(workspace) resp, err := r.client.ConfigureProviderRefresh(ctx, req) if err != nil { return nil, converter.FromGRPCError(err) @@ -48,9 +48,9 @@ func (r *refreshClient) Configure(ctx context.Context, workspace string, config func (r *refreshClient) Rotate(ctx context.Context, workspace, provider, credentialKey string) (*RefreshStatus, error) { resp, err := r.client.RotateProviderCredential(ctx, &pb.RotateProviderCredentialRequest{ - Provider: provider, - CredentialKey: credentialKey, - Workspace: workspace, + Provider: provider, + CredentialKey: credentialKey, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -60,9 +60,9 @@ func (r *refreshClient) Rotate(ctx context.Context, workspace, provider, credent func (r *refreshClient) Delete(ctx context.Context, workspace, provider, credentialKey string) (bool, error) { resp, err := r.client.DeleteProviderRefresh(ctx, &pb.DeleteProviderRefreshRequest{ - Provider: provider, - CredentialKey: credentialKey, - Workspace: workspace, + Provider: provider, + CredentialKey: credentialKey, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return false, converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/sandbox.go b/sdk/go/openshell/v1/sandbox.go index 79174ac7f3..59ab37ee91 100644 --- a/sdk/go/openshell/v1/sandbox.go +++ b/sdk/go/openshell/v1/sandbox.go @@ -56,6 +56,7 @@ type SandboxInterface interface { Create(ctx context.Context, workspace, name string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) Get(ctx context.Context, workspace, name string) (*Sandbox, error) List(ctx context.Context, workspace string, opts ...ListOptions) ([]*Sandbox, error) + ListAll(ctx context.Context, opts ...ListOptions) ([]*Sandbox, error) Stop(ctx context.Context, workspace, name string) (*Sandbox, error) Start(ctx context.Context, workspace, name string) (*Sandbox, error) Delete(ctx context.Context, workspace, name string) error diff --git a/sdk/go/openshell/v1/sandbox_client.go b/sdk/go/openshell/v1/sandbox_client.go index 75d8d4caa3..4028634e2d 100644 --- a/sdk/go/openshell/v1/sandbox_client.go +++ b/sdk/go/openshell/v1/sandbox_client.go @@ -34,10 +34,10 @@ func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec return nil, &StatusError{Code: ErrorInvalidArgument, Message: err.Error()} } req := &pb.CreateSandboxRequest{ - Name: name, - Spec: protoSpec, - Labels: labels, - Workspace: workspace, + Name: name, + Spec: protoSpec, + Labels: labels, + WorkspaceScope: namedWorkspaceScope(workspace), } if len(opts) > 0 { req.Annotations = converter.CopyStringMap(opts[0].Annotations) @@ -64,7 +64,7 @@ func (s *sandboxClient) CreateFromTemplate(ctx context.Context, workspace, name, Name: name, Spec: protoSpec, Labels: labels, - Workspace: workspace, + WorkspaceScope: namedWorkspaceScope(workspace), WorkloadTemplateName: templateName, } if len(opts) > 0 { @@ -89,8 +89,8 @@ 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, - Workspace: workspace, + Name: name, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -100,8 +100,16 @@ func (s *sandboxClient) Get(ctx context.Context, workspace, name string) (*Sandb func (s *sandboxClient) List(ctx context.Context, workspace string, opts ...ListOptions) ([]*Sandbox, error) { req := &pb.ListSandboxesRequest{ - Workspace: workspace, + WorkspaceScope: namedWorkspaceScope(workspace), } + return s.list(ctx, req, opts...) +} + +func (s *sandboxClient) ListAll(ctx context.Context, opts ...ListOptions) ([]*Sandbox, error) { + return s.list(ctx, &pb.ListSandboxesRequest{WorkspaceScope: allWorkspacesScope()}, opts...) +} + +func (s *sandboxClient) list(ctx context.Context, req *pb.ListSandboxesRequest, opts ...ListOptions) ([]*Sandbox, error) { if len(opts) > 0 { if opts[0].Limit < 0 { return nil, &StatusError{Code: ErrorInvalidArgument, Message: "limit must not be negative"} @@ -112,7 +120,6 @@ func (s *sandboxClient) List(ctx context.Context, workspace string, opts ...List req.Limit = uint32(opts[0].Limit) req.Offset = uint32(opts[0].Offset) req.LabelSelector = opts[0].LabelSelector - req.AllWorkspaces = opts[0].AllWorkspaces } resp, err := s.client.ListSandboxes(ctx, req) @@ -129,8 +136,8 @@ func (s *sandboxClient) List(ctx context.Context, workspace string, opts ...List func (s *sandboxClient) Delete(ctx context.Context, workspace, name string) error { _, err := s.client.DeleteSandbox(ctx, &pb.DeleteSandboxRequest{ - Name: name, - Workspace: workspace, + Name: name, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return converter.FromGRPCError(err) @@ -140,8 +147,8 @@ 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, - Workspace: workspace, + Name: name, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -151,8 +158,8 @@ 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, - Workspace: workspace, + Name: name, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -165,7 +172,7 @@ func (s *sandboxClient) AttachProvider(ctx context.Context, workspace, sandboxNa SandboxName: sandboxName, ProviderName: providerName, ExpectedResourceVersion: expectedResourceVersion, - Workspace: workspace, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -181,7 +188,7 @@ func (s *sandboxClient) DetachProvider(ctx context.Context, workspace, sandboxNa SandboxName: sandboxName, ProviderName: providerName, ExpectedResourceVersion: expectedResourceVersion, - Workspace: workspace, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -194,8 +201,8 @@ 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, - Workspace: workspace, + SandboxName: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -361,11 +368,11 @@ func (s *sandboxClient) GetLogs(ctx context.Context, workspace, sandboxName stri cfg := types.ApplyLogOptions(opts) req := &pb.GetSandboxLogsRequest{ - SandboxId: sb.ID, - Lines: cfg.Lines(), - Sources: cfg.Sources(), - MinLevel: cfg.MinLevel(), - Workspace: workspace, + SandboxId: sb.ID, + 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_template.go b/sdk/go/openshell/v1/sandbox_template.go index e2f2d71874..395b254bac 100644 --- a/sdk/go/openshell/v1/sandbox_template.go +++ b/sdk/go/openshell/v1/sandbox_template.go @@ -41,5 +41,6 @@ type SandboxTemplateInterface interface { Create(ctx context.Context, workspace string, template *SandboxWorkloadTemplate) (*SandboxWorkloadTemplate, error) Get(ctx context.Context, workspace, name string) (*SandboxWorkloadTemplate, error) List(ctx context.Context, workspace string, opts ...ListOptions) ([]*SandboxWorkloadTemplate, error) + ListAll(ctx context.Context, opts ...ListOptions) ([]*SandboxWorkloadTemplate, error) Delete(ctx context.Context, workspace, name string) (bool, error) } diff --git a/sdk/go/openshell/v1/sandbox_template_client.go b/sdk/go/openshell/v1/sandbox_template_client.go index de6e19237c..fd9ddb53e3 100644 --- a/sdk/go/openshell/v1/sandbox_template_client.go +++ b/sdk/go/openshell/v1/sandbox_template_client.go @@ -30,8 +30,8 @@ func (s *sandboxTemplateClient) Create(ctx context.Context, workspace string, te return nil, &StatusError{Code: ErrorInvalidArgument, Message: err.Error()} } resp, err := s.client.CreateSandboxTemplate(ctx, &pb.CreateSandboxTemplateRequest{ - Template: protoTemplate, - Workspace: workspace, + Template: protoTemplate, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -41,8 +41,8 @@ func (s *sandboxTemplateClient) Create(ctx context.Context, workspace string, te func (s *sandboxTemplateClient) Get(ctx context.Context, workspace, name string) (*SandboxWorkloadTemplate, error) { resp, err := s.client.GetSandboxTemplate(ctx, &pb.GetSandboxTemplateRequest{ - Name: name, - Workspace: workspace, + Name: name, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -52,8 +52,16 @@ func (s *sandboxTemplateClient) Get(ctx context.Context, workspace, name string) func (s *sandboxTemplateClient) List(ctx context.Context, workspace string, opts ...ListOptions) ([]*SandboxWorkloadTemplate, error) { req := &pb.ListSandboxTemplatesRequest{ - Workspace: workspace, + WorkspaceScope: namedWorkspaceScope(workspace), } + return s.list(ctx, req, opts...) +} + +func (s *sandboxTemplateClient) ListAll(ctx context.Context, opts ...ListOptions) ([]*SandboxWorkloadTemplate, error) { + return s.list(ctx, &pb.ListSandboxTemplatesRequest{WorkspaceScope: allWorkspacesScope()}, opts...) +} + +func (s *sandboxTemplateClient) list(ctx context.Context, req *pb.ListSandboxTemplatesRequest, opts ...ListOptions) ([]*SandboxWorkloadTemplate, error) { if len(opts) > 0 { if opts[0].Limit < 0 { return nil, &StatusError{Code: ErrorInvalidArgument, Message: "limit must not be negative"} @@ -64,10 +72,6 @@ func (s *sandboxTemplateClient) List(ctx context.Context, workspace string, opts req.Limit = uint32(opts[0].Limit) req.Offset = uint32(opts[0].Offset) req.LabelSelector = opts[0].LabelSelector - req.AllWorkspaces = opts[0].AllWorkspaces - if req.AllWorkspaces { - req.Workspace = "" - } } resp, err := s.client.ListSandboxTemplates(ctx, req) @@ -84,8 +88,8 @@ func (s *sandboxTemplateClient) List(ctx context.Context, workspace string, opts func (s *sandboxTemplateClient) Delete(ctx context.Context, workspace, name string) (bool, error) { resp, err := s.client.DeleteSandboxTemplate(ctx, &pb.DeleteSandboxTemplateRequest{ - Name: name, - Workspace: workspace, + Name: name, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return false, converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/sandbox_template_client_test.go b/sdk/go/openshell/v1/sandbox_template_client_test.go index 4f9e3058c0..48045dbec8 100644 --- a/sdk/go/openshell/v1/sandbox_template_client_test.go +++ b/sdk/go/openshell/v1/sandbox_template_client_test.go @@ -57,7 +57,7 @@ func (s *mockSandboxTemplateServer) CreateSandboxTemplate(_ context.Context, req if template.Metadata == nil { template.Metadata = &dm.ObjectMeta{} } - template.Metadata.Workspace = req.GetWorkspace() + template.Metadata.Workspace = req.GetWorkspaceScope().GetWorkspace() template.Metadata.ResourceVersion = 1 s.templates[template.Metadata.GetName()] = template return &pb.SandboxTemplateResponse{Template: proto.Clone(template).(*pb.SandboxWorkloadTemplate)}, nil @@ -164,7 +164,7 @@ func TestSandboxTemplateCreate(t *testing.T) { mock.mu.Lock() defer mock.mu.Unlock() require.NotNil(t, mock.createRequest) - assert.Equal(t, "default", mock.createRequest.Workspace) + assert.Equal(t, "default", mock.createRequest.GetWorkspaceScope().GetWorkspace()) assert.Equal(t, "gpu-kata", mock.createRequest.Template.Metadata.Name) assert.Equal(t, "2", mock.createRequest.Template.Spec.Workload.Resources.Cpu) assert.Equal(t, "8Gi", mock.createRequest.Template.Spec.Workload.Resources.Memory) @@ -220,11 +220,10 @@ func TestSandboxTemplateGetListDelete(t *testing.T) { assert.Equal(t, "gpu-kata", got.Name) assert.Equal(t, "img:v1", got.Spec.Workload.Image) - list, err := client.List(context.Background(), "default", ListOptions{ + list, err := client.ListAll(context.Background(), ListOptions{ Limit: 10, Offset: 2, LabelSelector: "team=runtime", - AllWorkspaces: true, }) require.NoError(t, err) require.Len(t, list, 1) @@ -237,16 +236,15 @@ func TestSandboxTemplateGetListDelete(t *testing.T) { mock.mu.Lock() defer mock.mu.Unlock() require.NotNil(t, mock.getRequest) - assert.Equal(t, "default", mock.getRequest.Workspace) + assert.Equal(t, "default", mock.getRequest.GetWorkspaceScope().GetWorkspace()) assert.Equal(t, "gpu-kata", mock.getRequest.Name) require.NotNil(t, mock.listRequest) - assert.Empty(t, mock.listRequest.Workspace) + assert.NotNil(t, mock.listRequest.GetWorkspaceScope().GetAllWorkspaces()) assert.Equal(t, uint32(10), mock.listRequest.Limit) assert.Equal(t, uint32(2), mock.listRequest.Offset) assert.Equal(t, "team=runtime", mock.listRequest.LabelSelector) - assert.True(t, mock.listRequest.AllWorkspaces) require.NotNil(t, mock.deleteRequest) - assert.Equal(t, "default", mock.deleteRequest.Workspace) + assert.Equal(t, "default", mock.deleteRequest.GetWorkspaceScope().GetWorkspace()) assert.Equal(t, "gpu-kata", mock.deleteRequest.Name) } diff --git a/sdk/go/openshell/v1/service.go b/sdk/go/openshell/v1/service.go index 4ee819c522..acdcd1eeb6 100644 --- a/sdk/go/openshell/v1/service.go +++ b/sdk/go/openshell/v1/service.go @@ -17,5 +17,6 @@ type ServiceInterface interface { Expose(ctx context.Context, workspace, sandboxName, serviceName string, targetPort uint32, domain bool) (*ServiceEndpoint, error) Get(ctx context.Context, workspace, sandboxName, serviceName string) (*ServiceEndpoint, error) List(ctx context.Context, workspace, sandboxName string, opts ...ListOptions) ([]*ServiceEndpoint, error) + ListAll(ctx context.Context, opts ...ListOptions) ([]*ServiceEndpoint, error) Delete(ctx context.Context, workspace, sandboxName, serviceName string) error } diff --git a/sdk/go/openshell/v1/service_client.go b/sdk/go/openshell/v1/service_client.go index a16dd0dc05..86013f8b30 100644 --- a/sdk/go/openshell/v1/service_client.go +++ b/sdk/go/openshell/v1/service_client.go @@ -21,11 +21,11 @@ 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, - Service: serviceName, - TargetPort: targetPort, - Domain: domain, - Workspace: workspace, + Sandbox: sandboxName, + Service: serviceName, + TargetPort: targetPort, + Domain: domain, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -35,9 +35,9 @@ 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, - Workspace: workspace, + Sandbox: sandboxName, + Service: serviceName, + WorkspaceScope: namedWorkspaceScope(workspace), }) if err != nil { return nil, converter.FromGRPCError(err) @@ -47,9 +47,17 @@ func (s *serviceClient) Get(ctx context.Context, workspace, sandboxName, service func (s *serviceClient) List(ctx context.Context, workspace, sandboxName string, opts ...ListOptions) ([]*ServiceEndpoint, error) { req := &pb.ListServicesRequest{ - Sandbox: sandboxName, - Workspace: workspace, + Sandbox: sandboxName, + WorkspaceScope: namedWorkspaceScope(workspace), } + return s.list(ctx, req, opts...) +} + +func (s *serviceClient) ListAll(ctx context.Context, opts ...ListOptions) ([]*ServiceEndpoint, error) { + return s.list(ctx, &pb.ListServicesRequest{WorkspaceScope: allWorkspacesScope()}, opts...) +} + +func (s *serviceClient) list(ctx context.Context, req *pb.ListServicesRequest, opts ...ListOptions) ([]*ServiceEndpoint, error) { if len(opts) > 0 { if opts[0].Limit < 0 { return nil, &StatusError{Code: ErrorInvalidArgument, Message: "limit must not be negative"} @@ -59,7 +67,6 @@ func (s *serviceClient) List(ctx context.Context, workspace, sandboxName string, } req.Limit = uint32(opts[0].Limit) req.Offset = uint32(opts[0].Offset) - req.AllWorkspaces = opts[0].AllWorkspaces } resp, err := s.client.ListServices(ctx, req) @@ -76,9 +83,9 @@ func (s *serviceClient) List(ctx context.Context, workspace, sandboxName string, func (s *serviceClient) Delete(ctx context.Context, workspace, sandboxName, serviceName string) error { _, err := s.client.DeleteService(ctx, &pb.DeleteServiceRequest{ - Sandbox: sandboxName, - Service: serviceName, - Workspace: workspace, + Sandbox: sandboxName, + Service: serviceName, + WorkspaceScope: namedWorkspaceScope(workspace), }) 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 334acd215a..bf0b4eccb8 100644 --- a/sdk/go/openshell/v1/service_client_test.go +++ b/sdk/go/openshell/v1/service_client_test.go @@ -30,6 +30,7 @@ type mockServiceServer struct { getErr error listErr error deleteErr error + lastList *pb.ListServicesRequest } func newMockServiceServer() *mockServiceServer { @@ -85,6 +86,7 @@ func (s *mockServiceServer) GetService(_ context.Context, req *pb.GetServiceRequ func (s *mockServiceServer) ListServices(_ context.Context, req *pb.ListServicesRequest) (*pb.ListServicesResponse, error) { s.mu.Lock() defer s.mu.Unlock() + s.lastList = req if s.listErr != nil { return nil, s.listErr } @@ -266,6 +268,20 @@ func TestServiceList_WithOptions(t *testing.T) { assert.Len(t, endpoints, 1) } +func TestServiceListAll_SelectsAllWorkspaces(t *testing.T) { + mock := newMockServiceServer() + client, cleanup := setupServiceTest(t, mock) + defer cleanup() + + endpoints, err := client.ListAll(context.Background(), ListOptions{Limit: 10}) + + require.NoError(t, err) + assert.Empty(t, endpoints) + require.NotNil(t, mock.lastList) + assert.Empty(t, mock.lastList.GetSandbox()) + assert.NotNil(t, mock.lastList.GetWorkspaceScope().GetAllWorkspaces()) +} + func TestServiceList_Error(t *testing.T) { mock := newMockServiceServer() mock.listErr = status.Errorf(codes.Unavailable, "unavailable") diff --git a/sdk/go/openshell/v1/ssh_client_test.go b/sdk/go/openshell/v1/ssh_client_test.go index 5b600b3370..f1d0516ca1 100644 --- a/sdk/go/openshell/v1/ssh_client_test.go +++ b/sdk/go/openshell/v1/ssh_client_test.go @@ -152,6 +152,9 @@ func (m *mockSandboxResolver) Get(_ context.Context, _, name string) (*Sandbox, func (m *mockSandboxResolver) List(_ context.Context, _ string, _ ...ListOptions) ([]*Sandbox, error) { return nil, nil } +func (m *mockSandboxResolver) ListAll(_ context.Context, _ ...ListOptions) ([]*Sandbox, error) { + return nil, nil +} func (m *mockSandboxResolver) Delete(_ context.Context, _, _ string) error { return nil } func (m *mockSandboxResolver) AttachProvider(_ context.Context, _, _, _ string, _ uint64) (*AttachProviderResult, error) { return nil, nil diff --git a/sdk/go/openshell/v1/tcp_client_test.go b/sdk/go/openshell/v1/tcp_client_test.go index e445ab2fab..100a746ccb 100644 --- a/sdk/go/openshell/v1/tcp_client_test.go +++ b/sdk/go/openshell/v1/tcp_client_test.go @@ -999,6 +999,9 @@ func (r *flippableResolver) Create(context.Context, string, string, *SandboxSpec func (r *flippableResolver) List(context.Context, string, ...ListOptions) ([]*Sandbox, error) { panic("not implemented") } +func (r *flippableResolver) ListAll(context.Context, ...ListOptions) ([]*Sandbox, error) { + panic("not implemented") +} func (r *flippableResolver) Delete(context.Context, string, string) error { panic("not implemented") } diff --git a/sdk/go/openshell/v1/types/options.go b/sdk/go/openshell/v1/types/options.go index b0f6145999..0d9136a407 100644 --- a/sdk/go/openshell/v1/types/options.go +++ b/sdk/go/openshell/v1/types/options.go @@ -15,7 +15,6 @@ type ListOptions struct { Limit int Offset int LabelSelector string - AllWorkspaces bool } // WatchOptions configures watch behavior. diff --git a/sdk/go/openshell/v1/workspace_scope.go b/sdk/go/openshell/v1/workspace_scope.go new file mode 100644 index 0000000000..0ae2e8d52e --- /dev/null +++ b/sdk/go/openshell/v1/workspace_scope.go @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" + +func namedWorkspaceScope(workspace string) *dm.WorkspaceSelector { + return &dm.WorkspaceSelector{ + Selection: &dm.WorkspaceSelector_Workspace{Workspace: workspace}, + } +} + +func allWorkspacesScope() *dm.WorkspaceSelector { + return &dm.WorkspaceSelector{ + Selection: &dm.WorkspaceSelector_AllWorkspaces{AllWorkspaces: &dm.AllWorkspaces{}}, + } +} diff --git a/sdk/go/proto/datamodelv1/datamodel.pb.go b/sdk/go/proto/datamodelv1/datamodel.pb.go index a672bf3d8b..1b9eec16b4 100644 --- a/sdk/go/proto/datamodelv1/datamodel.pb.go +++ b/sdk/go/proto/datamodelv1/datamodel.pb.go @@ -75,6 +75,135 @@ func (WorkspacePhase) EnumDescriptor() ([]byte, []int) { return file_datamodel_proto_rawDescGZIP(), []int{0} } +// Selects the workspace scope for a public API request. +// +// Requests that operate on one workspace require a non-empty `workspace`. +// Cross-workspace list requests additionally accept `all_workspaces`. The +// containing request documents which selections it supports; an omitted +// selector is invalid for workspace-scoped operations. +type WorkspaceSelector struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Selection: + // + // *WorkspaceSelector_Workspace + // *WorkspaceSelector_AllWorkspaces + Selection isWorkspaceSelector_Selection `protobuf_oneof:"selection"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceSelector) Reset() { + *x = WorkspaceSelector{} + mi := &file_datamodel_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceSelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceSelector) ProtoMessage() {} + +func (x *WorkspaceSelector) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceSelector.ProtoReflect.Descriptor instead. +func (*WorkspaceSelector) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{0} +} + +func (x *WorkspaceSelector) GetSelection() isWorkspaceSelector_Selection { + if x != nil { + return x.Selection + } + return nil +} + +func (x *WorkspaceSelector) GetWorkspace() string { + if x != nil { + if x, ok := x.Selection.(*WorkspaceSelector_Workspace); ok { + return x.Workspace + } + } + return "" +} + +func (x *WorkspaceSelector) GetAllWorkspaces() *AllWorkspaces { + if x != nil { + if x, ok := x.Selection.(*WorkspaceSelector_AllWorkspaces); ok { + return x.AllWorkspaces + } + } + return nil +} + +type isWorkspaceSelector_Selection interface { + isWorkspaceSelector_Selection() +} + +type WorkspaceSelector_Workspace struct { + // One explicitly named workspace. Use `default` to select the gateway's + // default workspace; an empty name is invalid. + Workspace string `protobuf:"bytes,1,opt,name=workspace,proto3,oneof"` +} + +type WorkspaceSelector_AllWorkspaces struct { + // All workspaces the caller is authorized to access. Only supported by + // requests that explicitly document cross-workspace behavior. + AllWorkspaces *AllWorkspaces `protobuf:"bytes,2,opt,name=all_workspaces,json=allWorkspaces,proto3,oneof"` +} + +func (*WorkspaceSelector_Workspace) isWorkspaceSelector_Selection() {} + +func (*WorkspaceSelector_AllWorkspaces) isWorkspaceSelector_Selection() {} + +// Marker for the all-workspaces selector variant. +type AllWorkspaces struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AllWorkspaces) Reset() { + *x = AllWorkspaces{} + mi := &file_datamodel_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AllWorkspaces) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AllWorkspaces) ProtoMessage() {} + +func (x *AllWorkspaces) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AllWorkspaces.ProtoReflect.Descriptor instead. +func (*AllWorkspaces) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{1} +} + // Kubernetes-style metadata shared by all top-level OpenShell domain objects. // // This structure provides consistent metadata (identity, labels, annotations, @@ -110,7 +239,7 @@ type ObjectMeta struct { func (x *ObjectMeta) Reset() { *x = ObjectMeta{} - mi := &file_datamodel_proto_msgTypes[0] + mi := &file_datamodel_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -122,7 +251,7 @@ func (x *ObjectMeta) String() string { func (*ObjectMeta) ProtoMessage() {} func (x *ObjectMeta) ProtoReflect() protoreflect.Message { - mi := &file_datamodel_proto_msgTypes[0] + mi := &file_datamodel_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -135,7 +264,7 @@ func (x *ObjectMeta) ProtoReflect() protoreflect.Message { // Deprecated: Use ObjectMeta.ProtoReflect.Descriptor instead. func (*ObjectMeta) Descriptor() ([]byte, []int) { - return file_datamodel_proto_rawDescGZIP(), []int{0} + return file_datamodel_proto_rawDescGZIP(), []int{2} } func (x *ObjectMeta) GetId() string { @@ -204,7 +333,7 @@ type WorkspaceStatus struct { func (x *WorkspaceStatus) Reset() { *x = WorkspaceStatus{} - mi := &file_datamodel_proto_msgTypes[1] + mi := &file_datamodel_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -216,7 +345,7 @@ func (x *WorkspaceStatus) String() string { func (*WorkspaceStatus) ProtoMessage() {} func (x *WorkspaceStatus) ProtoReflect() protoreflect.Message { - mi := &file_datamodel_proto_msgTypes[1] + mi := &file_datamodel_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -229,7 +358,7 @@ func (x *WorkspaceStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceStatus.ProtoReflect.Descriptor instead. func (*WorkspaceStatus) Descriptor() ([]byte, []int) { - return file_datamodel_proto_rawDescGZIP(), []int{1} + return file_datamodel_proto_rawDescGZIP(), []int{3} } func (x *WorkspaceStatus) GetPhase() WorkspacePhase { @@ -255,7 +384,7 @@ type Workspace struct { func (x *Workspace) Reset() { *x = Workspace{} - mi := &file_datamodel_proto_msgTypes[2] + mi := &file_datamodel_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -267,7 +396,7 @@ func (x *Workspace) String() string { func (*Workspace) ProtoMessage() {} func (x *Workspace) ProtoReflect() protoreflect.Message { - mi := &file_datamodel_proto_msgTypes[2] + mi := &file_datamodel_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -280,7 +409,7 @@ func (x *Workspace) ProtoReflect() protoreflect.Message { // Deprecated: Use Workspace.ProtoReflect.Descriptor instead. func (*Workspace) Descriptor() ([]byte, []int) { - return file_datamodel_proto_rawDescGZIP(), []int{2} + return file_datamodel_proto_rawDescGZIP(), []int{4} } func (x *Workspace) GetMetadata() *ObjectMeta { @@ -313,7 +442,7 @@ type CredentialHandle struct { func (x *CredentialHandle) Reset() { *x = CredentialHandle{} - mi := &file_datamodel_proto_msgTypes[3] + mi := &file_datamodel_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -325,7 +454,7 @@ func (x *CredentialHandle) String() string { func (*CredentialHandle) ProtoMessage() {} func (x *CredentialHandle) ProtoReflect() protoreflect.Message { - mi := &file_datamodel_proto_msgTypes[3] + mi := &file_datamodel_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -338,7 +467,7 @@ func (x *CredentialHandle) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialHandle.ProtoReflect.Descriptor instead. func (*CredentialHandle) Descriptor() ([]byte, []int) { - return file_datamodel_proto_rawDescGZIP(), []int{3} + return file_datamodel_proto_rawDescGZIP(), []int{5} } func (x *CredentialHandle) GetDriver() string { @@ -389,7 +518,7 @@ type Provider struct { func (x *Provider) Reset() { *x = Provider{} - mi := &file_datamodel_proto_msgTypes[4] + mi := &file_datamodel_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -401,7 +530,7 @@ func (x *Provider) String() string { func (*Provider) ProtoMessage() {} func (x *Provider) ProtoReflect() protoreflect.Message { - mi := &file_datamodel_proto_msgTypes[4] + mi := &file_datamodel_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -414,7 +543,7 @@ func (x *Provider) ProtoReflect() protoreflect.Message { // Deprecated: Use Provider.ProtoReflect.Descriptor instead. func (*Provider) Descriptor() ([]byte, []int) { - return file_datamodel_proto_rawDescGZIP(), []int{4} + return file_datamodel_proto_rawDescGZIP(), []int{6} } func (x *Provider) GetMetadata() *ObjectMeta { @@ -470,7 +599,12 @@ var File_datamodel_proto protoreflect.FileDescriptor const file_datamodel_proto_rawDesc = "" + "\n" + - "\x0fdatamodel.proto\x12\x16openshell.datamodel.v1\x1a\roptions.proto\"\xeb\x03\n" + + "\x0fdatamodel.proto\x12\x16openshell.datamodel.v1\x1a\roptions.proto\"\x90\x01\n" + + "\x11WorkspaceSelector\x12\x1e\n" + + "\tworkspace\x18\x01 \x01(\tH\x00R\tworkspace\x12N\n" + + "\x0eall_workspaces\x18\x02 \x01(\v2%.openshell.datamodel.v1.AllWorkspacesH\x00R\rallWorkspacesB\v\n" + + "\tselection\"\x0f\n" + + "\rAllWorkspaces\"\xeb\x03\n" + "\n" + "ObjectMeta\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + @@ -537,40 +671,43 @@ func file_datamodel_proto_rawDescGZIP() []byte { } var file_datamodel_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_datamodel_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_datamodel_proto_msgTypes = make([]protoimpl.MessageInfo, 14) var file_datamodel_proto_goTypes = []any{ - (WorkspacePhase)(0), // 0: openshell.datamodel.v1.WorkspacePhase - (*ObjectMeta)(nil), // 1: openshell.datamodel.v1.ObjectMeta - (*WorkspaceStatus)(nil), // 2: openshell.datamodel.v1.WorkspaceStatus - (*Workspace)(nil), // 3: openshell.datamodel.v1.Workspace - (*CredentialHandle)(nil), // 4: openshell.datamodel.v1.CredentialHandle - (*Provider)(nil), // 5: openshell.datamodel.v1.Provider - nil, // 6: openshell.datamodel.v1.ObjectMeta.LabelsEntry - nil, // 7: openshell.datamodel.v1.ObjectMeta.AnnotationsEntry - nil, // 8: openshell.datamodel.v1.CredentialHandle.MetadataEntry - nil, // 9: openshell.datamodel.v1.Provider.CredentialsEntry - nil, // 10: openshell.datamodel.v1.Provider.ConfigEntry - nil, // 11: openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry - nil, // 12: openshell.datamodel.v1.Provider.CredentialHandlesEntry + (WorkspacePhase)(0), // 0: openshell.datamodel.v1.WorkspacePhase + (*WorkspaceSelector)(nil), // 1: openshell.datamodel.v1.WorkspaceSelector + (*AllWorkspaces)(nil), // 2: openshell.datamodel.v1.AllWorkspaces + (*ObjectMeta)(nil), // 3: openshell.datamodel.v1.ObjectMeta + (*WorkspaceStatus)(nil), // 4: openshell.datamodel.v1.WorkspaceStatus + (*Workspace)(nil), // 5: openshell.datamodel.v1.Workspace + (*CredentialHandle)(nil), // 6: openshell.datamodel.v1.CredentialHandle + (*Provider)(nil), // 7: openshell.datamodel.v1.Provider + nil, // 8: openshell.datamodel.v1.ObjectMeta.LabelsEntry + nil, // 9: openshell.datamodel.v1.ObjectMeta.AnnotationsEntry + nil, // 10: openshell.datamodel.v1.CredentialHandle.MetadataEntry + nil, // 11: openshell.datamodel.v1.Provider.CredentialsEntry + nil, // 12: openshell.datamodel.v1.Provider.ConfigEntry + nil, // 13: openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry + nil, // 14: openshell.datamodel.v1.Provider.CredentialHandlesEntry } var file_datamodel_proto_depIdxs = []int32{ - 6, // 0: openshell.datamodel.v1.ObjectMeta.labels:type_name -> openshell.datamodel.v1.ObjectMeta.LabelsEntry - 7, // 1: openshell.datamodel.v1.ObjectMeta.annotations:type_name -> openshell.datamodel.v1.ObjectMeta.AnnotationsEntry - 0, // 2: openshell.datamodel.v1.WorkspaceStatus.phase:type_name -> openshell.datamodel.v1.WorkspacePhase - 1, // 3: openshell.datamodel.v1.Workspace.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 2, // 4: openshell.datamodel.v1.Workspace.status:type_name -> openshell.datamodel.v1.WorkspaceStatus - 8, // 5: openshell.datamodel.v1.CredentialHandle.metadata:type_name -> openshell.datamodel.v1.CredentialHandle.MetadataEntry - 1, // 6: openshell.datamodel.v1.Provider.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 9, // 7: openshell.datamodel.v1.Provider.credentials:type_name -> openshell.datamodel.v1.Provider.CredentialsEntry - 10, // 8: openshell.datamodel.v1.Provider.config:type_name -> openshell.datamodel.v1.Provider.ConfigEntry - 11, // 9: openshell.datamodel.v1.Provider.credential_expires_at_ms:type_name -> openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry - 12, // 10: openshell.datamodel.v1.Provider.credential_handles:type_name -> openshell.datamodel.v1.Provider.CredentialHandlesEntry - 4, // 11: openshell.datamodel.v1.Provider.CredentialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 2, // 0: openshell.datamodel.v1.WorkspaceSelector.all_workspaces:type_name -> openshell.datamodel.v1.AllWorkspaces + 8, // 1: openshell.datamodel.v1.ObjectMeta.labels:type_name -> openshell.datamodel.v1.ObjectMeta.LabelsEntry + 9, // 2: openshell.datamodel.v1.ObjectMeta.annotations:type_name -> openshell.datamodel.v1.ObjectMeta.AnnotationsEntry + 0, // 3: openshell.datamodel.v1.WorkspaceStatus.phase:type_name -> openshell.datamodel.v1.WorkspacePhase + 3, // 4: openshell.datamodel.v1.Workspace.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 4, // 5: openshell.datamodel.v1.Workspace.status:type_name -> openshell.datamodel.v1.WorkspaceStatus + 10, // 6: openshell.datamodel.v1.CredentialHandle.metadata:type_name -> openshell.datamodel.v1.CredentialHandle.MetadataEntry + 3, // 7: openshell.datamodel.v1.Provider.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 11, // 8: openshell.datamodel.v1.Provider.credentials:type_name -> openshell.datamodel.v1.Provider.CredentialsEntry + 12, // 9: openshell.datamodel.v1.Provider.config:type_name -> openshell.datamodel.v1.Provider.ConfigEntry + 13, // 10: openshell.datamodel.v1.Provider.credential_expires_at_ms:type_name -> openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry + 14, // 11: openshell.datamodel.v1.Provider.credential_handles:type_name -> openshell.datamodel.v1.Provider.CredentialHandlesEntry + 6, // 12: openshell.datamodel.v1.Provider.CredentialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_datamodel_proto_init() } @@ -578,13 +715,17 @@ func file_datamodel_proto_init() { if File_datamodel_proto != nil { return } + file_datamodel_proto_msgTypes[0].OneofWrappers = []any{ + (*WorkspaceSelector_Workspace)(nil), + (*WorkspaceSelector_AllWorkspaces)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_datamodel_proto_rawDesc), len(file_datamodel_proto_rawDesc)), NumEnums: 1, - NumMessages: 12, + NumMessages: 14, NumExtensions: 0, NumServices: 0, }, diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 1716e006ee..6443c3a361 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -2477,16 +2477,16 @@ type CreateSandboxRequest struct { Labels map[string]string `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Optional annotations for the sandbox (non-selector metadata). Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Workspace for the sandbox. Empty defaults to "default". - Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` // One-shot launch hint indicating that the creating client will attach to // the canonical main process. The supervisor keeps the terminal transport // alive until that attachment connects and closes naturally. AwaitMainProcessAttachment bool `protobuf:"varint,6,opt,name=await_main_process_attachment,json=awaitMainProcessAttachment,proto3" json:"await_main_process_attachment,omitempty"` // Workspace-scoped SandboxWorkloadTemplate name to resolve at creation time. WorkloadTemplateName string `protobuf:"bytes,7,opt,name=workload_template_name,json=workloadTemplateName,proto3" json:"workload_template_name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace for the sandbox. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,8,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateSandboxRequest) Reset() { @@ -2547,13 +2547,6 @@ func (x *CreateSandboxRequest) GetAnnotations() map[string]string { return nil } -func (x *CreateSandboxRequest) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" -} - func (x *CreateSandboxRequest) GetAwaitMainProcessAttachment() bool { if x != nil { return x.AwaitMainProcessAttachment @@ -2568,13 +2561,20 @@ func (x *CreateSandboxRequest) GetWorkloadTemplateName() string { return "" } +func (x *CreateSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + type CreateSandboxTemplateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Template *SandboxWorkloadTemplate `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` - // Workspace for the template. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace for the template. The all-workspaces selection is invalid. + 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 *CreateSandboxTemplateRequest) Reset() { @@ -2614,20 +2614,20 @@ func (x *CreateSandboxTemplateRequest) GetTemplate() *SandboxWorkloadTemplate { return nil } -func (x *CreateSandboxTemplateRequest) GetWorkspace() string { +func (x *CreateSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type GetSandboxTemplateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *GetSandboxTemplateRequest) Reset() { @@ -2667,25 +2667,23 @@ func (x *GetSandboxTemplateRequest) GetName() string { return "" } -func (x *GetSandboxTemplateRequest) GetWorkspace() string { +func (x *GetSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type ListSandboxTemplatesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - // List across all workspaces. Mutually exclusive with workspace. - AllWorkspaces bool `protobuf:"varint,4,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` // Optional label selector in key=value comma-separated form. LabelSelector string `protobuf:"bytes,5,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 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 } func (x *ListSandboxTemplatesRequest) Reset() { @@ -2732,34 +2730,27 @@ func (x *ListSandboxTemplatesRequest) GetOffset() uint32 { return 0 } -func (x *ListSandboxTemplatesRequest) GetWorkspace() string { +func (x *ListSandboxTemplatesRequest) GetLabelSelector() string { if x != nil { - return x.Workspace + return x.LabelSelector } return "" } -func (x *ListSandboxTemplatesRequest) GetAllWorkspaces() bool { - if x != nil { - return x.AllWorkspaces - } - return false -} - -func (x *ListSandboxTemplatesRequest) GetLabelSelector() string { +func (x *ListSandboxTemplatesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.LabelSelector + return x.WorkspaceScope } - return "" + return nil } type DeleteSandboxTemplateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *DeleteSandboxTemplateRequest) Reset() { @@ -2799,11 +2790,11 @@ func (x *DeleteSandboxTemplateRequest) GetName() string { return "" } -func (x *DeleteSandboxTemplateRequest) GetWorkspace() string { +func (x *DeleteSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type SandboxTemplateResponse struct { @@ -2941,17 +2932,17 @@ func (x *DeleteSandboxTemplateResponse) GetDeleted() bool { // Request a gateway-owned staging slot for a local rootfs tar archive. type BeginRootfsTarStagingRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Workspace that will own the sandbox created from this archive. Empty - // defaults to "default", matching CreateSandboxRequest.workspace. - Workspace string `protobuf:"bytes,1,opt,name=workspace,proto3" json:"workspace,omitempty"` // Base file name of the local archive. The gateway uses it only to name the // staged file; path separators and traversal components are rejected. FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` // Size of the local archive in bytes, checked against the driver limit // before the gateway allocates a slot. - SizeBytes uint64 `protobuf:"varint,3,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + SizeBytes uint64 `protobuf:"varint,3,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + // Explicit workspace that will own the sandbox created from this archive. + // The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BeginRootfsTarStagingRequest) Reset() { @@ -2984,13 +2975,6 @@ func (*BeginRootfsTarStagingRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{39} } -func (x *BeginRootfsTarStagingRequest) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" -} - func (x *BeginRootfsTarStagingRequest) GetFileName() string { if x != nil { return x.FileName @@ -3005,6 +2989,13 @@ func (x *BeginRootfsTarStagingRequest) GetSizeBytes() uint64 { return 0 } +func (x *BeginRootfsTarStagingRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + // Gateway-issued staging slot. type BeginRootfsTarStagingResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3085,10 +3076,10 @@ 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *GetSandboxRequest) Reset() { @@ -3128,11 +3119,11 @@ func (x *GetSandboxRequest) GetName() string { return "" } -func (x *GetSandboxRequest) GetWorkspace() string { +func (x *GetSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // List sandboxes request. @@ -3142,12 +3133,10 @@ type ListSandboxesRequest struct { Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` // Optional label selector for filtering (format: "key1=value1,key2=value2"). LabelSelector string `protobuf:"bytes,3,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - // List across all workspaces. Mutually exclusive with workspace. - AllWorkspaces bool `protobuf:"varint,5,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 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 } func (x *ListSandboxesRequest) Reset() { @@ -3201,18 +3190,11 @@ func (x *ListSandboxesRequest) GetLabelSelector() string { return "" } -func (x *ListSandboxesRequest) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" -} - -func (x *ListSandboxesRequest) GetAllWorkspaces() bool { +func (x *ListSandboxesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.AllWorkspaces + return x.WorkspaceScope } - return false + return nil } // List providers attached to a sandbox request. @@ -3220,10 +3202,10 @@ 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *ListSandboxProvidersRequest) Reset() { @@ -3263,11 +3245,11 @@ func (x *ListSandboxProvidersRequest) GetSandboxName() string { return "" } -func (x *ListSandboxProvidersRequest) GetWorkspace() string { +func (x *ListSandboxProvidersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Attach provider to sandbox request. @@ -3282,10 +3264,10 @@ type AttachSandboxProviderRequest struct { // 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 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 } func (x *AttachSandboxProviderRequest) Reset() { @@ -3339,11 +3321,11 @@ func (x *AttachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { return 0 } -func (x *AttachSandboxProviderRequest) GetWorkspace() string { +func (x *AttachSandboxProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Detach provider from sandbox request. @@ -3358,10 +3340,10 @@ type DetachSandboxProviderRequest struct { // 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 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 } func (x *DetachSandboxProviderRequest) Reset() { @@ -3415,11 +3397,11 @@ func (x *DetachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { return 0 } -func (x *DetachSandboxProviderRequest) GetWorkspace() string { +func (x *DetachSandboxProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Delete sandbox request. @@ -3427,10 +3409,10 @@ 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *DeleteSandboxRequest) Reset() { @@ -3470,11 +3452,11 @@ func (x *DeleteSandboxRequest) GetName() string { return "" } -func (x *DeleteSandboxRequest) GetWorkspace() string { +func (x *DeleteSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Stop sandbox request. @@ -3482,10 +3464,10 @@ 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *StopSandboxRequest) Reset() { @@ -3525,11 +3507,11 @@ func (x *StopSandboxRequest) GetName() string { return "" } -func (x *StopSandboxRequest) GetWorkspace() string { +func (x *StopSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Start sandbox request. @@ -3537,10 +3519,10 @@ 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *StartSandboxRequest) Reset() { @@ -3580,11 +3562,11 @@ func (x *StartSandboxRequest) GetName() string { return "" } -func (x *StartSandboxRequest) GetWorkspace() string { +func (x *StartSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Sandbox response. @@ -4042,10 +4024,10 @@ type ExposeServiceRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,6,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExposeServiceRequest) Reset() { @@ -4106,11 +4088,11 @@ func (x *ExposeServiceRequest) GetDomain() bool { return false } -func (x *ExposeServiceRequest) GetWorkspace() string { +func (x *ExposeServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Request to fetch an exposed sandbox service endpoint. @@ -4120,10 +4102,10 @@ type GetServiceRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetServiceRequest) Reset() { @@ -4170,11 +4152,11 @@ func (x *GetServiceRequest) GetService() string { return "" } -func (x *GetServiceRequest) GetWorkspace() string { +func (x *GetServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Request to list exposed sandbox service endpoints. @@ -4186,12 +4168,10 @@ type ListServicesRequest struct { Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` // Page offset. Offset uint32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - // List across all workspaces. Mutually exclusive with workspace. - AllWorkspaces bool `protobuf:"varint,5,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 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 } func (x *ListServicesRequest) Reset() { @@ -4245,18 +4225,11 @@ func (x *ListServicesRequest) GetOffset() uint32 { return 0 } -func (x *ListServicesRequest) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" -} - -func (x *ListServicesRequest) GetAllWorkspaces() bool { +func (x *ListServicesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.AllWorkspaces + return x.WorkspaceScope } - return false + return nil } // Response containing exposed sandbox service endpoints. @@ -4311,10 +4284,10 @@ type DeleteServiceRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeleteServiceRequest) Reset() { @@ -4361,11 +4334,11 @@ func (x *DeleteServiceRequest) GetService() string { return "" } -func (x *DeleteServiceRequest) GetWorkspace() string { +func (x *DeleteServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Response for deleting an exposed sandbox service endpoint. @@ -5858,10 +5831,10 @@ func (x *SandboxStreamWarning) GetMessage() string { type CreateProviderRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` - // Workspace for the provider. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace for the provider. The all-workspaces selection is invalid. + 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 *CreateProviderRequest) Reset() { @@ -5901,21 +5874,21 @@ func (x *CreateProviderRequest) GetProvider() *datamodelv1.Provider { return nil } -func (x *CreateProviderRequest) GetWorkspace() string { +func (x *CreateProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Get provider request. type GetProviderRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *GetProviderRequest) Reset() { @@ -5955,11 +5928,11 @@ func (x *GetProviderRequest) GetName() string { return "" } -func (x *GetProviderRequest) GetWorkspace() string { +func (x *GetProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // List providers request. @@ -5967,12 +5940,10 @@ type ListProvidersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - // List across all workspaces. Mutually exclusive with workspace. - AllWorkspaces bool `protobuf:"varint,4,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit named or all-workspaces scope. + 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 *ListProvidersRequest) Reset() { @@ -6019,18 +5990,11 @@ func (x *ListProvidersRequest) GetOffset() uint32 { return 0 } -func (x *ListProvidersRequest) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" -} - -func (x *ListProvidersRequest) GetAllWorkspaces() bool { +func (x *ListProvidersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.AllWorkspaces + return x.WorkspaceScope } - return false + return nil } // Update provider request. @@ -6040,10 +6004,10 @@ type UpdateProviderRequest struct { // Optional per-credential expiry timestamps to merge into the provider. // A zero value removes the expiry for that credential. CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,2,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateProviderRequest) Reset() { @@ -6090,21 +6054,21 @@ func (x *UpdateProviderRequest) GetCredentialExpiresAtMs() map[string]int64 { return nil } -func (x *UpdateProviderRequest) GetWorkspace() string { +func (x *UpdateProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Delete provider request. type DeleteProviderRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *DeleteProviderRequest) Reset() { @@ -6144,11 +6108,11 @@ func (x *DeleteProviderRequest) GetName() string { return "" } -func (x *DeleteProviderRequest) GetWorkspace() string { +func (x *DeleteProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Provider response. @@ -7301,10 +7265,10 @@ type GetProviderRefreshStatusRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` CredentialKey string `protobuf:"bytes,2,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProviderRefreshStatusRequest) Reset() { @@ -7351,11 +7315,11 @@ func (x *GetProviderRefreshStatusRequest) GetCredentialKey() string { return "" } -func (x *GetProviderRefreshStatusRequest) GetWorkspace() string { +func (x *GetProviderRefreshStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type GetProviderRefreshStatusResponse struct { @@ -7413,10 +7377,10 @@ type ConfigureProviderRefreshRequest struct { // the authoritative provider profile and refresh strategy. SecretMaterialKeys []string `protobuf:"bytes,5,rep,name=secret_material_keys,json=secretMaterialKeys,proto3" json:"secret_material_keys,omitempty"` ExpiresAtMs *int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3,oneof" json:"expires_at_ms,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,7,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,8,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ConfigureProviderRefreshRequest) Reset() { @@ -7491,11 +7455,11 @@ func (x *ConfigureProviderRefreshRequest) GetExpiresAtMs() int64 { return 0 } -func (x *ConfigureProviderRefreshRequest) GetWorkspace() string { +func (x *ConfigureProviderRefreshRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type ConfigureProviderRefreshResponse struct { @@ -7546,10 +7510,10 @@ type RotateProviderCredentialRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` CredentialKey string `protobuf:"bytes,2,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RotateProviderCredentialRequest) Reset() { @@ -7596,11 +7560,11 @@ func (x *RotateProviderCredentialRequest) GetCredentialKey() string { return "" } -func (x *RotateProviderCredentialRequest) GetWorkspace() string { +func (x *RotateProviderCredentialRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type RotateProviderCredentialResponse struct { @@ -7651,10 +7615,10 @@ type DeleteProviderRefreshRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` CredentialKey string `protobuf:"bytes,2,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeleteProviderRefreshRequest) Reset() { @@ -7701,11 +7665,11 @@ func (x *DeleteProviderRefreshRequest) GetCredentialKey() string { return "" } -func (x *DeleteProviderRefreshRequest) GetWorkspace() string { +func (x *DeleteProviderRefreshRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type DeleteProviderRefreshResponse struct { @@ -8953,10 +8917,11 @@ 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"` - // Workspace scope. Empty defaults to "default". Ignored for global-scoped updates. - Workspace string `protobuf:"bytes,10,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope for sandbox-scoped updates. Omit only when + // `global` is true; the all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,11,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateConfigRequest) Reset() { @@ -9052,11 +9017,11 @@ func (x *UpdateConfigRequest) GetAnnotations() map[string]string { return nil } -func (x *UpdateConfigRequest) GetWorkspace() string { +func (x *UpdateConfigRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type PolicyMergeOperation struct { @@ -9624,10 +9589,11 @@ type GetSandboxPolicyStatusRequest struct { 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"` - // Workspace scope. Empty defaults to "default". Ignored when global is true. - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope for sandbox-scoped queries. Omit only when + // `global` is true; 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 } func (x *GetSandboxPolicyStatusRequest) Reset() { @@ -9681,11 +9647,11 @@ func (x *GetSandboxPolicyStatusRequest) GetGlobal() bool { return false } -func (x *GetSandboxPolicyStatusRequest) GetWorkspace() string { +func (x *GetSandboxPolicyStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Get sandbox policy status response. @@ -9752,10 +9718,11 @@ type ListSandboxPoliciesRequest struct { Offset uint32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` // List global policy revisions instead of sandbox-scoped ones. Global bool `protobuf:"varint,4,opt,name=global,proto3" json:"global,omitempty"` - // Workspace scope. Empty defaults to "default". Ignored when global is true. - Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope for sandbox-scoped queries. Omit only when + // `global` is true; the all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,6,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListSandboxPoliciesRequest) Reset() { @@ -9816,11 +9783,11 @@ func (x *ListSandboxPoliciesRequest) GetGlobal() bool { return false } -func (x *ListSandboxPoliciesRequest) GetWorkspace() string { +func (x *ListSandboxPoliciesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // List sandbox policies response. @@ -10107,10 +10074,10 @@ type GetSandboxLogsRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,6,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,7,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSandboxLogsRequest) Reset() { @@ -10178,11 +10145,11 @@ func (x *GetSandboxLogsRequest) GetMinLevel() string { return "" } -func (x *GetSandboxLogsRequest) GetWorkspace() string { +func (x *GetSandboxLogsRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } // Batch of log lines pushed from sandbox to server. @@ -12343,10 +12310,10 @@ type GetDraftPolicyRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetDraftPolicyRequest) Reset() { @@ -12393,11 +12360,11 @@ func (x *GetDraftPolicyRequest) GetStatusFilter() string { return "" } -func (x *GetDraftPolicyRequest) GetWorkspace() string { +func (x *GetDraftPolicyRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type GetDraftPolicyResponse struct { @@ -12479,13 +12446,13 @@ type ApproveDraftChunkRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,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"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + 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. + 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 *ApproveDraftChunkRequest) Reset() { @@ -12532,18 +12499,18 @@ func (x *ApproveDraftChunkRequest) GetChunkId() string { return "" } -func (x *ApproveDraftChunkRequest) GetWorkspace() string { +func (x *ApproveDraftChunkRequest) GetReviewToken() string { if x != nil { - return x.Workspace + return x.ReviewToken } return "" } -func (x *ApproveDraftChunkRequest) GetReviewToken() string { +func (x *ApproveDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.ReviewToken + return x.WorkspaceScope } - return "" + return nil } type ApproveDraftChunkResponse struct { @@ -12609,10 +12576,10 @@ type RejectDraftChunkRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 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 } func (x *RejectDraftChunkRequest) Reset() { @@ -12666,11 +12633,11 @@ func (x *RejectDraftChunkRequest) GetReason() string { return "" } -func (x *RejectDraftChunkRequest) GetWorkspace() string { +func (x *RejectDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type RejectDraftChunkResponse struct { @@ -12768,13 +12735,13 @@ type ApproveAllDraftChunksRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,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"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Approvals []*DraftChunkApproval `protobuf:"bytes,4,rep,name=approvals,proto3" json:"approvals,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 } func (x *ApproveAllDraftChunksRequest) Reset() { @@ -12821,16 +12788,16 @@ func (x *ApproveAllDraftChunksRequest) GetIncludeSecurityFlagged() bool { return false } -func (x *ApproveAllDraftChunksRequest) GetWorkspace() string { +func (x *ApproveAllDraftChunksRequest) GetApprovals() []*DraftChunkApproval { if x != nil { - return x.Workspace + return x.Approvals } - return "" + return nil } -func (x *ApproveAllDraftChunksRequest) GetApprovals() []*DraftChunkApproval { +func (x *ApproveAllDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Approvals + return x.WorkspaceScope } return nil } @@ -12917,10 +12884,10 @@ type EditDraftChunkRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 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 } func (x *EditDraftChunkRequest) Reset() { @@ -12974,11 +12941,11 @@ func (x *EditDraftChunkRequest) GetProposedRule() *sandboxv1.NetworkPolicyRule { return nil } -func (x *EditDraftChunkRequest) GetWorkspace() string { +func (x *EditDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type EditDraftChunkResponse struct { @@ -13024,10 +12991,10 @@ type UndoDraftChunkRequest struct { 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"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UndoDraftChunkRequest) Reset() { @@ -13074,11 +13041,11 @@ func (x *UndoDraftChunkRequest) GetChunkId() string { return "" } -func (x *UndoDraftChunkRequest) GetWorkspace() string { +func (x *UndoDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type UndoDraftChunkResponse struct { @@ -13140,10 +13107,10 @@ type ClearDraftChunksRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Sandbox name. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *ClearDraftChunksRequest) Reset() { @@ -13183,11 +13150,11 @@ func (x *ClearDraftChunksRequest) GetName() string { return "" } -func (x *ClearDraftChunksRequest) GetWorkspace() string { +func (x *ClearDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type ClearDraftChunksResponse struct { @@ -13240,10 +13207,10 @@ type GetDraftHistoryRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Sandbox name. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Explicit workspace scope. The all-workspaces selection is invalid. + 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 *GetDraftHistoryRequest) Reset() { @@ -13283,11 +13250,11 @@ func (x *GetDraftHistoryRequest) GetName() string { return "" } -func (x *GetDraftHistoryRequest) GetWorkspace() string { +func (x *GetDraftHistoryRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { if x != nil { - return x.Workspace + return x.WorkspaceScope } - return "" + return nil } type DraftHistoryEntry struct { @@ -14395,84 +14362,82 @@ const file_openshell_proto_rawDesc = "" + "\bmetadata\x18\x06 \x03(\v2).openshell.v1.PlatformEvent.MetadataEntryR\bmetadata\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x8a\x04\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd1\x04\n" + "\x14CreateSandboxRequest\x12-\n" + "\x04spec\x18\x01 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12F\n" + "\x06labels\x18\x03 \x03(\v2..openshell.v1.CreateSandboxRequest.LabelsEntryR\x06labels\x12U\n" + - "\vannotations\x18\x04 \x03(\v23.openshell.v1.CreateSandboxRequest.AnnotationsEntryR\vannotations\x12\x1c\n" + - "\tworkspace\x18\x05 \x01(\tR\tworkspace\x12A\n" + + "\vannotations\x18\x04 \x03(\v23.openshell.v1.CreateSandboxRequest.AnnotationsEntryR\vannotations\x12A\n" + "\x1dawait_main_process_attachment\x18\x06 \x01(\bR\x1aawaitMainProcessAttachment\x124\n" + - "\x16workload_template_name\x18\a \x01(\tR\x14workloadTemplateName\x1a9\n" + + "\x16workload_template_name\x18\a \x01(\tR\x14workloadTemplateName\x12R\n" + + "\x0fworkspace_scope\x18\b \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x7f\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x05\x10\x06R\tworkspace\"\xc6\x01\n" + "\x1cCreateSandboxTemplateRequest\x12A\n" + - "\btemplate\x18\x01 \x01(\v2%.openshell.v1.SandboxWorkloadTemplateR\btemplate\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"M\n" + + "\btemplate\x18\x01 \x01(\v2%.openshell.v1.SandboxWorkloadTemplateR\btemplate\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\x94\x01\n" + "\x19GetSandboxTemplateRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\xb7\x01\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\"\xed\x01\n" + "\x1bListSandboxTemplatesRequest\x12\x14\n" + "\x05limit\x18\x01 \x01(\rR\x05limit\x12\x16\n" + - "\x06offset\x18\x02 \x01(\rR\x06offset\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12%\n" + - "\x0eall_workspaces\x18\x04 \x01(\bR\rallWorkspaces\x12%\n" + - "\x0elabel_selector\x18\x05 \x01(\tR\rlabelSelector\"P\n" + + "\x06offset\x18\x02 \x01(\rR\x06offset\x12%\n" + + "\x0elabel_selector\x18\x05 \x01(\tR\rlabelSelector\x12R\n" + + "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04J\x04\b\x04\x10\x05R\tworkspaceR\x0eall_workspaces\"\x97\x01\n" + "\x1cDeleteSandboxTemplateRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\\\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\"\\\n" + "\x17SandboxTemplateResponse\x12A\n" + "\btemplate\x18\x01 \x01(\v2%.openshell.v1.SandboxWorkloadTemplateR\btemplate\"c\n" + "\x1cListSandboxTemplatesResponse\x12C\n" + "\ttemplates\x18\x01 \x03(\v2%.openshell.v1.SandboxWorkloadTemplateR\ttemplates\"9\n" + "\x1dDeleteSandboxTemplateResponse\x12\x18\n" + - "\adeleted\x18\x01 \x01(\bR\adeleted\"x\n" + - "\x1cBeginRootfsTarStagingRequest\x12\x1c\n" + - "\tworkspace\x18\x01 \x01(\tR\tworkspace\x12\x1b\n" + + "\adeleted\x18\x01 \x01(\bR\adeleted\"\xbf\x01\n" + + "\x1cBeginRootfsTarStagingRequest\x12\x1b\n" + "\tfile_name\x18\x02 \x01(\tR\bfileName\x12\x1d\n" + "\n" + - "size_bytes\x18\x03 \x01(\x04R\tsizeBytes\"\xa6\x01\n" + + "size_bytes\x18\x03 \x01(\x04R\tsizeBytes\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x01\x10\x02R\tworkspace\"\xa6\x01\n" + "\x1dBeginRootfsTarStagingResponse\x12#\n" + "\rstaging_token\x18\x01 \x01(\tR\fstagingToken\x12\x1f\n" + "\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\"E\n" + + "\rexpires_at_ms\x18\x04 \x01(\x03R\vexpiresAtMs\"\x8c\x01\n" + "\x11GetSandboxRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\xb0\x01\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\"\xe6\x01\n" + "\x14ListSandboxesRequest\x12\x14\n" + "\x05limit\x18\x01 \x01(\rR\x05limit\x12\x16\n" + "\x06offset\x18\x02 \x01(\rR\x06offset\x12%\n" + - "\x0elabel_selector\x18\x03 \x01(\tR\rlabelSelector\x12\x1c\n" + - "\tworkspace\x18\x04 \x01(\tR\tworkspace\x12%\n" + - "\x0eall_workspaces\x18\x05 \x01(\bR\rallWorkspaces\"^\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\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\xc0\x01\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" + "\rprovider_name\x18\x02 \x01(\tR\fproviderName\x12:\n" + - "\x19expected_resource_version\x18\x03 \x01(\x04R\x17expectedResourceVersion\x12\x1c\n" + - "\tworkspace\x18\x04 \x01(\tR\tworkspace\"\xc0\x01\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" + "\rprovider_name\x18\x02 \x01(\tR\fproviderName\x12:\n" + - "\x19expected_resource_version\x18\x03 \x01(\x04R\x17expectedResourceVersion\x12\x1c\n" + - "\tworkspace\x18\x04 \x01(\tR\tworkspace\"H\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\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"F\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\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"G\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\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"B\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" + "\x0fSandboxResponse\x12/\n" + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\"L\n" + "\x15ListSandboxesResponse\x123\n" + @@ -14498,30 +14463,29 @@ const file_openshell_proto_rawDesc = "" + "\fgateway_port\x18\x04 \x01(\rR\vgatewayPort\x12%\n" + "\x0egateway_scheme\x18\x05 \x01(\tR\rgatewayScheme\x120\n" + "\x14host_key_fingerprint\x18\a \x01(\tR\x12hostKeyFingerprint\x12\"\n" + - "\rexpires_at_ms\x18\b \x01(\x03R\vexpiresAtMs\"\xa1\x01\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\x12\x1c\n" + - "\tworkspace\x18\x05 \x01(\tR\tworkspace\"e\n" + + "\x06domain\x18\x04 \x01(\bR\x06domain\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\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"\xa2\x01\n" + + "\aservice\x18\x02 \x01(\tR\aservice\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"\xd8\x01\n" + "\x13ListServicesRequest\x12\x18\n" + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x14\n" + "\x05limit\x18\x02 \x01(\rR\x05limit\x12\x16\n" + - "\x06offset\x18\x03 \x01(\rR\x06offset\x12\x1c\n" + - "\tworkspace\x18\x04 \x01(\tR\tworkspace\x12%\n" + - "\x0eall_workspaces\x18\x05 \x01(\bR\rallWorkspaces\"Y\n" + + "\x06offset\x18\x03 \x01(\rR\x06offset\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\"Y\n" + "\x14ListServicesResponse\x12A\n" + - "\bservices\x18\x01 \x03(\v2%.openshell.v1.ServiceEndpointResponseR\bservices\"h\n" + + "\bservices\x18\x01 \x03(\v2%.openshell.v1.ServiceEndpointResponseR\bservices\"\xaf\x01\n" + "\x14DeleteServiceRequest\x12\x18\n" + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + - "\aservice\x18\x02 \x01(\tR\aservice\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"1\n" + + "\aservice\x18\x02 \x01(\tR\aservice\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" + "\x0fServiceEndpoint\x12>\n" + @@ -14632,28 +14596,27 @@ const file_openshell_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"0\n" + "\x14SandboxStreamWarning\x12\x18\n" + - "\amessage\x18\x01 \x01(\tR\amessage\"s\n" + + "\amessage\x18\x01 \x01(\tR\amessage\"\xba\x01\n" + "\x15CreateProviderRequest\x12<\n" + - "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"F\n" + + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\x8d\x01\n" + "\x12GetProviderRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\x89\x01\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\"\xbf\x01\n" + "\x14ListProvidersRequest\x12\x14\n" + "\x05limit\x18\x01 \x01(\rR\x05limit\x12\x16\n" + - "\x06offset\x18\x02 \x01(\rR\x06offset\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12%\n" + - "\x0eall_workspaces\x18\x04 \x01(\bR\rallWorkspaces\"\xb6\x02\n" + + "\x06offset\x18\x02 \x01(\rR\x06offset\x12R\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04J\x04\b\x04\x10\x05R\tworkspaceR\x0eall_workspaces\"\xfd\x02\n" + "\x15UpdateProviderRequest\x12<\n" + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12w\n" + - "\x18credential_expires_at_ms\x18\x02 \x03(\v2>.openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntryR\x15credentialExpiresAtMs\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\x1aH\n" + + "\x18credential_expires_at_ms\x18\x02 \x03(\v2>.openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntryR\x15credentialExpiresAtMs\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x1aH\n" + "\x1aCredentialExpiresAtMsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"I\n" + + "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01J\x04\b\x03\x10\x04R\tworkspace\"\x90\x01\n" + "\x15DeleteProviderRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"P\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\"P\n" + "\x10ProviderResponse\x12<\n" + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\"W\n" + "\x15ListProvidersResponse\x12>\n" + @@ -14752,37 +14715,37 @@ const file_openshell_proto_rawDesc = "" + "\x16provider_error_subtype\x18\f \x01(\tR\x14providerErrorSubtype\x12'\n" + "\x10last_error_at_ms\x18\r \x01(\x03R\rlastErrorAtMs\"<\n" + "\x18ProviderProfileDiscovery\x12 \n" + - "\vcredentials\x18\x01 \x03(\tR\vcredentials\"\x82\x01\n" + + "\vcredentials\x18\x01 \x03(\tR\vcredentials\"\xc9\x01\n" + "\x1fGetProviderRefreshStatusRequest\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + - "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"s\n" + + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"s\n" + " GetProviderRefreshStatusResponse\x12O\n" + - "\vcredentials\x18\x01 \x03(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\vcredentials\"\xd8\x03\n" + + "\vcredentials\x18\x01 \x03(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\vcredentials\"\x9f\x04\n" + "\x1fConfigureProviderRefreshRequest\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12K\n" + "\bstrategy\x18\x03 \x01(\x0e2/.openshell.v1.ProviderCredentialRefreshStrategyR\bstrategy\x12]\n" + "\bmaterial\x18\x04 \x03(\v2;.openshell.v1.ConfigureProviderRefreshRequest.MaterialEntryB\x04\x88\xb5\x18\x01R\bmaterial\x120\n" + "\x14secret_material_keys\x18\x05 \x03(\tR\x12secretMaterialKeys\x12'\n" + - "\rexpires_at_ms\x18\x06 \x01(\x03H\x00R\vexpiresAtMs\x88\x01\x01\x12\x1c\n" + - "\tworkspace\x18\a \x01(\tR\tworkspace\x1a;\n" + + "\rexpires_at_ms\x18\x06 \x01(\x03H\x00R\vexpiresAtMs\x88\x01\x01\x12R\n" + + "\x0fworkspace_scope\x18\b \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x1a;\n" + "\rMaterialEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x10\n" + - "\x0e_expires_at_ms\"i\n" + + "\x0e_expires_at_msJ\x04\b\a\x10\bR\tworkspace\"i\n" + " ConfigureProviderRefreshResponse\x12E\n" + - "\x06status\x18\x01 \x01(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\x06status\"\x82\x01\n" + + "\x06status\x18\x01 \x01(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\x06status\"\xc9\x01\n" + "\x1fRotateProviderCredentialRequest\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + - "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"i\n" + + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"i\n" + " RotateProviderCredentialResponse\x12E\n" + - "\x06status\x18\x01 \x01(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\x06status\"\x7f\n" + + "\x06status\x18\x01 \x01(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\x06status\"\xc6\x01\n" + "\x1cDeleteProviderRefreshRequest\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + - "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"9\n" + + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x03\x10\x04R\tworkspace\"9\n" + "\x1dDeleteProviderRefreshResponse\x12\x18\n" + "\adeleted\x18\x01 \x01(\bR\adeleted\"\xd8\x05\n" + "\x0fProviderProfile\x12\x0e\n" + @@ -14878,7 +14841,7 @@ const file_openshell_proto_rawDesc = "" + "\n" + "expires_in\x18\x02 \x01(\x03R\texpiresIn\x12\x1d\n" + "\n" + - "token_type\x18\x03 \x01(\tR\ttokenType\"\xce\x04\n" + + "token_type\x18\x03 \x01(\tR\ttokenType\"\x95\x05\n" + "\x13UpdateConfigRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + "\x06policy\x18\x02 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x1f\n" + @@ -14889,12 +14852,12 @@ 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\x12\x1c\n" + - "\tworkspace\x18\n" + - " \x01(\tR\tworkspace\x1a>\n" + + "\vannotations\x18\t \x03(\v22.openshell.v1.UpdateConfigRequest.AnnotationsEntryR\vannotations\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\x01\"\xc7\x03\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\n" + + "\x10\vR\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" + @@ -14935,21 +14898,21 @@ 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\"\x83\x01\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" + "\aversion\x18\x02 \x01(\rR\aversion\x12\x16\n" + - "\x06global\x18\x03 \x01(\bR\x06global\x12\x1c\n" + - "\tworkspace\x18\x04 \x01(\tR\tworkspace\"\x88\x01\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" + "\x1eGetSandboxPolicyStatusResponse\x12?\n" + "\brevision\x18\x01 \x01(\v2#.openshell.v1.SandboxPolicyRevisionR\brevision\x12%\n" + - "\x0eactive_version\x18\x02 \x01(\rR\ractiveVersion\"\x94\x01\n" + + "\x0eactive_version\x18\x02 \x01(\rR\ractiveVersion\"\xdb\x01\n" + "\x1aListSandboxPoliciesRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05limit\x18\x02 \x01(\rR\x05limit\x12\x16\n" + "\x06offset\x18\x03 \x01(\rR\x06offset\x12\x16\n" + - "\x06global\x18\x04 \x01(\bR\x06global\x12\x1c\n" + - "\tworkspace\x18\x05 \x01(\tR\tworkspace\"`\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\"`\n" + "\x1bListSandboxPoliciesResponse\x12A\n" + "\trevisions\x18\x01 \x03(\v2#.openshell.v1.SandboxPolicyRevisionR\trevisions\"\xa7\x01\n" + "\x19ReportPolicyStatusRequest\x12\x1d\n" + @@ -14976,15 +14939,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\"\xbc\x01\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" + "\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\x12\x1c\n" + - "\tworkspace\x18\x06 \x01(\tR\tworkspace\"i\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" + "\x16PushSandboxLogsRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x120\n" + @@ -15149,67 +15112,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\"n\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\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"\xc8\x01\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" + "\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\"\x8a\x01\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" + - "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12!\n" + - "\freview_token\x18\x04 \x01(\tR\vreviewToken\"c\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" + "\x19ApproveDraftChunkResponse\x12%\n" + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + - "policyHash\"~\n" + + "policyHash\"\xc5\x01\n" + "\x17RejectDraftChunkRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x16\n" + - "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x1c\n" + - "\tworkspace\x18\x04 \x01(\tR\tworkspace\"\x1a\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" + "\x18RejectDraftChunkResponse\"R\n" + "\x12DraftChunkApproval\x12\x19\n" + "\bchunk_id\x18\x01 \x01(\tR\achunkId\x12!\n" + - "\freview_token\x18\x02 \x01(\tR\vreviewToken\"\xca\x01\n" + + "\freview_token\x18\x02 \x01(\tR\vreviewToken\"\x91\x02\n" + "\x1cApproveAllDraftChunksRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x128\n" + - "\x18include_security_flagged\x18\x02 \x01(\bR\x16includeSecurityFlagged\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12>\n" + - "\tapprovals\x18\x04 \x03(\v2 .openshell.v1.DraftChunkApprovalR\tapprovals\"\xb7\x01\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" + "\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\"\xb2\x01\n" + + "\x0echunks_skipped\x18\x04 \x01(\rR\rchunksSkipped\"\xf9\x01\n" + "\x15EditDraftChunkRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12L\n" + - "\rproposed_rule\x18\x03 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\fproposedRule\x12\x1c\n" + - "\tworkspace\x18\x04 \x01(\tR\tworkspace\"\x18\n" + - "\x16EditDraftChunkResponse\"d\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\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"`\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" + "\x16UndoDraftChunkResponse\x12%\n" + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + - "policyHash\"K\n" + + "policyHash\"\x92\x01\n" + "\x17ClearDraftChunksRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"A\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" + "\x18ClearDraftChunksResponse\x12%\n" + - "\x0echunks_cleared\x18\x01 \x01(\rR\rchunksCleared\"J\n" + + "\x0echunks_cleared\x18\x01 \x01(\rR\rchunksCleared\"\x91\x01\n" + "\x16GetDraftHistoryRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\x92\x01\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" + "\x11DraftHistoryEntry\x12!\n" + "\ftimestamp_ms\x18\x01 \x01(\x03R\vtimestampMs\x12\x1d\n" + "\n" + @@ -15728,18 +15691,19 @@ var file_openshell_proto_goTypes = []any{ (*sandboxv1.SandboxPolicy)(nil), // 237: openshell.sandbox.v1.SandboxPolicy (*structpb.Struct)(nil), // 238: google.protobuf.Struct (*durationpb.Duration)(nil), // 239: google.protobuf.Duration - (*datamodelv1.Provider)(nil), // 240: openshell.datamodel.v1.Provider - (*sandboxv1.NetworkEndpoint)(nil), // 241: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 242: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 243: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 244: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 245: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 246: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 247: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 248: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 249: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 250: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 251: openshell.sandbox.v1.GetGatewayConfigResponse + (*datamodelv1.WorkspaceSelector)(nil), // 240: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 241: openshell.datamodel.v1.Provider + (*sandboxv1.NetworkEndpoint)(nil), // 242: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 243: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 244: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 245: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 246: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 247: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 248: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 249: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 250: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 251: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 252: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ 214, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential @@ -15781,285 +15745,324 @@ var file_openshell_proto_depIdxs = []int32{ 25, // 36: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec 221, // 37: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry 222, // 38: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 29, // 39: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 29, // 40: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 29, // 41: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate - 24, // 42: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 24, // 43: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 240, // 44: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 24, // 45: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 24, // 46: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 72, // 47: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 236, // 48: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 71, // 49: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 223, // 50: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 76, // 51: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 77, // 52: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 78, // 53: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 167, // 54: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 168, // 55: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 80, // 56: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 75, // 57: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 83, // 58: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 236, // 59: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 24, // 60: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 87, // 61: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 38, // 62: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 88, // 63: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 178, // 64: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 224, // 65: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 240, // 66: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 240, // 67: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 225, // 68: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 240, // 69: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 240, // 70: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 117, // 71: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 100, // 72: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 1, // 73: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 101, // 74: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 106, // 75: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 102, // 76: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 2, // 77: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 104, // 78: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 105, // 79: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 2, // 80: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 7, // 81: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 107, // 82: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 83: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 226, // 84: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 107, // 85: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 107, // 86: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 3, // 87: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 103, // 88: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 241, // 89: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 242, // 90: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 108, // 91: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 227, // 92: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 117, // 93: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 117, // 94: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 98, // 95: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 99, // 96: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 117, // 97: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 98, // 98: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 99, // 99: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 117, // 100: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 98, // 101: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 99, // 102: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 130, // 103: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 228, // 104: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 229, // 105: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 230, // 106: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 231, // 107: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 237, // 108: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 243, // 109: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 136, // 110: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 232, // 111: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 137, // 112: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 138, // 113: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 139, // 114: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 140, // 115: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 141, // 116: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 142, // 117: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 244, // 118: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 245, // 119: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 246, // 120: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 233, // 121: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 150, // 122: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 150, // 123: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 4, // 124: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 4, // 125: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 237, // 126: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 234, // 127: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 87, // 128: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 87, // 129: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 157, // 130: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 160, // 131: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 171, // 132: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 172, // 133: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 158, // 134: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 159, // 135: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 161, // 136: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 166, // 137: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 172, // 138: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 167, // 139: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 168, // 140: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 169, // 141: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 173, // 142: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 175, // 143: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 244, // 144: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 237, // 145: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 237, // 146: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 174, // 147: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 177, // 148: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 176, // 149: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 177, // 150: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 187, // 151: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 244, // 152: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 197, // 153: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 235, // 154: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 247, // 155: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 247, // 156: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 247, // 157: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 236, // 158: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 159: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 6, // 160: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 207, // 161: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 207, // 162: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 103, // 163: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 131, // 164: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 12, // 165: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 14, // 166: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 16, // 167: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 39, // 168: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 47, // 169: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 49, // 170: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 50, // 171: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 40, // 172: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 41, // 173: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 42, // 174: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 43, // 175: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 51, // 176: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 52, // 177: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 53, // 178: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 54, // 179: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 55, // 180: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 56, // 181: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 63, // 182: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 65, // 183: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 66, // 184: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 67, // 185: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 69, // 186: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 73, // 187: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 75, // 188: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 81, // 189: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 82, // 190: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 89, // 191: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 90, // 192: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 91, // 193: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 96, // 194: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 97, // 195: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 120, // 196: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 122, // 197: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 124, // 198: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 92, // 199: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 109, // 200: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 111, // 201: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 113, // 202: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 115, // 203: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 93, // 204: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 127, // 205: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 248, // 206: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 249, // 207: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 135, // 208: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 144, // 209: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 146, // 210: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 148, // 211: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 129, // 212: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 133, // 213: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 151, // 214: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 152, // 215: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 155, // 216: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 162, // 217: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 164, // 218: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 170, // 219: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 85, // 220: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 179, // 221: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 181, // 222: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 183, // 223: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 185, // 224: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 188, // 225: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 190, // 226: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 192, // 227: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 194, // 228: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 196, // 229: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 8, // 230: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 10, // 231: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 199, // 232: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 201, // 233: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 203, // 234: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 205, // 235: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 208, // 236: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 210, // 237: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 212, // 238: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 13, // 239: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 15, // 240: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 17, // 241: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 57, // 242: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 48, // 243: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 57, // 244: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 58, // 245: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 44, // 246: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 44, // 247: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 45, // 248: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 46, // 249: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 59, // 250: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 60, // 251: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 61, // 252: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 62, // 253: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 57, // 254: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 57, // 255: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 64, // 256: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 72, // 257: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 72, // 258: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 68, // 259: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 70, // 260: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 74, // 261: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 79, // 262: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 81, // 263: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 79, // 264: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 94, // 265: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 94, // 266: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 95, // 267: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 119, // 268: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 118, // 269: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 121, // 270: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 123, // 271: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 125, // 272: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 94, // 273: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 110, // 274: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 112, // 275: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 114, // 276: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 116, // 277: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 126, // 278: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 128, // 279: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 250, // 280: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 251, // 281: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 143, // 282: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 145, // 283: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 147, // 284: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 149, // 285: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 132, // 286: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 134, // 287: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 154, // 288: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 153, // 289: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 156, // 290: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 163, // 291: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 165, // 292: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 170, // 293: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 86, // 294: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 180, // 295: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 182, // 296: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 184, // 297: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 186, // 298: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 189, // 299: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 191, // 300: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 193, // 301: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 195, // 302: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 198, // 303: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 9, // 304: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 11, // 305: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 200, // 306: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 202, // 307: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 204, // 308: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 206, // 309: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 209, // 310: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 211, // 311: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 213, // 312: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 239, // [239:313] is the sub-list for method output_type - 165, // [165:239] is the sub-list for method input_type - 165, // [165:165] is the sub-list for extension type_name - 165, // [165:165] is the sub-list for extension extendee - 0, // [0:165] is the sub-list for field type_name + 240, // 39: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 29, // 40: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 240, // 41: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 42: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 43: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 44: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 29, // 45: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 29, // 46: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 240, // 47: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 48: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 49: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 50: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 51: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 52: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 53: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 54: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 240, // 55: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 24, // 56: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 24, // 57: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 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 } func init() { file_openshell_proto_init() } diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 322b1a092c..112bfdc5fe 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -190,9 +190,9 @@ await client.sandboxTemplates.list({ workspace: 'default', limit: 100 }) await client.sandboxTemplates.delete('python', { workspace: 'default' }) ``` -Use `allWorkspaces: true` on `list()` for a platform-admin view. The SDK clears -the workspace field in that request because the gateway treats `workspace` and -`allWorkspaces` as mutually exclusive. +Use `allWorkspaces: true` on `list()` for a platform-admin view. The +discriminated option type makes `workspace` and `allWorkspaces` mutually +exclusive. Omitting both options explicitly selects the `default` workspace. ## Surface and roadmap @@ -212,14 +212,24 @@ Curated methods are added deliberately, so some gateway RPCs are not yet wrapped `client.raw` is a generated client for every gateway RPC, including surface the curated sub-clients do not wrap yet (gateway config, provider CRUD, policy status, watch, logs, and the full observed `Sandbox`). `client.transport` is the shared connection, so extra clients reuse one socket. Generated request and response types live at `@nvidia/openshell-sdk/raw`. ```ts +import { create } from '@bufbuild/protobuf' import { OpenShellClient } from '@nvidia/openshell-sdk' +import { WorkspaceSelectorSchema } from '@nvidia/openshell-sdk/raw' import type { GetGatewayConfigResponse } from '@nvidia/openshell-sdk/raw' const client = await OpenShellClient.connect({ gateway, oidcToken }) // Reach RPCs the curated surface does not wrap yet: const cfg: GetGatewayConfigResponse = await client.raw.getGatewayConfig({}) -const status = await client.raw.getSandboxPolicyStatus({ name: 'my-sandbox', version: 0, global: false }) +const defaultWorkspace = create(WorkspaceSelectorSchema, { + selection: { case: 'workspace', value: 'default' }, +}) +const status = await client.raw.getSandboxPolicyStatus({ + name: 'my-sandbox', + version: 0, + global: false, + workspaceScope: defaultWorkspace, +}) ``` The raw layer returns the generated wire messages verbatim, preserving proto distinctions (an omitted optional versus an explicitly empty map) that the curated types may smooth over. As curated sub-clients land, prefer them; `raw` stays as the always-available floor. diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index 5b12a48e23..60cd5c3f0e 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -56,6 +56,19 @@ function readySandbox( const enc = (s: string) => new TextEncoder().encode(s); +type ScopedRequest = { + workspaceScope?: { selection?: { case?: string; value?: unknown } }; +}; + +function selectedWorkspace(req: ScopedRequest): string | undefined { + const selection = req.workspaceScope?.selection; + return selection?.case === 'workspace' && typeof selection.value === 'string' ? selection.value : undefined; +} + +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[] } = {}; @@ -276,7 +289,7 @@ describe('create', () => { let created: { workloadTemplateName?: string; name?: string; - workspace?: string; + workspaceScope?: ScopedRequest['workspaceScope']; labels?: Record; spec?: { policy?: { version?: number }; @@ -289,7 +302,7 @@ describe('create', () => { const sandbox = client({ createSandbox: (req) => { created = req; - return readySandbox('job-1', 'sb-id', 7n, undefined, req.workspace || 'default'); + return readySandbox('job-1', 'sb-id', 7n, undefined, selectedWorkspace(req) ?? 'default'); }, }); const ref = await sandbox.createFromTemplate({ @@ -305,7 +318,7 @@ describe('create', () => { expect(created.workloadTemplateName).toBe('gpu-kata'); expect(created.name).toBe('job-1'); - expect(created.workspace).toBe('staging'); + expect(selectedWorkspace(created)).toBe('staging'); expect(created.labels).toEqual({ team: 'runtime' }); expect(created.spec?.providers).toEqual(['github']); expect(created.spec?.command).toEqual(['/opt/worker', '--serve']); @@ -317,15 +330,15 @@ describe('create', () => { it('propagates workspace through sandbox lifecycle calls', async () => { const observed: { - create?: { workspace?: string }; - get?: { workspace?: string }; - list?: { workspace?: string; allWorkspaces?: boolean }; - delete?: { workspace?: string }; - attach?: { workspace?: string }; - detach?: { workspace?: string }; - listProviders?: { workspace?: string }; - updatePolicy?: { workspace?: string }; - updateSetting?: { workspace?: string }; + create?: ScopedRequest; + get?: ScopedRequest; + list?: ScopedRequest; + delete?: ScopedRequest; + attach?: ScopedRequest; + detach?: ScopedRequest; + listProviders?: ScopedRequest; + updatePolicy?: ScopedRequest; + updateSetting?: ScopedRequest; configGets: string[]; execGet?: string; interactiveGet?: string; @@ -335,16 +348,17 @@ describe('create', () => { const sandbox = client({ createSandbox: (req) => { observed.create = req; - return readySandbox(req.name || 'sb', 'sb-created', 7n, undefined, req.workspace || 'default'); + return readySandbox(req.name || 'sb', 'sb-created', 7n, undefined, selectedWorkspace(req) ?? 'default'); }, getSandbox: (req) => { - if (req.name === 'exec') observed.execGet = req.workspace; - else if (req.name === 'interactive') observed.interactiveGet = req.workspace; - else if (req.name === 'ssh') observed.sshGet = req.workspace; - else if (req.name === 'forward') observed.forwardGet = req.workspace; - else if (req.name === 'config') observed.configGets.push(req.workspace); + 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); else observed.get = req; - return readySandbox(req.name, `${req.name}-id`, 7n, undefined, req.workspace || 'default'); + return readySandbox(req.name, `${req.name}-id`, 7n, undefined, workspace ?? 'default'); }, listSandboxes: (req) => { observed.list = req; @@ -354,7 +368,7 @@ describe('create', () => { metadata: { id: 'listed-id', name: 'listed', - workspace: req.workspace || 'default', + workspace: selectedWorkspace(req) ?? 'default', labels: { team: 'aire' }, resourceVersion: 7n, }, @@ -370,14 +384,16 @@ describe('create', () => { attachSandboxProvider: (req) => { observed.attach = req; return { - sandbox: readySandbox(req.sandboxName, 'attach-id', 7n, undefined, req.workspace || 'default').sandbox, + sandbox: readySandbox(req.sandboxName, 'attach-id', 7n, undefined, selectedWorkspace(req) ?? 'default') + .sandbox, attached: true, }; }, detachSandboxProvider: (req) => { observed.detach = req; return { - sandbox: readySandbox(req.sandboxName, 'detach-id', 7n, undefined, req.workspace || 'default').sandbox, + sandbox: readySandbox(req.sandboxName, 'detach-id', 7n, undefined, selectedWorkspace(req) ?? 'default') + .sandbox, detached: true, }; }, @@ -451,19 +467,20 @@ describe('create', () => { expect(deleted).toBe(true); expect(attached.sandbox.workspace).toBe('staging'); expect(detached.sandbox.workspace).toBe('staging'); - expect(observed.create?.workspace).toBe('staging'); - expect(observed.get?.workspace).toBe('staging'); - expect(observed.list).toMatchObject({ workspace: 'staging', allWorkspaces: false }); - expect(observed.delete?.workspace).toBe('staging'); + expect(selectedWorkspace(observed.create ?? {})).toBe('staging'); + expect(selectedWorkspace(observed.get ?? {})).toBe('staging'); + expect(selectedWorkspace(observed.list ?? {})).toBe('staging'); + expect(selectsAllWorkspaces(observed.list ?? {})).toBe(false); + expect(selectedWorkspace(observed.delete ?? {})).toBe('staging'); expect(observed.execGet).toBe('staging'); expect(observed.interactiveGet).toBe('staging'); expect(observed.sshGet).toBe('staging'); - expect(observed.attach?.workspace).toBe('staging'); - expect(observed.detach?.workspace).toBe('staging'); - expect(observed.listProviders?.workspace).toBe('staging'); + expect(selectedWorkspace(observed.attach ?? {})).toBe('staging'); + expect(selectedWorkspace(observed.detach ?? {})).toBe('staging'); + expect(selectedWorkspace(observed.listProviders ?? {})).toBe('staging'); expect(observed.configGets).toContain('staging'); - expect(observed.updatePolicy?.workspace).toBe('staging'); - expect(observed.updateSetting?.workspace).toBe('staging'); + expect(selectedWorkspace(observed.updatePolicy ?? {})).toBe('staging'); + expect(selectedWorkspace(observed.updateSetting ?? {})).toBe('staging'); }); it('createFromTemplate rejects an empty template name locally', async () => { @@ -521,8 +538,7 @@ describe('create', () => { describe('sandbox templates', () => { it('create sends the template resource and workspace', async () => { - let observed: { - workspace?: string; + let observed: ScopedRequest & { template?: { metadata?: { name?: string; labels?: Record }; spec?: { @@ -544,7 +560,7 @@ describe('sandbox templates', () => { id: 'template-python', name: req.template?.metadata?.name ?? '', labels: req.template?.metadata?.labels ?? {}, - workspace: req.workspace, + workspace: selectedWorkspace(req), resourceVersion: 1n, }, spec: req.template?.spec, @@ -568,7 +584,7 @@ describe('sandbox templates', () => { { workspace: 'default' }, ); - expect(observed.workspace).toBe('default'); + expect(selectedWorkspace(observed)).toBe('default'); expect(observed.template?.metadata?.name).toBe('python'); expect(observed.template?.metadata?.labels).toEqual({ team: 'runtime' }); expect(observed.template?.spec?.workload?.environment).toEqual({ FEATURE_FLAG: 'on' }); @@ -579,16 +595,16 @@ describe('sandbox templates', () => { it('get list and delete forward workspace and pagination', async () => { const observed: { - get?: { name?: string; workspace?: string }; - list?: { limit?: number; offset?: number; workspace?: string; allWorkspaces?: boolean }; - delete?: { name?: string; workspace?: string }; + get?: ScopedRequest & { name?: string }; + list?: ScopedRequest & { limit?: number; offset?: number; labelSelector?: string }; + delete?: ScopedRequest & { name?: string }; } = {}; const templates = templateClient({ getSandboxTemplate: (req) => { observed.get = req; return { template: { - metadata: { id: 'template-gpu-kata', name: req.name, workspace: req.workspace }, + metadata: { id: 'template-gpu-kata', name: req.name, workspace: selectedWorkspace(req) }, spec: { workload: { image: 'img:v1' } }, }, }; @@ -598,7 +614,7 @@ describe('sandbox templates', () => { return { templates: [ { - metadata: { id: 'template-python', name: 'python', workspace: req.workspace || 'default' }, + metadata: { id: 'template-python', name: 'python', workspace: selectedWorkspace(req) ?? 'default' }, spec: { workload: { image: 'img:v1' } }, }, ], @@ -617,19 +633,21 @@ describe('sandbox templates', () => { expect(got.metadata?.name).toBe('gpu-kata'); expect(listed).toHaveLength(1); expect(deleted).toBe(true); - expect(observed.get).toMatchObject({ name: 'gpu-kata', workspace: 'staging' }); + expect(observed.get).toMatchObject({ name: 'gpu-kata' }); + expect(selectedWorkspace(observed.get ?? {})).toBe('staging'); expect(observed.list).toMatchObject({ limit: 10, offset: 2, - workspace: 'staging', - allWorkspaces: false, labelSelector: 'team=runtime', }); - expect(observed.delete).toMatchObject({ name: 'gpu-kata', workspace: 'staging' }); + expect(selectedWorkspace(observed.list ?? {})).toBe('staging'); + expect(selectsAllWorkspaces(observed.list ?? {})).toBe(false); + expect(observed.delete).toMatchObject({ name: 'gpu-kata' }); + expect(selectedWorkspace(observed.delete ?? {})).toBe('staging'); }); - it('list clears workspace when allWorkspaces is set', async () => { - let observed: { workspace?: string; allWorkspaces?: boolean } = {}; + it('list selects all workspaces explicitly', async () => { + let observed: ScopedRequest = {}; const templates = templateClient({ listSandboxTemplates: (req) => { observed = req; @@ -637,10 +655,10 @@ describe('sandbox templates', () => { }, }); - await templates.list({ workspace: 'staging', allWorkspaces: true }); + await templates.list({ allWorkspaces: true }); - expect(observed.workspace).toBe(''); - expect(observed.allWorkspaces).toBe(true); + expect(selectedWorkspace(observed)).toBeUndefined(); + expect(selectsAllWorkspaces(observed)).toBe(true); }); it('rejects empty names and missing template responses locally', async () => { diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index 4d1ff362ac..839a425e2b 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -16,7 +16,7 @@ import * as net from 'node:net'; import type { MessageInitShape } from '@bufbuild/protobuf'; import { type CallOptions, type Client, createClient, type Transport } from '@connectrpc/connect'; import { errorCode, fromConnect, SdkError } from './errors.js'; -import type { Provider } from './gen/datamodel_pb.js'; +import type { Provider, WorkspaceSelectorSchema } from './gen/datamodel_pb.js'; import type { Sandbox, SandboxWorkloadTemplate, UpdateConfigResponse } from './gen/openshell_pb.js'; import { type ExecSandboxInputSchema, @@ -86,7 +86,7 @@ export interface Health { export interface SandboxSpec { name?: string; - /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + /** Workspace name. Omit for `default`; empty strings are invalid. */ workspace?: string; image?: string; labels?: Record; @@ -115,7 +115,7 @@ export interface SandboxSpec { export interface SandboxFromTemplateSpec { name?: string; - /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + /** Workspace name. Omit for `default`; empty strings are invalid. */ workspace?: string; templateName: string; labels?: Record; @@ -149,36 +149,37 @@ export interface SandboxWorkloadTemplateProvenance { resourceVersion: string; } -export interface ListOptions { +interface PaginationOptions { limit?: number; offset?: number; labelSelector?: string; - /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ - workspace?: string; - /** List across all workspaces. Requires platform admin permission. */ - allWorkspaces?: boolean; } +/** Mutually exclusive named/default or all-workspaces list scope. */ +export type WorkspaceListScope = + | { workspace?: string; allWorkspaces?: false | undefined } + | { workspace?: never; allWorkspaces: true }; + +export type ListOptions = PaginationOptions & WorkspaceListScope; + export interface SandboxWorkspaceOptions { - /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + /** Workspace name. Omit for `default`; empty strings are invalid. */ workspace?: string; } export type SandboxCallOptions = CallOptions & SandboxWorkspaceOptions; export interface SandboxTemplateWorkspaceOptions { - /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + /** Workspace name. Omit for `default`; empty strings are invalid. */ workspace?: string; } -export interface SandboxTemplateListOptions extends SandboxTemplateWorkspaceOptions { +export type SandboxTemplateListOptions = WorkspaceListScope & { limit?: number; offset?: number; /** Optional label selector in key=value comma-separated form. */ labelSelector?: string; - /** List templates across all workspaces. Requires platform admin permission. */ - allWorkspaces?: boolean; -} +}; export interface ExecOptions extends SandboxWorkspaceOptions { workdir?: string; @@ -491,8 +492,18 @@ function versionPin(value: string | undefined): bigint { const FORWARD_CHUNK = 64 * 1024; -function workspaceOption(options?: SandboxWorkspaceOptions | null): string { - return options?.workspace ?? ''; +function workspaceName(options?: SandboxWorkspaceOptions | null): string { + const workspace = options?.workspace ?? 'default'; + if (workspace.trim() === '') throw new SdkError('invalid_config', 'workspace must be non-empty'); + return workspace; +} + +function workspaceScope(options?: SandboxWorkspaceOptions | null): MessageInitShape { + return { selection: { case: 'workspace', value: workspaceName(options) } }; +} + +function listWorkspaceScope(options?: WorkspaceListScope | null): MessageInitShape { + return options?.allWorkspaces ? { selection: { case: 'allWorkspaces', value: {} } } : workspaceScope(options); } function requestCallOptions(options?: SandboxCallOptions | null): CallOptions | undefined { @@ -653,7 +664,7 @@ export class SandboxTemplateClient { try { const resp = await this.grpc.createSandboxTemplate({ template, - workspace: options?.workspace ?? '', + workspaceScope: workspaceScope(options), }); return sandboxTemplate(resp.template); } catch (e) { @@ -666,7 +677,7 @@ export class SandboxTemplateClient { try { const resp = await this.grpc.getSandboxTemplate({ name, - workspace: options?.workspace ?? '', + workspaceScope: workspaceScope(options), }); return sandboxTemplate(resp.template); } catch (e) { @@ -676,13 +687,11 @@ export class SandboxTemplateClient { async list(options?: SandboxTemplateListOptions | null): Promise { try { - const allWorkspaces = options?.allWorkspaces ?? false; const resp = await this.grpc.listSandboxTemplates({ limit: options?.limit ?? 0, offset: options?.offset ?? 0, - workspace: allWorkspaces ? '' : (options?.workspace ?? ''), - allWorkspaces, labelSelector: options?.labelSelector ?? '', + workspaceScope: listWorkspaceScope(options), }); return resp.templates; } catch (e) { @@ -695,7 +704,7 @@ export class SandboxTemplateClient { try { const resp = await this.grpc.deleteSandboxTemplate({ name, - workspace: options?.workspace ?? '', + workspaceScope: workspaceScope(options), }); return resp.deleted; } catch (e) { @@ -758,7 +767,7 @@ export class SandboxClient { const resp = await this.grpc.createSandbox({ name: spec.name ?? '', labels: spec.labels ?? {}, - workspace: spec.workspace ?? '', + workspaceScope: workspaceScope(spec), spec: specInit, }); return sandboxRef(resp.sandbox); @@ -773,7 +782,7 @@ export class SandboxClient { const resp = await this.grpc.createSandbox({ name: spec.name ?? '', labels: spec.labels ?? {}, - workspace: spec.workspace ?? '', + workspaceScope: workspaceScope(spec), spec: { providers: spec.providers ?? [], command: spec.command ?? [], @@ -791,7 +800,7 @@ export class SandboxClient { async get(name: string, options?: SandboxCallOptions | null): Promise { try { const resp = await this.grpc.getSandbox( - { name, workspace: workspaceOption(options) }, + { name, workspaceScope: workspaceScope(options) }, requestCallOptions(options), ); return sandboxRef(resp.sandbox); @@ -802,13 +811,11 @@ export class SandboxClient { async list(options?: ListOptions | null): Promise { try { - const allWorkspaces = options?.allWorkspaces ?? false; const resp = await this.grpc.listSandboxes({ limit: options?.limit ?? 0, offset: options?.offset ?? 0, labelSelector: options?.labelSelector ?? '', - workspace: allWorkspaces ? '' : (options?.workspace ?? ''), - allWorkspaces, + workspaceScope: listWorkspaceScope(options), }); return resp.sandboxes.map((s) => sandboxRef(s)); } catch (e) { @@ -818,7 +825,7 @@ export class SandboxClient { async delete(name: string, options?: SandboxWorkspaceOptions | null): Promise { try { - const resp = await this.grpc.deleteSandbox({ name, workspace: workspaceOption(options) }); + const resp = await this.grpc.deleteSandbox({ name, workspaceScope: workspaceScope(options) }); return resp.deleted; } catch (e) { throw fromConnect(e); @@ -1307,7 +1314,7 @@ export class SandboxClient { sandboxName: name, providerName: provider, expectedResourceVersion: versionPin(options?.expectedResourceVersion), - workspace: workspaceOption(options), + workspaceScope: workspaceScope(options), }); return { sandbox: sandboxRef(resp.sandbox), changed: resp.attached }; } catch (e) { @@ -1325,7 +1332,7 @@ export class SandboxClient { sandboxName: name, providerName: provider, expectedResourceVersion: versionPin(options?.expectedResourceVersion), - workspace: workspaceOption(options), + workspaceScope: workspaceScope(options), }); return { sandbox: sandboxRef(resp.sandbox), changed: resp.detached }; } catch (e) { @@ -1335,7 +1342,10 @@ export class SandboxClient { async listProviders(name: string, options?: SandboxWorkspaceOptions | null): Promise { try { - const resp = await this.grpc.listSandboxProviders({ sandboxName: name, workspace: workspaceOption(options) }); + const resp = await this.grpc.listSandboxProviders({ + sandboxName: name, + workspaceScope: workspaceScope(options), + }); return resp.providers.map((p) => providerRef(p)); } catch (e) { throw fromConnect(e); @@ -1367,7 +1377,7 @@ export class SandboxClient { policy, global: false, expectedResourceVersion: versionPin(options?.expectedResourceVersion), - workspace: workspaceOption(options), + workspaceScope: workspaceScope(options), }); const result = updateConfigResult(resp); if (options?.wait) @@ -1392,7 +1402,7 @@ export class SandboxClient { settingKey: key, settingValue: value, global: false, - workspace: workspaceOption(options), + workspaceScope: workspaceScope(options), }); return updateConfigResult(resp); } catch (e) { diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 31571865be..5d02fe6b61 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -43,6 +43,7 @@ export type { SshSession, UpdateConfigResult, WaitOptions, + WorkspaceListScope, } from './client.js'; export { errorCode, OpenShellClient, SandboxClient, SandboxTemplateClient } from './client.js'; export type { SdkErrorCode } from './errors.js';