feat(api)!: unify sandbox references

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
Drew Newberry
2026-09-10 21:17:27 -07:00
parent 0ade78f580
commit 72f349d271
61 changed files with 2944 additions and 2616 deletions
+10 -7
View File
@@ -56,13 +56,16 @@ cross-origin or sibling-subdomain request.
Public workspace-scoped RPCs carry a typed `WorkspaceSelector`. A request must
select one non-empty workspace explicitly; `default` is an ordinary explicit
name, not an omitted-value fallback. Sandbox, sandbox template, provider, and
service list RPCs also accept an all-workspaces marker after Platform Admin
authorization. Single-workspace handlers reject that marker. Platform-global
policy operations require the selector to be absent, while workspace policy
operations require it. The gateway authorizes the selected scope before
performing resource lookup so malformed, unsupported, and unauthorized scopes
have consistent behavior across resource types.
name, not an omitted-value fallback. Sandbox-scoped RPCs instead carry a
`SandboxReference`: name references include a named workspace selector, while
canonical ID references omit it. The gateway resolves either form to the same
persisted sandbox record and authorizes that record's workspace. Missing and
unauthorized references use the same response within each principal class so
the resolver does not expose an object-existence oracle. Sandbox, sandbox
template, provider, and service list RPCs also accept an all-workspaces marker
after Platform Admin authorization. Single-workspace handlers reject that
marker. Platform-global policy operations require the sandbox reference to be
absent, while sandbox policy operations require it.
Docker and Podman report the local address through which their sandboxes can
reach the gateway. When the primary listener covers that address, the gateway
+15 -10
View File
@@ -58,8 +58,9 @@ pub async fn sandbox_provider_list(
let mut client = grpc_client(server, tls).await?;
let response = client
.list_sandbox_providers(ListSandboxProvidersRequest {
sandbox_name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
})
.await
.into_diagnostic()?;
@@ -90,8 +91,9 @@ pub async fn sandbox_provider_attach(
// Fetch current sandbox to get resource_version for CAS
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
})
.await
.into_diagnostic()?
@@ -103,10 +105,11 @@ pub async fn sandbox_provider_attach(
let response = match client
.attach_sandbox_provider(AttachSandboxProviderRequest {
sandbox_name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
provider_name: provider.to_string(),
expected_resource_version: resource_version,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
@@ -146,8 +149,9 @@ pub async fn sandbox_provider_detach(
// Fetch current sandbox to get resource_version for CAS
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
})
.await
.into_diagnostic()?
@@ -159,10 +163,11 @@ pub async fn sandbox_provider_detach(
let response = match client
.detach_sandbox_provider(DetachSandboxProviderRequest {
sandbox_name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
provider_name: provider.to_string(),
expected_resource_version: resource_version,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
+14 -1
View File
@@ -2137,7 +2137,7 @@ enum ServiceCommands {
offset: u32,
/// List services across all workspaces (overrides --workspace).
#[arg(long)]
#[arg(long, conflicts_with = "sandbox")]
all_workspaces: bool,
/// Output format.
@@ -6059,6 +6059,19 @@ mod tests {
}
}
#[test]
fn service_list_rejects_sandbox_with_all_workspaces() {
let result = Cli::try_parse_from([
"openshell",
"service",
"list",
"my-sandbox",
"--all-workspaces",
]);
assert!(result.is_err());
}
#[test]
fn service_get_accepts_optional_service_name() {
let cli = Cli::try_parse_from(["openshell", "service", "get", "my-sandbox", "api"])
+162 -87
View File
@@ -686,10 +686,12 @@ pub async fn sandbox_create(
let setting = parse_cli_setting_value(settings::PROPOSAL_APPROVAL_MODE_KEY, approval_mode)?;
match client
.update_config(UpdateConfigRequest {
name: sandbox_name.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
sandbox_name.clone(),
)),
setting_key: settings::PROPOSAL_APPROVAL_MODE_KEY.to_string(),
setting_value: Some(setting),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
..Default::default()
})
.await
@@ -752,7 +754,9 @@ pub async fn sandbox_create(
};
let mut stream = client
.watch_sandbox(WatchSandboxRequest {
id: sandbox_id.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox_id.clone(),
)),
follow_status: true,
follow_logs: true,
follow_events: true,
@@ -1494,8 +1498,10 @@ pub async fn sandbox_get(
let response = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?;
@@ -1511,7 +1517,9 @@ pub async fn sandbox_get(
};
let config = client
.get_sandbox_config(GetSandboxConfigRequest { sandbox_id })
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
})
.await
.into_diagnostic()?
.into_inner();
@@ -1656,8 +1664,10 @@ pub async fn sandbox_exec_grpc(
// Resolve sandbox name to id.
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?
@@ -1725,7 +1735,9 @@ pub async fn sandbox_exec_grpc(
// Make the streaming gRPC call.
let mut stream = client
.exec_sandbox(ExecSandboxRequest {
sandbox_id: sandbox.object_id().to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox.object_id(),
)),
command: command.to_vec(),
workdir: workdir.unwrap_or_default().to_string(),
environment: environment.clone(),
@@ -1876,7 +1888,7 @@ async fn create_forward_session_token(
) -> std::result::Result<String, ForwardTcpConnectionError> {
let response = client
.create_ssh_session(CreateSshSessionRequest {
sandbox_id: sandbox_id.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
})
.await
.map_err(ForwardTcpConnectionError::from_status)?;
@@ -1890,8 +1902,10 @@ async fn fetch_ready_sandbox_for_forward(
) -> Result<Sandbox> {
let response = match client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
{
@@ -1989,7 +2003,7 @@ async fn forward_one_tcp_connection(
tx.send(TcpForwardFrame {
payload: Some(openshell_core::proto::tcp_forward_frame::Payload::Init(
TcpForwardInit {
sandbox_id,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
service_id,
target: Some(tcp_forward_init::Target::Tcp(TcpRelayTarget {
host: target_host,
@@ -2109,7 +2123,9 @@ async fn sandbox_exec_interactive_grpc(
input_tx
.send(ExecSandboxInput {
payload: Some(exec_sandbox_input::Payload::Start(ExecSandboxRequest {
sandbox_id: sandbox.object_id().to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox.object_id(),
)),
command: command.to_vec(),
workdir: workdir.unwrap_or_default().to_string(),
environment: environment.clone(),
@@ -3065,8 +3081,10 @@ pub async fn sandbox_delete(
let response = match client
.delete_sandbox(DeleteSandboxRequest {
name: name.clone(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.clone(),
)),
})
.await
{
@@ -3117,8 +3135,10 @@ pub async fn sandbox_stop(
let mut client = grpc_client(server, tls).await?;
let sandbox = client
.stop_sandbox(StopSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?
@@ -3140,8 +3160,10 @@ pub async fn sandbox_start(
let mut client = grpc_client(server, tls).await?;
let sandbox = client
.start_sandbox(StartSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?
@@ -3177,7 +3199,7 @@ async fn wait_for_lifecycle_phase(
let sandbox_id = sandbox.object_id().to_string();
let mut stream = client
.watch_sandbox(WatchSandboxRequest {
id: sandbox_id,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
follow_status: true,
follow_logs: false,
follow_events: false,
@@ -3238,11 +3260,12 @@ pub async fn service_expose(
let mut client = grpc_client(server, tls).await?;
let response = client
.expose_service(ExposeServiceRequest {
sandbox: sandbox.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, sandbox,
)),
service: service.to_string(),
target_port: u32::from(target_port),
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.map_err(service_expose_status_error)?
@@ -3289,13 +3312,16 @@ pub async fn service_list(
let mut client = grpc_client(server, tls).await?;
let response = client
.list_services(ListServicesRequest {
sandbox: sandbox.unwrap_or_default().to_string(),
sandbox_ref: sandbox
.map(|name| openshell_core::proto::sandbox_reference_by_name(workspace, name)),
limit,
offset,
workspace_scope: Some(if all_workspaces {
openshell_core::proto::all_workspaces_selector()
} else {
openshell_core::proto::workspace_selector(workspace)
workspace_scope: sandbox.is_none().then(|| {
if all_workspaces {
openshell_core::proto::all_workspaces_selector()
} else {
openshell_core::proto::workspace_selector(workspace)
}
}),
})
.await
@@ -3334,9 +3360,10 @@ pub async fn service_get(
let mut client = grpc_client(server, tls).await?;
let response = client
.get_service(GetServiceRequest {
sandbox: sandbox.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, sandbox,
)),
service: service.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.map_err(|status| service_status_error("get service", "sandbox:read", status))?
@@ -3356,9 +3383,10 @@ pub async fn service_delete(
let mut client = grpc_client(server, tls).await?;
let response = client
.delete_service(DeleteServiceRequest {
sandbox: sandbox.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, sandbox,
)),
service: service.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.map_err(|status| service_status_error("delete service", "sandbox:write", status))?
@@ -4152,7 +4180,7 @@ pub async fn sandbox_policy_set_global(
let mut client = grpc_client(server, tls).await?;
let response = client
.update_config(UpdateConfigRequest {
name: String::new(),
sandbox_ref: None,
policy: Some(policy),
global: true,
..Default::default()
@@ -4184,8 +4212,10 @@ pub async fn sandbox_settings_get(
let mut client = grpc_client(server, tls).await?;
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?
@@ -4195,7 +4225,9 @@ pub async fn sandbox_settings_get(
let response = client
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_id: sandbox.object_id().to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox.object_id(),
)),
})
.await
.into_diagnostic()?
@@ -4352,7 +4384,7 @@ pub async fn gateway_setting_set(
let mut client = grpc_client(server, tls).await?;
let response = client
.update_config(UpdateConfigRequest {
name: String::new(),
sandbox_ref: None,
setting_key: key.to_string(),
setting_value: Some(setting_value),
global: true,
@@ -4385,10 +4417,12 @@ pub async fn sandbox_setting_set(
let mut client = grpc_client(server, tls).await?;
let response = client
.update_config(UpdateConfigRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
setting_key: key.to_string(),
setting_value: Some(setting_value),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
..Default::default()
})
.await
@@ -4418,7 +4452,7 @@ pub async fn gateway_setting_delete(
let mut client = grpc_client(server, tls).await?;
let response = client
.update_config(UpdateConfigRequest {
name: String::new(),
sandbox_ref: None,
setting_key: key.to_string(),
delete_setting: true,
global: true,
@@ -4451,10 +4485,12 @@ pub async fn sandbox_setting_delete(
let mut client = grpc_client(server, tls).await?;
let response = client
.update_config(UpdateConfigRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
setting_key: key.to_string(),
delete_setting: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
..Default::default()
})
.await
@@ -4497,10 +4533,12 @@ pub async fn sandbox_policy_set(
// Get current version so we can detect no-ops.
let current_version = client
.get_sandbox_policy_status(GetSandboxPolicyStatusRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
version: 0,
global: false,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.ok()
@@ -4509,9 +4547,11 @@ pub async fn sandbox_policy_set(
let response = client
.update_config(UpdateConfigRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
policy: Some(policy),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
..Default::default()
})
.await
@@ -4556,10 +4596,12 @@ pub async fn sandbox_policy_set(
let status_resp = client
.get_sandbox_policy_status(GetSandboxPolicyStatusRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
version: resp.version,
global: false,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
@@ -4635,8 +4677,10 @@ pub async fn sandbox_policy_update(
let mut client = grpc_client(server, tls).await?;
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?
@@ -4651,7 +4695,9 @@ pub async fn sandbox_policy_update(
};
let current = client
.get_sandbox_config(GetSandboxConfigRequest { sandbox_id })
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
})
.await
.into_diagnostic()?
.into_inner();
@@ -4683,9 +4729,11 @@ pub async fn sandbox_policy_update(
let current_hash = current.policy_hash.clone();
let response = client
.update_config(UpdateConfigRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
merge_operations: plan.merge_operations,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
..Default::default()
})
.await
@@ -4730,10 +4778,12 @@ pub async fn sandbox_policy_update(
let status_resp = client
.get_sandbox_policy_status(GetSandboxPolicyStatusRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
version: response.version,
global: false,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
@@ -4838,10 +4888,12 @@ where
let status_resp = client
.get_sandbox_policy_status(GetSandboxPolicyStatusRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
version,
global: false,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
@@ -4922,8 +4974,10 @@ where
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?
@@ -4937,7 +4991,7 @@ where
let config = client
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_id: sandbox_id.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
})
.await
.into_diagnostic()?
@@ -5032,10 +5086,9 @@ pub async fn sandbox_policy_get_global(
let status_resp = client
.get_sandbox_policy_status(GetSandboxPolicyStatusRequest {
name: String::new(),
sandbox_ref: None,
version,
global: true,
workspace_scope: None,
})
.await
.into_diagnostic()?;
@@ -5172,11 +5225,13 @@ pub async fn sandbox_policy_list(
let resp = client
.list_sandbox_policies(ListSandboxPoliciesRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
limit,
offset: 0,
global: false,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
@@ -5207,11 +5262,10 @@ pub async fn sandbox_policy_list_global(
let resp = client
.list_sandbox_policies(ListSandboxPoliciesRequest {
name: String::new(),
sandbox_ref: None,
limit,
offset: 0,
global: true,
workspace_scope: None,
})
.await
.into_diagnostic()?;
@@ -5302,8 +5356,10 @@ pub async fn sandbox_logs(
// Resolve sandbox name to id.
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?
@@ -5336,7 +5392,9 @@ pub async fn sandbox_logs(
// Streaming mode: use WatchSandbox.
let mut stream = client
.watch_sandbox(WatchSandboxRequest {
id: sandbox.object_id().to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox.object_id().to_string(),
)),
follow_status: false,
follow_logs: true,
follow_events: false,
@@ -5363,12 +5421,13 @@ pub async fn sandbox_logs(
// One-shot mode: use GetSandboxLogs.
let resp = client
.get_sandbox_logs(GetSandboxLogsRequest {
sandbox_id: sandbox.object_id().to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox.object_id(),
)),
lines,
since_ms,
sources: source_filter,
min_level: level.to_uppercase(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
@@ -5442,9 +5501,11 @@ pub async fn sandbox_draft_get(
let response = client
.get_draft_policy(GetDraftPolicyRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
status_filter: status_filter.unwrap_or("").to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
@@ -5550,9 +5611,11 @@ pub async fn sandbox_draft_approve(
let mut client = grpc_client(server, tls).await?;
let review_token = client
.get_draft_policy(GetDraftPolicyRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
status_filter: String::new(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?
@@ -5565,9 +5628,11 @@ pub async fn sandbox_draft_approve(
let response = client
.approve_draft_chunk(ApproveDraftChunkRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
chunk_id: chunk_id.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
review_token,
})
.await
@@ -5597,10 +5662,12 @@ pub async fn sandbox_draft_reject(
client
.reject_draft_chunk(RejectDraftChunkRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
chunk_id: chunk_id.to_string(),
reason: reason.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
@@ -5621,9 +5688,11 @@ pub async fn sandbox_draft_approve_all(
let mut client = grpc_client(server, tls).await?;
let approvals = client
.get_draft_policy(GetDraftPolicyRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
status_filter: "pending".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?
@@ -5638,9 +5707,11 @@ pub async fn sandbox_draft_approve_all(
let response = client
.approve_all_draft_chunks(ApproveAllDraftChunksRequest {
name: name.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
include_security_flagged,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
approvals,
})
.await
@@ -5669,8 +5740,10 @@ pub async fn sandbox_draft_clear(
let response = client
.clear_draft_chunks(ClearDraftChunksRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?;
@@ -5696,8 +5769,10 @@ pub async fn sandbox_draft_history(
let response = client
.get_draft_history(GetDraftHistoryRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?;
+8 -4
View File
@@ -91,8 +91,10 @@ async fn ssh_session_config(
// Resolve sandbox name to id.
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
name.to_string(),
)),
})
.await
.into_diagnostic()?
@@ -105,7 +107,9 @@ async fn ssh_session_config(
let response = loop {
match client
.create_ssh_session(CreateSshSessionRequest {
sandbox_id: sandbox.object_id().to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox.object_id(),
)),
})
.await
{
@@ -1436,7 +1440,7 @@ pub async fn sandbox_ssh_proxy(
tx.send(TcpForwardFrame {
payload: Some(openshell_core::proto::tcp_forward_frame::Payload::Init(
TcpForwardInit {
sandbox_id: sandbox_id.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
service_id: format!("ssh-proxy:{sandbox_id}"),
target: Some(tcp_forward_init::Target::Ssh(SshRelayTarget {})),
authorization_token: token.to_string(),
@@ -39,6 +39,13 @@ use tokio_stream::wrappers::TcpListenerStream;
use tonic::transport::{Certificate as TlsCertificate, Identity, Server, ServerTlsConfig};
use tonic::{Response, Status};
fn sandbox_reference_name(sandbox_ref: Option<&openshell_core::proto::SandboxReference>) -> String {
match sandbox_ref.and_then(|reference| reference.identifier.as_ref()) {
Some(openshell_core::proto::sandbox_reference::Identifier::Name(name)) => name.clone(),
_ => panic!("expected sandbox name reference"),
}
}
#[derive(Clone, Default)]
struct ProviderState {
providers: Arc<Mutex<HashMap<String, Provider>>>,
@@ -174,7 +181,8 @@ impl OpenShell for TestOpenShell {
&self,
request: tonic::Request<GetSandboxRequest>,
) -> Result<Response<SandboxResponse>, Status> {
let name = request.into_inner().name;
let request = request.into_inner();
let name = sandbox_reference_name(request.sandbox_ref.as_ref());
// Return a minimal sandbox with metadata for CAS operations
Ok(Response::new(SandboxResponse {
sandbox: Some(Sandbox {
@@ -208,7 +216,8 @@ impl OpenShell for TestOpenShell {
&self,
request: tonic::Request<ListSandboxProvidersRequest>,
) -> Result<Response<ListSandboxProvidersResponse>, Status> {
let sandbox_name = request.into_inner().sandbox_name;
let request = request.into_inner();
let sandbox_name = sandbox_reference_name(request.sandbox_ref.as_ref());
self.state
.sandbox_provider_requests
.lock()
@@ -237,12 +246,13 @@ impl OpenShell for TestOpenShell {
request: tonic::Request<AttachSandboxProviderRequest>,
) -> Result<Response<AttachSandboxProviderResponse>, Status> {
let request = request.into_inner();
let sandbox_name = sandbox_reference_name(request.sandbox_ref.as_ref());
self.state
.sandbox_provider_requests
.lock()
.await
.push(SandboxProviderRequestLog::Attach {
sandbox_name: request.sandbox_name.clone(),
sandbox_name: sandbox_name.clone(),
provider_name: request.provider_name.clone(),
});
if !self
@@ -255,9 +265,7 @@ impl OpenShell for TestOpenShell {
return Err(Status::failed_precondition("provider not found"));
}
let mut sandbox_providers = self.state.sandbox_providers.lock().await;
let providers = sandbox_providers
.entry(request.sandbox_name.clone())
.or_default();
let providers = sandbox_providers.entry(sandbox_name.clone()).or_default();
let attached = if providers.contains(&request.provider_name) {
false
} else {
@@ -266,7 +274,7 @@ impl OpenShell for TestOpenShell {
};
let sandbox = Sandbox {
metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta {
name: request.sandbox_name,
name: sandbox_name,
..Default::default()
}),
spec: Some(openshell_core::proto::SandboxSpec {
@@ -286,24 +294,23 @@ impl OpenShell for TestOpenShell {
request: tonic::Request<DetachSandboxProviderRequest>,
) -> Result<Response<DetachSandboxProviderResponse>, Status> {
let request = request.into_inner();
let sandbox_name = sandbox_reference_name(request.sandbox_ref.as_ref());
self.state
.sandbox_provider_requests
.lock()
.await
.push(SandboxProviderRequestLog::Detach {
sandbox_name: request.sandbox_name.clone(),
sandbox_name: sandbox_name.clone(),
provider_name: request.provider_name.clone(),
});
let mut sandbox_providers = self.state.sandbox_providers.lock().await;
let providers = sandbox_providers
.entry(request.sandbox_name.clone())
.or_default();
let providers = sandbox_providers.entry(sandbox_name.clone()).or_default();
let before_len = providers.len();
providers.retain(|name| name != &request.provider_name);
let detached = providers.len() != before_len;
let sandbox = Sandbox {
metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta {
name: request.sandbox_name,
name: sandbox_name,
..Default::default()
}),
spec: Some(openshell_core::proto::SandboxSpec {
@@ -56,6 +56,20 @@ fn selected_workspace(
}
}
fn sandbox_reference_name(sandbox_ref: Option<&openshell_core::proto::SandboxReference>) -> String {
match sandbox_ref.and_then(|reference| reference.identifier.as_ref()) {
Some(openshell_core::proto::sandbox_reference::Identifier::Name(name)) => name.clone(),
_ => panic!("expected sandbox name reference"),
}
}
fn sandbox_reference_id(sandbox_ref: Option<&openshell_core::proto::SandboxReference>) -> String {
match sandbox_ref.and_then(|reference| reference.identifier.as_ref()) {
Some(openshell_core::proto::sandbox_reference::Identifier::Id(id)) => id.clone(),
_ => panic!("expected sandbox ID reference"),
}
}
#[derive(Clone, Default)]
struct SandboxState {
deleted_names: Arc<Mutex<Vec<Vec<String>>>>,
@@ -182,7 +196,8 @@ impl OpenShell for TestOpenShell {
&self,
request: tonic::Request<GetSandboxRequest>,
) -> Result<Response<SandboxResponse>, Status> {
let name = request.into_inner().name;
let request = request.into_inner();
let name = sandbox_reference_name(request.sandbox_ref.as_ref());
let mut sandbox = Sandbox {
metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta {
id: format!("id-{name}"),
@@ -332,7 +347,7 @@ impl OpenShell for TestOpenShell {
.deleted_names
.lock()
.await
.push(vec![request.name.clone()]);
.push(vec![sandbox_reference_name(request.sandbox_ref.as_ref())]);
let delete_failure = self.state.fail_delete_sandbox_message.lock().await.take();
if let Some(message) = delete_failure {
return Err(Status::internal(message));
@@ -386,7 +401,8 @@ impl OpenShell for TestOpenShell {
{
return Err(Status::failed_precondition("sandbox is not ready"));
}
let sandbox_id = request.into_inner().sandbox_id;
let request = request.into_inner();
let sandbox_id = sandbox_reference_id(request.sandbox_ref.as_ref());
Ok(Response::new(CreateSshSessionResponse {
sandbox_id,
token: "test-token".to_string(),
@@ -574,7 +590,8 @@ impl OpenShell for TestOpenShell {
&self,
request: tonic::Request<WatchSandboxRequest>,
) -> Result<Response<Self::WatchSandboxStream>, Status> {
let sandbox_id = request.into_inner().id;
let request = request.into_inner();
let sandbox_id = sandbox_reference_id(request.sandbox_ref.as_ref());
let (tx, rx) = mpsc::channel(4);
let vm_error_after_started = self.state.vm_error_after_started.load(Ordering::SeqCst);
let vm_error_with_observed_exit = self
@@ -32,6 +32,20 @@ use tokio_stream::wrappers::TcpListenerStream;
use tonic::transport::{Certificate as TlsCertificate, Identity, Server, ServerTlsConfig};
use tonic::{Response, Status};
fn sandbox_reference_name(sandbox_ref: Option<&openshell_core::proto::SandboxReference>) -> String {
match sandbox_ref.and_then(|reference| reference.identifier.as_ref()) {
Some(openshell_core::proto::sandbox_reference::Identifier::Name(name)) => name.clone(),
_ => panic!("expected sandbox name reference"),
}
}
fn sandbox_reference_id(sandbox_ref: Option<&openshell_core::proto::SandboxReference>) -> String {
match sandbox_ref.and_then(|reference| reference.identifier.as_ref()) {
Some(openshell_core::proto::sandbox_reference::Identifier::Id(id)) => id.clone(),
_ => panic!("expected sandbox ID reference"),
}
}
// ── mock OpenShell server ─────────────────────────────────────────────
/// Records which sandbox name was requested via `get_sandbox`.
@@ -117,7 +131,8 @@ impl OpenShell for TestOpenShell {
&self,
request: tonic::Request<GetSandboxRequest>,
) -> Result<Response<SandboxResponse>, Status> {
let name = request.into_inner().name;
let request = request.into_inner();
let name = sandbox_reference_name(request.sandbox_ref.as_ref());
*self.state.last_get_name.lock().await = Some(name.clone());
Ok(Response::new(SandboxResponse {
sandbox: Some(Sandbox {
@@ -179,7 +194,8 @@ impl OpenShell for TestOpenShell {
) -> Result<Response<GetSandboxConfigResponse>, Status> {
let req = request.into_inner();
assert_eq!(
req.sandbox_id, "test-id",
sandbox_reference_id(req.sandbox_ref.as_ref()),
"test-id",
"GetSandboxConfig should pass the id from GetSandbox"
);
Ok(Response::new(GetSandboxConfigResponse {
@@ -450,7 +466,10 @@ impl OpenShell for TestOpenShell {
request: tonic::Request<GetSandboxPolicyStatusRequest>,
) -> Result<Response<GetSandboxPolicyStatusResponse>, Status> {
let req = request.into_inner();
assert_eq!(req.name, "my-sandbox");
assert_eq!(
sandbox_reference_name(req.sandbox_ref.as_ref()),
"my-sandbox"
);
assert_eq!(req.version, 3);
assert!(!req.global);
+7 -7
View File
@@ -28,7 +28,6 @@ use crate::proto::{
NetworkActivitySummary, PolicyChunk, PolicySource, PolicyStatus, RefreshSandboxTokenRequest,
ReportPolicyStatusRequest, SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest,
SubmitPolicyAnalysisResponse, UpdateConfigRequest, open_shell_client::OpenShellClient,
workspace_selector,
};
use crate::sandbox_env;
use miette::{IntoDiagnostic, Result, WrapErr};
@@ -717,7 +716,7 @@ async fn fetch_settings_snapshot_with_client(
) -> Result<SettingsPollResult> {
let response = client
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_id: sandbox_id.to_string(),
sandbox_ref: Some(crate::proto::sandbox_reference_by_id(sandbox_id)),
})
.await
.into_diagnostic()?;
@@ -751,9 +750,8 @@ async fn sync_policy_with_client(
) -> Result<()> {
client
.update_config(UpdateConfigRequest {
name: sandbox.to_string(),
sandbox_ref: Some(crate::proto::sandbox_reference_by_name(workspace, sandbox)),
policy: Some(policy.clone()),
workspace_scope: Some(workspace_selector(workspace)),
..Default::default()
})
.await
@@ -1053,7 +1051,7 @@ impl CachedOpenShellClient {
.client
.clone()
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_id: sandbox_id.to_string(),
sandbox_ref: Some(crate::proto::sandbox_reference_by_id(sandbox_id)),
})
.await
.into_diagnostic()?;
@@ -1179,9 +1177,11 @@ impl CachedOpenShellClient {
.client
.clone()
.get_draft_policy(GetDraftPolicyRequest {
name: sandbox_name.to_string(),
status_filter: status_filter.to_string(),
workspace_scope: Some(workspace_selector(self.workspace())),
sandbox_ref: Some(crate::proto::sandbox_reference_by_name(
self.workspace(),
sandbox_name,
)),
})
.await
.into_diagnostic()?;
+19
View File
@@ -89,3 +89,22 @@ pub fn all_workspaces_selector() -> WorkspaceSelector {
)),
}
}
/// Build a canonical reference from a stable sandbox ID.
pub fn sandbox_reference_by_id(id: impl Into<String>) -> SandboxReference {
SandboxReference {
identifier: Some(sandbox_reference::Identifier::Id(id.into())),
workspace_scope: None,
}
}
/// Build a canonical reference from a workspace-scoped sandbox name.
pub fn sandbox_reference_by_name(
workspace: impl Into<String>,
name: impl Into<String>,
) -> SandboxReference {
SandboxReference {
identifier: Some(sandbox_reference::Identifier::Name(name.into())),
workspace_scope: Some(workspace_selector(workspace)),
}
}
@@ -291,7 +291,7 @@ mod tests {
use openshell_core::proto::{
CreateProviderRequest, CreateSandboxRequest, GpuResourceRequirements, Provider,
SandboxSpec, UpdateConfigRequest, workspace_selector,
SandboxSpec, UpdateConfigRequest, sandbox_reference_by_name, workspace_selector,
};
use prost::Message as _;
use prost_types::{
@@ -454,12 +454,11 @@ mod tests {
let codec =
ProtoJsonCodec::from_descriptor_set(openshell_core::FILE_DESCRIPTOR_SET).unwrap();
let request = UpdateConfigRequest {
name: "demo".to_string(),
sandbox_ref: Some(sandbox_reference_by_name("default", "demo")),
annotations: HashMap::from([(
"openshell.nvidia.com/policy-signature".to_string(),
"signed".to_string(),
)]),
workspace_scope: Some(workspace_selector("default")),
..Default::default()
};
let bytes = request.encode_to_vec();
@@ -1009,7 +1009,9 @@ mod tests {
codec: codec.clone(),
};
let request = UpdateConfigRequest {
name: "demo".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default", "demo",
)),
expected_resource_version: u64::MAX - 1,
annotations: HashMap::from([
("policy-hash".to_string(), "sha256:v2:abc".to_string()),
@@ -1017,7 +1019,6 @@ mod tests {
("policy-signature-kid".to_string(), "kid".to_string()),
("correlation-id".to_string(), "reload-1".to_string()),
]),
workspace_scope: Some(openshell_core::proto::workspace_selector("default")),
..UpdateConfigRequest::default()
};
let body = GrpcFrame {
@@ -1186,7 +1187,10 @@ mod tests {
let operation = ValidatedOperation::new(
&codec,
"openshell.v1.UpdateConfigRequest",
json!({"name": "demo", "expectedResourceVersion": "7"}),
json!({
"sandboxRef": {"name": "demo", "workspaceScope": {"workspace": "default"}},
"expectedResourceVersion": "7"
}),
)
.unwrap();
let prior = operation.clone();
@@ -1224,7 +1228,10 @@ mod tests {
let operation = ValidatedOperation::new(
&codec,
"openshell.v1.UpdateConfigRequest",
json!({"name": "demo", "expectedResourceVersion": "7"}),
json!({
"sandboxRef": {"name": "demo", "workspaceScope": {"workspace": "default"}},
"expectedResourceVersion": "7"
}),
)
.unwrap();
let plan = test_modify_plan(FailurePolicy::FailClosed);
@@ -1262,12 +1269,15 @@ mod tests {
let operation = ValidatedOperation::new(
&codec,
"openshell.v1.UpdateConfigRequest",
json!({"name": "demo", "expectedResourceVersion": "7"}),
json!({
"sandboxRef": {"name": "demo", "workspaceScope": {"workspace": "default"}},
"expectedResourceVersion": "7"
}),
)
.unwrap();
let prior = operation.clone();
let result = allowed_result(vec![
patch("replace", "/name", json!("partially-mutated")),
patch("replace", "/sandboxRef/name", json!("partially-mutated")),
patch("replace", "/expectedResourceVersion", json!("not-a-number")),
]);
@@ -1281,7 +1291,7 @@ mod tests {
.unwrap();
assert_eq!(outcome, prior);
assert_eq!(outcome.json["name"], "demo");
assert_eq!(outcome.json["sandboxRef"]["name"], "demo");
}
#[tokio::test]
@@ -1292,17 +1302,20 @@ mod tests {
let operation = ValidatedOperation::new(
&codec,
"openshell.v1.UpdateConfigRequest",
json!({"name": "demo", "expectedResourceVersion": "7"}),
json!({
"sandboxRef": {"name": "demo", "workspaceScope": {"workspace": "default"}},
"expectedResourceVersion": "7"
}),
)
.unwrap();
let plan = test_modify_plan(FailurePolicy::FailOpen);
let invalid_first = allowed_result(vec![
patch("replace", "/name", json!("rejected-candidate")),
patch("replace", "/sandboxRef/name", json!("rejected-candidate")),
patch("replace", "/expectedResourceVersion", json!("not-a-number")),
]);
let second = allowed_result(vec![
patch("test", "/name", json!("demo")),
patch("replace", "/name", json!("accepted-candidate")),
patch("test", "/sandboxRef/name", json!("demo")),
patch("replace", "/sandboxRef/name", json!("accepted-candidate")),
]);
let operation = apply_evaluation_result(
@@ -1322,9 +1335,14 @@ mod tests {
)
.unwrap();
assert_eq!(operation.json["name"], "accepted-candidate");
assert_eq!(operation.json["sandboxRef"]["name"], "accepted-candidate");
let decoded = UpdateConfigRequest::decode(operation.encoded.as_slice()).unwrap();
assert_eq!(decoded.name, "accepted-candidate");
assert_eq!(
decoded.sandbox_ref.unwrap().identifier,
Some(openshell_core::proto::sandbox_reference::Identifier::Name(
"accepted-candidate".to_string()
))
);
assert_eq!(decoded.expected_resource_version, 7);
}
@@ -1371,10 +1389,17 @@ mod tests {
let operation = ValidatedOperation::new(
&codec,
"openshell.v1.UpdateConfigRequest",
json!({"name": "demo", "expectedResourceVersion": "7"}),
json!({
"sandboxRef": {"name": "demo", "workspaceScope": {"workspace": "default"}},
"expectedResourceVersion": "7"
}),
)
.unwrap();
let result = allowed_result(vec![patch("replace", "/name", json!("accepted"))]);
let result = allowed_result(vec![patch(
"replace",
"/sandboxRef/name",
json!("accepted"),
)]);
let recorder = TestRecorder::default();
let operation = metrics::with_local_recorder(&recorder, || {
@@ -1389,7 +1414,12 @@ mod tests {
.unwrap();
let decoded = UpdateConfigRequest::decode(operation.encoded.as_slice()).unwrap();
assert_eq!(decoded.name, "accepted");
assert_eq!(
decoded.sandbox_ref.unwrap().identifier,
Some(openshell_core::proto::sandbox_reference::Identifier::Name(
"accepted".to_string()
))
);
assert_eq!(TestRecorder::count(&recorder.evaluations), 1);
assert_eq!(TestRecorder::count(&recorder.patches), 1);
assert_eq!(TestRecorder::count(&recorder.fail_open), 0);
+10 -18
View File
@@ -248,8 +248,7 @@ impl OpenShellClient {
let response = self
.unary(|mut grpc| {
let request = proto::GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(proto::workspace_selector("default")),
sandbox_ref: Some(proto::sandbox_reference_by_name("default", name)),
};
async move { grpc.get_sandbox(request).await }
})
@@ -287,8 +286,7 @@ impl OpenShellClient {
let response = self
.unary(|mut grpc| {
let request = proto::DeleteSandboxRequest {
name: name.to_string(),
workspace_scope: Some(proto::workspace_selector("default")),
sandbox_ref: Some(proto::sandbox_reference_by_name("default", name)),
};
async move { grpc.delete_sandbox(request).await }
})
@@ -301,8 +299,7 @@ impl OpenShellClient {
let response = self
.unary(|mut grpc| {
let request = proto::StopSandboxRequest {
name: name.to_string(),
workspace_scope: Some(proto::workspace_selector("default")),
sandbox_ref: Some(proto::sandbox_reference_by_name("default", name)),
};
async move { grpc.stop_sandbox(request).await }
})
@@ -315,8 +312,7 @@ impl OpenShellClient {
let response = self
.unary(|mut grpc| {
let request = proto::StartSandboxRequest {
name: name.to_string(),
workspace_scope: Some(proto::workspace_selector("default")),
sandbox_ref: Some(proto::sandbox_reference_by_name("default", name)),
};
async move { grpc.start_sandbox(request).await }
})
@@ -478,7 +474,7 @@ impl OpenShellClient {
pub async fn exec(&self, name: &str, cmd: &[String], opts: ExecOptions) -> Result<ExecResult> {
let sandbox = self.get_sandbox(name).await?;
let request = proto::ExecSandboxRequest {
sandbox_id: sandbox.id,
sandbox_ref: Some(proto::sandbox_reference_by_id(sandbox.id)),
command: cmd.to_vec(),
workdir: opts.workdir.unwrap_or_default(),
environment: opts.environment,
@@ -736,8 +732,7 @@ impl WorkspaceScopedClient {
.client
.unary(|mut grpc| {
let request = proto::GetSandboxRequest {
name: name.to_string(),
workspace_scope: Some(proto::workspace_selector(&self.workspace)),
sandbox_ref: Some(proto::sandbox_reference_by_name(&self.workspace, name)),
};
async move { grpc.get_sandbox(request).await }
})
@@ -772,8 +767,7 @@ impl WorkspaceScopedClient {
.client
.unary(|mut grpc| {
let request = proto::DeleteSandboxRequest {
name: name.to_string(),
workspace_scope: Some(proto::workspace_selector(&self.workspace)),
sandbox_ref: Some(proto::sandbox_reference_by_name(&self.workspace, name)),
};
async move { grpc.delete_sandbox(request).await }
})
@@ -787,8 +781,7 @@ impl WorkspaceScopedClient {
.client
.unary(|mut grpc| {
let request = proto::StopSandboxRequest {
name: name.to_string(),
workspace_scope: Some(proto::workspace_selector(&self.workspace)),
sandbox_ref: Some(proto::sandbox_reference_by_name(&self.workspace, name)),
};
async move { grpc.stop_sandbox(request).await }
})
@@ -802,8 +795,7 @@ impl WorkspaceScopedClient {
.client
.unary(|mut grpc| {
let request = proto::StartSandboxRequest {
name: name.to_string(),
workspace_scope: Some(proto::workspace_selector(&self.workspace)),
sandbox_ref: Some(proto::sandbox_reference_by_name(&self.workspace, name)),
};
async move { grpc.start_sandbox(request).await }
})
@@ -868,7 +860,7 @@ impl WorkspaceScopedClient {
pub async fn exec(&self, name: &str, cmd: &[String], opts: ExecOptions) -> Result<ExecResult> {
let sandbox = self.get_sandbox(name).await?;
let request = proto::ExecSandboxRequest {
sandbox_id: sandbox.id,
sandbox_ref: Some(proto::sandbox_reference_by_id(sandbox.id)),
command: cmd.to_vec(),
workdir: opts.workdir.unwrap_or_default(),
environment: opts.environment,
+43 -13
View File
@@ -39,6 +39,24 @@ fn selects_all_workspaces(scope: &Option<proto::datamodel::v1::WorkspaceSelector
)
}
fn sandbox_reference_name(reference: &Option<proto::SandboxReference>) -> Option<&str> {
match reference.as_ref()?.identifier.as_ref()? {
proto::sandbox_reference::Identifier::Name(name) => Some(name),
proto::sandbox_reference::Identifier::Id(_) => None,
}
}
fn sandbox_reference_id(reference: &Option<proto::SandboxReference>) -> Option<&str> {
match reference.as_ref()?.identifier.as_ref()? {
proto::sandbox_reference::Identifier::Id(id) => Some(id),
proto::sandbox_reference::Identifier::Name(_) => None,
}
}
fn sandbox_reference_workspace(reference: &Option<proto::SandboxReference>) -> Option<&str> {
selected_workspace(&reference.as_ref()?.workspace_scope)
}
/// Captured fixture state — what the mock observed and the canned replies it
/// returned. One per test so assertions are scoped.
#[derive(Default)]
@@ -297,9 +315,9 @@ impl OpenShell for TestOpenShell {
) -> Result<Response<proto::SandboxResponse>, Status> {
let request = request.into_inner();
let sandbox = sandbox_with_phase_ws(
&request.name,
sandbox_reference_name(&request.sandbox_ref).unwrap_or_default(),
proto::SandboxPhase::Stopped,
selected_workspace(&request.workspace_scope).unwrap_or("default"),
sandbox_reference_workspace(&request.sandbox_ref).unwrap_or("default"),
);
*self.state.last_stop.lock().await = Some(request);
Ok(Response::new(proto::SandboxResponse {
@@ -313,9 +331,9 @@ impl OpenShell for TestOpenShell {
) -> Result<Response<proto::SandboxResponse>, Status> {
let request = request.into_inner();
let sandbox = sandbox_with_phase_ws(
&request.name,
sandbox_reference_name(&request.sandbox_ref).unwrap_or_default(),
proto::SandboxPhase::Starting,
selected_workspace(&request.workspace_scope).unwrap_or("default"),
sandbox_reference_workspace(&request.sandbox_ref).unwrap_or("default"),
);
*self.state.last_start.lock().await = Some(request);
Ok(Response::new(proto::SandboxResponse {
@@ -328,10 +346,12 @@ impl OpenShell for TestOpenShell {
request: tonic::Request<proto::GetSandboxRequest>,
) -> Result<Response<proto::SandboxResponse>, Status> {
let req = request.into_inner();
let name = req.name;
let name = sandbox_reference_name(&req.sandbox_ref)
.unwrap_or_default()
.to_string();
*self.state.last_get_name.lock().await = Some(name.clone());
*self.state.last_get_workspace.lock().await =
selected_workspace(&req.workspace_scope).map(str::to_string);
sandbox_reference_workspace(&req.sandbox_ref).map(str::to_string);
let count = self.state.get_calls.fetch_add(1, Ordering::SeqCst);
if self.state.get_returns_not_found {
@@ -399,9 +419,10 @@ impl OpenShell for TestOpenShell {
request: tonic::Request<proto::DeleteSandboxRequest>,
) -> Result<Response<proto::DeleteSandboxResponse>, Status> {
let req = request.into_inner();
*self.state.last_delete_name.lock().await = Some(req.name);
*self.state.last_delete_name.lock().await =
sandbox_reference_name(&req.sandbox_ref).map(str::to_string);
*self.state.last_delete_workspace.lock().await =
selected_workspace(&req.workspace_scope).map(str::to_string);
sandbox_reference_workspace(&req.sandbox_ref).map(str::to_string);
Ok(Response::new(proto::DeleteSandboxResponse {
deleted: true,
}))
@@ -1104,8 +1125,11 @@ async fn stop_and_start_map_requests_and_phases() {
let stopped = client.stop_sandbox("sleepy").await.unwrap();
assert_eq!(stopped.phase, SandboxPhase::Stopped);
let stop = state.last_stop.lock().await.clone().unwrap();
assert_eq!(stop.name, "sleepy");
assert_eq!(selected_workspace(&stop.workspace_scope), Some("default"));
assert_eq!(sandbox_reference_name(&stop.sandbox_ref), Some("sleepy"));
assert_eq!(
sandbox_reference_workspace(&stop.sandbox_ref),
Some("default")
);
let started = client
.workspace("team-a")
@@ -1114,8 +1138,11 @@ async fn stop_and_start_map_requests_and_phases() {
.unwrap();
assert_eq!(started.phase, SandboxPhase::Starting);
let start = state.last_start.lock().await.clone().unwrap();
assert_eq!(start.name, "sleepy");
assert_eq!(selected_workspace(&start.workspace_scope), Some("team-a"));
assert_eq!(sandbox_reference_name(&start.sandbox_ref), Some("sleepy"));
assert_eq!(
sandbox_reference_workspace(&start.sandbox_ref),
Some("team-a")
);
}
#[tokio::test]
@@ -1247,7 +1274,10 @@ async fn exec_buffers_stdout_stderr_and_exit() {
assert_eq!(result.stderr, b"warn\n");
let observed = state.last_exec_request.lock().await.clone().unwrap();
assert_eq!(observed.sandbox_id, "id-my-box");
assert_eq!(
sandbox_reference_id(&observed.sandbox_ref),
Some("id-my-box")
);
assert_eq!(
observed.command,
vec!["echo".to_string(), "hello".to_string()]
+3 -1
View File
@@ -164,7 +164,9 @@ pub async fn handle_refresh_sandbox_token(
));
} else {
let mut config_request = Request::new(GetSandboxConfigRequest {
sandbox_id: sandbox.sandbox_id.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
&sandbox.sandbox_id,
)),
});
config_request
.extensions_mut()
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -6566,12 +6566,12 @@ mod tests {
let attached = super::super::sandbox::handle_attach_sandbox_provider(
&state,
authed_request(AttachSandboxProviderRequest {
sandbox_name: "sandbox-custom".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"sandbox-custom".to_string(),
)),
provider_name: "custom-provider".to_string(),
expected_resource_version: 0,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
File diff suppressed because it is too large Load Diff
+182 -169
View File
@@ -7,9 +7,9 @@ use std::sync::Arc;
use openshell_core::proto::datamodel::v1::ObjectMeta;
use openshell_core::proto::{
DeleteServiceRequest, DeleteServiceResponse, ExposeServiceRequest, GetServiceRequest,
ListServicesRequest, ListServicesResponse, Sandbox, ServiceEndpoint, ServiceEndpointResponse,
ListServicesRequest, ListServicesResponse, ServiceEndpoint, ServiceEndpointResponse,
};
use openshell_core::{ObjectId, ObjectWorkspace};
use openshell_core::{ObjectId, ObjectName, ObjectWorkspace};
use prost::Message as _;
use tonic::{Request, Response, Status};
use uuid::Uuid;
@@ -17,13 +17,11 @@ use uuid::Uuid;
use crate::ServerState;
use crate::auth::workspace_authz::{
AuthorizedWorkspaceScope, MinWorkspaceRole, authorize_list_workspace_selector,
authorize_workspace_selector,
};
use crate::persistence::{ObjectType, WriteCondition};
use crate::service_routing;
const MAX_SERVICE_NAME_LEN: usize = super::MAX_ROUTABLE_NAME_LEN;
const MAX_SANDBOX_NAME_LEN: usize = super::MAX_ROUTABLE_NAME_LEN;
pub(super) async fn handle_expose_service(
state: &Arc<ServerState>,
@@ -31,32 +29,25 @@ pub(super) async fn handle_expose_service(
) -> Result<Response<ServiceEndpointResponse>, Status> {
let principal = super::extract_principal(&request)?;
let req = request.into_inner();
let authz = authorize_workspace_selector(
&state.store,
&state.admin_role,
let sandbox = super::sandbox::resolve_and_authorize_sandbox_reference(
state,
&principal,
req.workspace_scope.as_ref(),
req.sandbox_ref.as_ref(),
MinWorkspaceRole::User,
)
.await?;
let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace)
.await?
.ensure_active()?;
validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?;
let workspace =
super::workspace::resolve_workspace(state.store.as_ref(), sandbox.object_workspace())
.await?
.ensure_active()?;
let sandbox_name = sandbox.object_name();
validate_optional_endpoint_name("service", &req.service, MAX_SERVICE_NAME_LEN)?;
if req.target_port == 0 || req.target_port > u32::from(u16::MAX) {
return Err(Status::invalid_argument("target_port must be in 1..=65535"));
}
let sandbox = state
.store
.get_message_by_name::<Sandbox>(&workspace, &req.sandbox)
.await
.map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))?
.ok_or_else(|| Status::not_found("sandbox not found"))?;
let now = crate::persistence::current_time_ms();
let key = service_routing::endpoint_key(&req.sandbox, &req.service);
let key = service_routing::endpoint_key(sandbox_name, &req.service);
// Fetch existing endpoint to determine create vs. update path
let existing = state
@@ -92,7 +83,7 @@ pub(super) async fn handle_expose_service(
let labels_json = serde_json::to_string(&HashMap::from([(
"sandbox".to_string(),
req.sandbox.clone(),
sandbox_name.to_string(),
)]))
.map_err(|e| Status::internal(format!("serialize labels failed: {e}")))?;
@@ -101,14 +92,14 @@ pub(super) async fn handle_expose_service(
id: id.clone(),
name: key.clone(),
created_at_ms,
labels: HashMap::from([("sandbox".to_string(), req.sandbox.clone())]),
labels: HashMap::from([("sandbox".to_string(), sandbox_name.to_string())]),
resource_version: 0,
annotations: HashMap::new(),
workspace: workspace.clone(),
deletion_timestamp_ms: 0,
}),
sandbox_id: sandbox.object_id().to_string(),
sandbox_name: req.sandbox.clone(),
sandbox_name: sandbox_name.to_string(),
service_name: req.service.clone(),
target_port: req.target_port,
domain: true,
@@ -134,7 +125,7 @@ pub(super) async fn handle_expose_service(
meta.resource_version = result.resource_version;
}
let url = service_routing::endpoint_url(&state.config, &workspace, &req.sandbox, &req.service)
let url = service_routing::endpoint_url(&state.config, &workspace, sandbox_name, &req.service)
.unwrap_or_default();
service_routing::emit_service_endpoint_config_event(&endpoint, &url, created);
@@ -150,21 +141,18 @@ pub(super) async fn handle_get_service(
) -> Result<Response<ServiceEndpointResponse>, Status> {
let principal = super::extract_principal(&request)?;
let req = request.into_inner();
let authz = authorize_workspace_selector(
&state.store,
&state.admin_role,
let sandbox = super::sandbox::resolve_and_authorize_sandbox_reference(
state,
&principal,
req.workspace_scope.as_ref(),
req.sandbox_ref.as_ref(),
MinWorkspaceRole::User,
)
.await?;
let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace)
.await?
.name;
validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?;
let workspace = sandbox.object_workspace();
let sandbox_name = sandbox.object_name();
validate_optional_endpoint_name("service", &req.service, MAX_SERVICE_NAME_LEN)?;
let endpoint = get_service_endpoint(state, &workspace, &req.sandbox, &req.service)
let endpoint = get_service_endpoint(state, workspace, sandbox_name, &req.service)
.await?
.ok_or_else(|| Status::not_found("service endpoint not found"))?;
@@ -177,11 +165,36 @@ pub(super) async fn handle_list_services(
) -> Result<Response<ListServicesResponse>, Status> {
let principal = super::extract_principal(&request)?;
let req = request.into_inner();
if !req.sandbox.is_empty() {
validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?;
}
let limit = super::clamp_limit(req.limit, 100, super::MAX_PAGE_SIZE);
if req.sandbox_ref.is_some() {
if req.workspace_scope.is_some() {
return Err(Status::invalid_argument(
"workspace_scope must be omitted when sandbox_ref is set",
));
}
let sandbox = super::sandbox::resolve_and_authorize_sandbox_reference(
state,
&principal,
req.sandbox_ref.as_ref(),
MinWorkspaceRole::User,
)
.await?;
let endpoints = state
.store
.list_messages_with_selector::<ServiceEndpoint>(
sandbox.object_workspace(),
&format!("sandbox={}", sandbox.object_name()),
limit,
req.offset,
)
.await
.map_err(|e| Status::internal(format!("list endpoints failed: {e}")))?;
let services = endpoints
.into_iter()
.map(|endpoint| service_endpoint_response(state, endpoint))
.collect();
return Ok(Response::new(ListServicesResponse { services }));
}
let scope = authorize_list_workspace_selector(
&state.store,
&state.admin_role,
@@ -192,11 +205,6 @@ pub(super) async fn handle_list_services(
.await?;
let endpoints: Vec<ServiceEndpoint> =
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 {
let AuthorizedWorkspaceScope::Workspace(authz) = scope else {
@@ -206,22 +214,10 @@ pub(super) async fn handle_list_services(
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
}
state
.store
.list_messages(&workspace, limit, req.offset)
.await
}
.map_err(|e| Status::internal(format!("list endpoints failed: {e}")))?;
@@ -239,29 +235,26 @@ pub(super) async fn handle_delete_service(
) -> Result<Response<DeleteServiceResponse>, Status> {
let principal = super::extract_principal(&request)?;
let req = request.into_inner();
let authz = authorize_workspace_selector(
&state.store,
&state.admin_role,
let sandbox = super::sandbox::resolve_and_authorize_sandbox_reference(
state,
&principal,
req.workspace_scope.as_ref(),
req.sandbox_ref.as_ref(),
MinWorkspaceRole::User,
)
.await?;
let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace)
.await?
.name;
validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?;
let workspace = sandbox.object_workspace();
let sandbox_name = sandbox.object_name();
validate_optional_endpoint_name("service", &req.service, MAX_SERVICE_NAME_LEN)?;
let endpoint = get_service_endpoint(state, &workspace, &req.sandbox, &req.service).await?;
let endpoint = get_service_endpoint(state, workspace, sandbox_name, &req.service).await?;
let Some(endpoint) = endpoint else {
return Ok(Response::new(DeleteServiceResponse { deleted: false }));
};
let key = service_routing::endpoint_key(&req.sandbox, &req.service);
let key = service_routing::endpoint_key(sandbox_name, &req.service);
let deleted = state
.store
.delete_by_name(ServiceEndpoint::object_type(), &workspace, &key)
.delete_by_name(ServiceEndpoint::object_type(), workspace, &key)
.await
.map_err(|e| Status::internal(format!("delete endpoint failed: {e}")))?;
@@ -305,6 +298,7 @@ fn service_endpoint_response(
}
#[allow(clippy::result_large_err)]
#[cfg(test)]
fn validate_endpoint_name(field: &str, value: &str, max_len: usize) -> Result<(), Status> {
if value.is_empty() {
return Err(Status::invalid_argument(format!("{field} is required")));
@@ -357,7 +351,7 @@ fn is_dns_label(value: &str) -> bool {
mod tests {
use super::*;
use crate::grpc::test_support::{authed_request, test_server_state};
use openshell_core::proto::SandboxPhase;
use openshell_core::proto::{Sandbox, SandboxPhase};
async fn seed_sandbox(state: &Arc<ServerState>, name: &str) {
let mut sandbox = Sandbox {
@@ -411,13 +405,13 @@ mod tests {
let exposed = handle_expose_service(
&state,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
target_port: 8080,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -428,12 +422,13 @@ mod tests {
let listed = handle_list_services(
&state,
authed_request(ListServicesRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
workspace_scope: None,
limit: 0,
offset: 0,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -448,11 +443,11 @@ mod tests {
let fetched = handle_get_service(
&state,
authed_request(GetServiceRequest {
sandbox: "my-sandbox".to_string(),
service: "web".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
}),
)
.await
@@ -463,11 +458,11 @@ mod tests {
let deleted = handle_delete_service(
&state,
authed_request(DeleteServiceRequest {
sandbox: "my-sandbox".to_string(),
service: "web".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
}),
)
.await
@@ -478,11 +473,11 @@ mod tests {
let err = handle_get_service(
&state,
authed_request(GetServiceRequest {
sandbox: "my-sandbox".to_string(),
service: "web".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
}),
)
.await
@@ -492,12 +487,13 @@ mod tests {
let listed = handle_list_services(
&state,
authed_request(ListServicesRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
workspace_scope: None,
limit: 0,
offset: 0,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -517,13 +513,13 @@ mod tests {
handle_expose_service(
&state1,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
target_port: 8080,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -534,13 +530,13 @@ mod tests {
handle_expose_service(
&state2,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
target_port: 9090,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -562,12 +558,13 @@ mod tests {
let listed = handle_list_services(
&state,
authed_request(ListServicesRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
workspace_scope: None,
limit: 0,
offset: 0,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -585,13 +582,13 @@ mod tests {
handle_expose_service(
&state,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
target_port: 7070,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -603,13 +600,13 @@ mod tests {
handle_expose_service(
&state1,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
target_port: 8080,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -620,13 +617,13 @@ mod tests {
handle_expose_service(
&state2,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
target_port: 9090,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -647,11 +644,11 @@ mod tests {
let fetched = handle_get_service(
&state,
authed_request(GetServiceRequest {
sandbox: "my-sandbox".to_string(),
service: "web".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
}),
)
.await
@@ -708,13 +705,13 @@ mod tests {
handle_expose_service(
&state,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
target_port: 8080,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -723,13 +720,13 @@ mod tests {
handle_expose_service(
&state,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"beta".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
target_port: 9090,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"beta".to_string(),
)),
}),
)
.await
@@ -739,11 +736,11 @@ mod tests {
let got = handle_get_service(
&state,
authed_request(GetServiceRequest {
sandbox: "my-sandbox".to_string(),
service: "web".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
}),
)
.await
@@ -755,11 +752,11 @@ mod tests {
let got = handle_get_service(
&state,
authed_request(GetServiceRequest {
sandbox: "my-sandbox".to_string(),
service: "web".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"beta".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
}),
)
.await
@@ -771,12 +768,13 @@ mod tests {
let listed = handle_list_services(
&state,
authed_request(ListServicesRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
workspace_scope: None,
limit: 100,
offset: 0,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -791,12 +789,13 @@ mod tests {
let listed = handle_list_services(
&state,
authed_request(ListServicesRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"beta".to_string(),
"my-sandbox".to_string(),
)),
workspace_scope: None,
limit: 100,
offset: 0,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"beta".to_string(),
)),
}),
)
.await
@@ -812,11 +811,11 @@ mod tests {
let deleted = handle_delete_service(
&state,
authed_request(DeleteServiceRequest {
sandbox: "my-sandbox".to_string(),
service: "web".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
}),
)
.await
@@ -827,12 +826,13 @@ mod tests {
let listed = handle_list_services(
&state,
authed_request(ListServicesRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
workspace_scope: None,
limit: 100,
offset: 0,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -843,11 +843,11 @@ mod tests {
let got = handle_get_service(
&state,
authed_request(GetServiceRequest {
sandbox: "my-sandbox".to_string(),
service: "web".to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"beta".to_string(),
"my-sandbox".to_string(),
)),
service: "web".to_string(),
}),
)
.await
@@ -860,13 +860,13 @@ mod tests {
handle_expose_service(
&state,
authed_request(ExposeServiceRequest {
sandbox: "my-sandbox".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default".to_string(),
"my-sandbox".to_string(),
)),
service: "api".to_string(),
target_port: 3000,
domain: true,
workspace_scope: Some(openshell_core::proto::workspace_selector(
"default".to_string(),
)),
}),
)
.await
@@ -875,10 +875,10 @@ mod tests {
let listed = handle_list_services(
&state,
authed_request(ListServicesRequest {
sandbox: String::new(),
sandbox_ref: None,
workspace_scope: Some(openshell_core::proto::all_workspaces_selector()),
limit: 100,
offset: 0,
workspace_scope: Some(openshell_core::proto::all_workspaces_selector()),
}),
)
.await
@@ -891,7 +891,7 @@ mod tests {
/// when targeting a workspace that does not exist. Returning `NOT_FOUND`
/// would create a CWE-203 workspace-name oracle.
#[tokio::test]
async fn non_member_gets_permission_denied_not_workspace_oracle() {
async fn non_member_gets_gets_not_found_without_sandbox_oracle() {
use crate::auth::identity::{Identity, IdentityProvider};
use crate::auth::principal::{Principal, UserPrincipal};
@@ -915,7 +915,10 @@ mod tests {
let err = handle_expose_service(
&state,
non_member_request(ExposeServiceRequest {
workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"no-such-ws",
"any",
)),
..Default::default()
}),
)
@@ -923,15 +926,18 @@ mod tests {
.unwrap_err();
assert_eq!(
err.code(),
tonic::Code::PermissionDenied,
"handle_expose_service should return PermissionDenied, got {:?}",
tonic::Code::NotFound,
"handle_expose_service should return NotFound, got {:?}",
err.code()
);
let err = handle_get_service(
&state,
non_member_request(GetServiceRequest {
workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"no-such-ws",
"any",
)),
..Default::default()
}),
)
@@ -939,15 +945,19 @@ mod tests {
.unwrap_err();
assert_eq!(
err.code(),
tonic::Code::PermissionDenied,
"handle_get_service should return PermissionDenied, got {:?}",
tonic::Code::NotFound,
"handle_get_service should return NotFound, got {:?}",
err.code()
);
let err = handle_list_services(
&state,
non_member_request(ListServicesRequest {
workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"no-such-ws",
"any",
)),
workspace_scope: None,
..Default::default()
}),
)
@@ -955,15 +965,18 @@ mod tests {
.unwrap_err();
assert_eq!(
err.code(),
tonic::Code::PermissionDenied,
"handle_list_services should return PermissionDenied, got {:?}",
tonic::Code::NotFound,
"handle_list_services should return NotFound, got {:?}",
err.code()
);
let err = handle_delete_service(
&state,
non_member_request(DeleteServiceRequest {
workspace_scope: Some(openshell_core::proto::workspace_selector("no-such-ws")),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"no-such-ws",
"any",
)),
..Default::default()
}),
)
@@ -971,8 +984,8 @@ mod tests {
.unwrap_err();
assert_eq!(
err.code(),
tonic::Code::PermissionDenied,
"handle_delete_service should return PermissionDenied, got {:?}",
tonic::Code::NotFound,
"handle_delete_service should return NotFound, got {:?}",
err.code()
);
}
+21 -6
View File
@@ -43,6 +43,9 @@ pub(super) const MAX_MAIN_PROCESS_ARGV_SIZE: usize = 256 * 1024;
/// Command arguments only reject NUL (newlines are valid for inline scripts).
/// Environment values and workdir reject both NUL and newlines.
pub(super) fn validate_exec_request_fields(req: &ExecSandboxRequest) -> Result<(), Status> {
if req.sandbox_ref.is_none() {
return Err(Status::invalid_argument("sandbox_ref is required"));
}
if req.command.len() > MAX_EXEC_COMMAND_ARGS {
return Err(Status::invalid_argument(format!(
"command array exceeds {MAX_EXEC_COMMAND_ARGS} argument limit"
@@ -1351,7 +1354,9 @@ mod tests {
#[test]
fn validate_exec_request_rejects_reserved_env_key() {
let req = ExecSandboxRequest {
sandbox_id: "id".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
"id".to_string(),
)),
command: vec!["echo".to_string()],
environment: std::iter::once(("OPENSHELL_SANDBOX_ID".to_string(), "evil".to_string()))
.collect(),
@@ -1368,7 +1373,9 @@ mod tests {
#[test]
fn validate_exec_request_allows_pyfunc_helper_key() {
let req = ExecSandboxRequest {
sandbox_id: "id".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
"id".to_string(),
)),
command: vec!["python".to_string()],
environment: std::iter::once(("OPENSHELL_PYFUNC_B64".to_string(), "data".to_string()))
.collect(),
@@ -2246,7 +2253,9 @@ mod tests {
#[test]
fn validate_exec_allows_newlines_in_command_args() {
let req = ExecSandboxRequest {
sandbox_id: "test".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
"test".to_string(),
)),
command: vec![
"python3".to_string(),
"-c".to_string(),
@@ -2260,7 +2269,9 @@ mod tests {
#[test]
fn validate_exec_still_rejects_null_bytes_in_command_args() {
let req = ExecSandboxRequest {
sandbox_id: "test".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
"test".to_string(),
)),
command: vec!["echo".to_string(), "hello\x00world".to_string()],
..Default::default()
};
@@ -2271,7 +2282,9 @@ mod tests {
#[test]
fn validate_exec_still_rejects_newlines_in_workdir() {
let req = ExecSandboxRequest {
sandbox_id: "test".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
"test".to_string(),
)),
command: vec!["ls".to_string()],
workdir: "/tmp\nmalicious".to_string(),
..Default::default()
@@ -2283,7 +2296,9 @@ mod tests {
#[test]
fn validate_exec_still_rejects_newlines_in_env_values() {
let req = ExecSandboxRequest {
sandbox_id: "test".to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
"test".to_string(),
)),
command: vec!["ls".to_string()],
environment: std::iter::once(("VAR".to_string(), "val\nmalicious".to_string()))
.collect(),
@@ -76,7 +76,7 @@ async fn run_get_sandbox_config(args: &[String]) -> Result<i32> {
let mut client = open_client().await?;
let resp = client
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_id: sandbox_id.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
})
.await;
match resp {
+58 -38
View File
@@ -653,17 +653,16 @@ fn spawn_log_stream(app: &mut App, tx: mpsc::UnboundedSender<Event>) {
};
let mut client = app.client.clone();
let workspace = app.selected_sandbox_workspace();
let handle = tokio::spawn(async move {
// Phase 1: Fetch initial history via unary RPC.
let req = openshell_core::proto::GetSandboxLogsRequest {
sandbox_id: sandbox_id.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox_id.clone(),
)),
lines: 500,
since_ms: 0,
sources: vec![],
min_level: String::new(),
workspace_scope: Some(named_workspace_scope(workspace)),
};
match tokio::time::timeout(Duration::from_secs(5), client.get_sandbox_logs(req)).await {
@@ -700,7 +699,7 @@ fn spawn_log_stream(app: &mut App, tx: mpsc::UnboundedSender<Event>) {
// Phase 2: Stream live logs via WatchSandbox.
let req = openshell_core::proto::WatchSandboxRequest {
id: sandbox_id,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
follow_status: false,
follow_logs: true,
follow_events: false,
@@ -765,8 +764,10 @@ async fn handle_sandbox_delete(app: &mut App) {
}
let req = openshell_core::proto::DeleteSandboxRequest {
name: sandbox_name,
workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
app.selected_sandbox_workspace(),
sandbox_name,
)),
};
match app.client.delete_sandbox(req).await {
Ok(_) => {
@@ -798,8 +799,10 @@ async fn fetch_sandbox_detail(app: &mut App) {
};
let req = openshell_core::proto::GetSandboxRequest {
name: sandbox_name.clone(),
workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
app.selected_sandbox_workspace(),
sandbox_name.clone(),
)),
};
// Step 1: Fetch sandbox metadata (providers, sandbox ID).
@@ -828,7 +831,9 @@ async fn fetch_sandbox_detail(app: &mut App) {
// Step 2: Fetch the current live policy (includes updates since creation).
if let Some(id) = sandbox_id {
let policy_req = openshell_core::proto::GetSandboxConfigRequest { sandbox_id: id };
let policy_req = openshell_core::proto::GetSandboxConfigRequest {
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(id)),
};
match tokio::time::timeout(
Duration::from_secs(5),
@@ -884,8 +889,10 @@ async fn handle_shell_connect(
// Step 1: Get sandbox ID.
let sandbox_id = {
let req = openshell_core::proto::GetSandboxRequest {
name: sandbox_name.clone(),
workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
app.selected_sandbox_workspace(),
sandbox_name.clone(),
)),
};
match tokio::time::timeout(Duration::from_secs(5), app.client.get_sandbox(req)).await {
Ok(Ok(resp)) => {
@@ -910,7 +917,9 @@ async fn handle_shell_connect(
// Step 2: Create SSH session.
let session = {
let req = openshell_core::proto::CreateSshSessionRequest {
sandbox_id: sandbox_id.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox_id.clone(),
)),
};
match tokio::time::timeout(Duration::from_secs(5), app.client.create_ssh_session(req)).await
{
@@ -1041,8 +1050,10 @@ async fn handle_exec_command(
// Step 1: Resolve sandbox → SSH session (same as handle_shell_connect).
let sandbox_id = {
let req = openshell_core::proto::GetSandboxRequest {
name: sandbox_name.to_string(),
workspace_scope: Some(named_workspace_scope(workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace,
sandbox_name,
)),
};
match tokio::time::timeout(Duration::from_secs(5), app.client.get_sandbox(req)).await {
Ok(Ok(resp)) => {
@@ -1066,7 +1077,9 @@ async fn handle_exec_command(
let session = {
let req = openshell_core::proto::CreateSshSessionRequest {
sandbox_id: sandbox_id.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox_id.clone(),
)),
};
match tokio::time::timeout(Duration::from_secs(5), app.client.create_ssh_session(req)).await
{
@@ -1460,8 +1473,10 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender<Event>) {
tokio::time::sleep(Duration::from_secs(2)).await;
let req = openshell_core::proto::GetSandboxRequest {
name: sandbox_name.clone(),
workspace_scope: Some(named_workspace_scope(&workspace)),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
&workspace,
sandbox_name.clone(),
)),
};
// Retry on transient errors.
if let Ok(resp) = client.get_sandbox(req).await
@@ -1519,7 +1534,7 @@ async fn start_port_forwards(
// Create SSH session.
let session = {
let req = openshell_core::proto::CreateSshSessionRequest {
sandbox_id: sandbox_id.to_string(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
};
match tokio::time::timeout(Duration::from_secs(10), client.create_ssh_session(req)).await {
Ok(Ok(resp)) => resp.into_inner(),
@@ -1894,9 +1909,10 @@ fn spawn_draft_approve(app: &App, tx: mpsc::UnboundedSender<Event>) {
tokio::spawn(async move {
let req = openshell_core::proto::ApproveDraftChunkRequest {
name,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
chunk_id,
workspace_scope: Some(named_workspace_scope(workspace)),
review_token,
};
match tokio::time::timeout(Duration::from_secs(5), client.approve_draft_chunk(req)).await {
@@ -1939,10 +1955,11 @@ fn spawn_draft_reject(app: &App, tx: mpsc::UnboundedSender<Event>) {
tokio::spawn(async move {
let req = openshell_core::proto::RejectDraftChunkRequest {
name,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
chunk_id,
reason: String::new(),
workspace_scope: Some(named_workspace_scope(workspace)),
};
match tokio::time::timeout(Duration::from_secs(5), client.reject_draft_chunk(req)).await {
Ok(Ok(_)) => {
@@ -1989,9 +2006,10 @@ fn spawn_draft_approve_all(
})
.collect();
let req = openshell_core::proto::ApproveAllDraftChunksRequest {
name,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
include_security_flagged: false,
workspace_scope: Some(named_workspace_scope(workspace)),
approvals,
};
match tokio::time::timeout(
@@ -2262,11 +2280,10 @@ async fn refresh_global_settings(app: &mut App) {
// Check for an active global policy only while the caller can read it.
let policy_req = openshell_core::proto::ListSandboxPoliciesRequest {
name: String::new(),
sandbox_ref: None,
limit: 1,
offset: 0,
global: true,
workspace_scope: None,
};
match tokio::time::timeout(
Duration::from_secs(5),
@@ -2342,7 +2359,6 @@ fn spawn_set_global_setting(app: &App, tx: mpsc::UnboundedSender<Event>) {
};
let req = UpdateConfigRequest {
name: String::new(),
setting_key: key,
setting_value: Some(SettingValue { value: Some(value) }),
global: true,
@@ -2375,7 +2391,6 @@ fn spawn_delete_global_setting(app: &App, tx: mpsc::UnboundedSender<Event>) {
use openshell_core::proto::UpdateConfigRequest;
let req = UpdateConfigRequest {
name: String::new(),
setting_key: key,
delete_setting: true,
global: true,
@@ -2443,10 +2458,11 @@ fn spawn_set_sandbox_setting(app: &App, tx: mpsc::UnboundedSender<Event>) {
};
let req = UpdateConfigRequest {
name,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
setting_key: key,
setting_value: Some(SettingValue { value: Some(value) }),
workspace_scope: Some(named_workspace_scope(workspace)),
..Default::default()
};
@@ -2481,10 +2497,11 @@ fn spawn_delete_sandbox_setting(app: &App, tx: mpsc::UnboundedSender<Event>) {
use openshell_core::proto::UpdateConfigRequest;
let req = UpdateConfigRequest {
name,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
workspace, name,
)),
setting_key: key,
delete_setting: true,
workspace_scope: Some(named_workspace_scope(workspace)),
..Default::default()
};
@@ -2640,7 +2657,9 @@ async fn refresh_sandbox_policy(app: &mut App) {
None => return,
};
let policy_req = openshell_core::proto::GetSandboxConfigRequest { sandbox_id };
let policy_req = openshell_core::proto::GetSandboxConfigRequest {
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
};
match tokio::time::timeout(
Duration::from_secs(5),
@@ -2678,9 +2697,11 @@ async fn refresh_draft_chunks(app: &mut App) {
};
let req = openshell_core::proto::GetDraftPolicyRequest {
name: sandbox_name,
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
app.selected_sandbox_workspace(),
sandbox_name,
)),
status_filter: String::new(),
workspace_scope: Some(named_workspace_scope(app.selected_sandbox_workspace())),
};
if let Ok(Ok(resp)) =
@@ -2709,9 +2730,8 @@ async fn refresh_sandbox_draft_counts(app: &mut App) {
.cloned()
.unwrap_or_else(|| app.current_workspace.clone());
let req = openshell_core::proto::GetDraftPolicyRequest {
name: name.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(ws, name)),
status_filter: "pending".to_string(),
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
+18 -1
View File
@@ -166,7 +166,9 @@ The public API represents this choice with a `WorkspaceSelector` oneof. Set
`all_workspaces` marker on list requests that support it. Omitting the selector
or sending an unset selector is invalid for workspace-scoped operations. The
all-workspaces variant is accepted only by sandbox, sandbox template, provider,
and service list requests, and it requires Platform Admin access.
and service list requests, and it requires Platform Admin access. For services,
choose either a sandbox name or `--all-workspaces`; the options are mutually
exclusive.
Clients migrating from the previous request fields should make the scope
explicit:
@@ -178,6 +180,21 @@ explicit:
| `all_workspaces: true` | `workspace_scope.all_workspaces: {}` |
| `global: true` | Omit `workspace_scope` |
Sandbox-scoped RPCs use a `SandboxReference` instead of separate sandbox and
workspace fields. Use the name variant for human-readable lookup and include a
named workspace selector. Use the ID variant when the canonical sandbox ID is
already known; ID references must omit `workspace_scope`.
| Previous request fields | Sandbox reference |
| --- | --- |
| `name: "agent"`, `workspace_scope.workspace: "team-ml"` | `sandbox_ref.name: "agent"`, `sandbox_ref.workspace_scope.workspace: "team-ml"` |
| `sandbox_name: "agent"`, `workspace_scope.workspace: "team-ml"` | `sandbox_ref.name: "agent"`, `sandbox_ref.workspace_scope.workspace: "team-ml"` |
| `sandbox_id: "<canonical-id>"` | `sandbox_ref.id: "<canonical-id>"` |
Do not set both `sandbox_ref` and a request-level `workspace_scope`. List RPCs
that are not filtered to one sandbox continue to use the request-level
`workspace_scope` selector.
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.
+4 -2
View File
@@ -1252,11 +1252,13 @@ async fn propagate_policy_to_running_sandboxes(
.map_or(0, |metadata| metadata.resource_version);
let result = client
.update_config(UpdateConfigRequest {
name: name.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default",
name.clone(),
)),
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;
@@ -57,7 +57,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let before = client
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_id: sandbox_id.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(
sandbox_id.clone(),
)),
})
.await?
.into_inner();
@@ -83,9 +85,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy_result = client
.update_config(UpdateConfigRequest {
name: sandbox_name.clone(),
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_name(
"default",
sandbox_name.clone(),
)),
policy: Some(widened_policy),
workspace_scope: Some(openshell_core::proto::workspace_selector("default")),
..Default::default()
})
.await;
@@ -120,7 +124,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.map_err(|status| format!("telemetry-only policy analysis was denied: {status}"))?;
let after = client
.get_sandbox_config(GetSandboxConfigRequest { sandbox_id })
.get_sandbox_config(GetSandboxConfigRequest {
sandbox_ref: Some(openshell_core::proto::sandbox_reference_by_id(sandbox_id)),
})
.await?
.into_inner();
if after.version != before.version || after.policy_hash != before.policy_hash {
+15
View File
@@ -27,6 +27,21 @@ message WorkspaceSelector {
// Marker for the all-workspaces selector variant.
message AllWorkspaces {}
// Identifies one existing sandbox in the public API.
//
// Exactly one identifier is required. `id` is the immutable, globally unique
// gateway-generated ObjectMeta.id and MUST NOT include workspace_scope. `name`
// is the human-readable ObjectMeta.name and requires a named workspace_scope;
// all_workspaces is never valid for a sandbox reference.
message SandboxReference {
oneof identifier {
string id = 1;
string name = 2;
}
WorkspaceSelector workspace_scope = 3;
}
// Kubernetes-style metadata shared by all top-level OpenShell domain objects.
//
// This structure provides consistent metadata (identity, labels, annotations,
+90 -148
View File
@@ -1214,12 +1214,9 @@ message BeginRootfsTarStagingResponse {
// Get sandbox request.
message GetSandboxRequest {
reserved 2;
reserved "workspace";
// Sandbox name (canonical lookup key).
string name = 1;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3;
reserved 1, 2, 3;
reserved "name", "workspace", "workspace_scope";
openshell.datamodel.v1.SandboxReference sandbox_ref = 4;
}
// List sandboxes request.
@@ -1236,20 +1233,15 @@ message ListSandboxesRequest {
// List providers attached to a sandbox request.
message ListSandboxProvidersRequest {
reserved 2;
reserved "workspace";
// Sandbox name (canonical lookup key).
string sandbox_name = 1;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3;
reserved 1, 2, 3;
reserved "sandbox_name", "workspace", "workspace_scope";
openshell.datamodel.v1.SandboxReference sandbox_ref = 4;
}
// Attach provider to sandbox request.
message AttachSandboxProviderRequest {
reserved 4;
reserved "workspace";
// Sandbox name (canonical lookup key).
string sandbox_name = 1;
reserved 1, 4, 5;
reserved "sandbox_name", "workspace", "workspace_scope";
// Provider name to attach.
string provider_name = 2;
// Expected resource version for optimistic concurrency control.
@@ -1257,16 +1249,13 @@ message AttachSandboxProviderRequest {
// If non-zero, the server validates that the sandbox's current resource_version
// matches this value before applying the mutation, returning ABORTED on mismatch.
uint64 expected_resource_version = 3;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5;
openshell.datamodel.v1.SandboxReference sandbox_ref = 6;
}
// Detach provider from sandbox request.
message DetachSandboxProviderRequest {
reserved 4;
reserved "workspace";
// Sandbox name (canonical lookup key).
string sandbox_name = 1;
reserved 1, 4, 5;
reserved "sandbox_name", "workspace", "workspace_scope";
// Provider name to detach.
string provider_name = 2;
// Expected resource version for optimistic concurrency control.
@@ -1274,38 +1263,28 @@ message DetachSandboxProviderRequest {
// If non-zero, the server validates that the sandbox's current resource_version
// matches this value before applying the mutation, returning ABORTED on mismatch.
uint64 expected_resource_version = 3;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5;
openshell.datamodel.v1.SandboxReference sandbox_ref = 6;
}
// Delete sandbox request.
message DeleteSandboxRequest {
reserved 2;
reserved "workspace";
// Sandbox name (canonical lookup key).
string name = 1;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3;
reserved 1, 2, 3;
reserved "name", "workspace", "workspace_scope";
openshell.datamodel.v1.SandboxReference sandbox_ref = 4;
}
// Stop sandbox request.
message StopSandboxRequest {
reserved 2;
reserved "workspace";
// Sandbox name (canonical lookup key).
string name = 1;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3;
reserved 1, 2, 3;
reserved "name", "workspace", "workspace_scope";
openshell.datamodel.v1.SandboxReference sandbox_ref = 4;
}
// Start sandbox request.
message StartSandboxRequest {
reserved 2;
reserved "workspace";
// Sandbox name (canonical lookup key).
string name = 1;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3;
reserved 1, 2, 3;
reserved "name", "workspace", "workspace_scope";
openshell.datamodel.v1.SandboxReference sandbox_ref = 4;
}
// Sandbox response.
@@ -1344,8 +1323,9 @@ message DeleteSandboxResponse {
// Create SSH session request.
message CreateSshSessionRequest {
// Sandbox id.
string sandbox_id = 1;
reserved 1;
reserved "sandbox_id";
openshell.datamodel.v1.SandboxReference sandbox_ref = 2;
}
// Create SSH session response.
@@ -1384,44 +1364,40 @@ message CreateSshSessionResponse {
// Request to expose an HTTP service running inside a sandbox.
message ExposeServiceRequest {
reserved 5;
reserved "workspace";
// Sandbox name.
string sandbox = 1;
reserved 1, 5, 6;
reserved "sandbox", "workspace", "workspace_scope";
// Service name within the sandbox.
string service = 2;
// Loopback TCP port inside the sandbox.
uint32 target_port = 3;
// Whether to print/use the browser-facing service URL.
bool domain = 4;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6;
openshell.datamodel.v1.SandboxReference sandbox_ref = 7;
}
// Request to fetch an exposed sandbox service endpoint.
message GetServiceRequest {
reserved 3;
reserved "workspace";
// Sandbox name.
string sandbox = 1;
reserved 1, 3, 4;
reserved "sandbox", "workspace", "workspace_scope";
// Service name within the sandbox. Empty selects the unnamed endpoint.
string service = 2;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4;
openshell.datamodel.v1.SandboxReference sandbox_ref = 5;
}
// 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;
reserved 1;
reserved "sandbox";
// Page size. Zero uses the server default.
uint32 limit = 2;
// Page offset.
uint32 offset = 3;
// Explicit named or all-workspaces scope.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6;
// Optional sandbox filter. When present, workspace_scope must be omitted.
openshell.datamodel.v1.SandboxReference sandbox_ref = 7;
}
// Response containing exposed sandbox service endpoints.
@@ -1431,14 +1407,11 @@ message ListServicesResponse {
// Request to delete an exposed sandbox service endpoint.
message DeleteServiceRequest {
reserved 3;
reserved "workspace";
// Sandbox name.
string sandbox = 1;
reserved 1, 3, 4;
reserved "sandbox", "workspace", "workspace_scope";
// Service name within the sandbox. Empty selects the unnamed endpoint.
string service = 2;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4;
openshell.datamodel.v1.SandboxReference sandbox_ref = 5;
}
// Response for deleting an exposed sandbox service endpoint.
@@ -1483,8 +1456,8 @@ message RevokeSshSessionResponse {
// Execute command request.
message ExecSandboxRequest {
// Sandbox id.
string sandbox_id = 1;
reserved 1;
reserved "sandbox_id";
// Command and arguments.
repeated string command = 2;
@@ -1516,6 +1489,7 @@ message ExecSandboxRequest {
// sourced by them) are applied. When true, the command runs without those
// files (`bash -c`), for automation that needs predictable startup behavior.
bool no_login_shell = 10;
openshell.datamodel.v1.SandboxReference sandbox_ref = 11;
}
// One stdout chunk from a sandbox exec.
@@ -1544,8 +1518,9 @@ message ExecSandboxEvent {
// Initial frame for one TCP forward stream.
message TcpForwardInit {
// Sandbox id.
string sandbox_id = 1;
reserved 1;
reserved "sandbox_id";
openshell.datamodel.v1.SandboxReference sandbox_ref = 2;
// Optional service identifier for audit/correlation.
string service_id = 4;
// Target the gateway should request from the supervisor.
@@ -1606,8 +1581,8 @@ message SshSession {
// Watch sandbox request.
message WatchSandboxRequest {
// Sandbox id.
string id = 1;
reserved 1;
reserved "id";
// Stream sandbox status snapshots.
bool follow_status = 2;
@@ -1637,6 +1612,7 @@ message WatchSandboxRequest {
// Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels.
string log_min_level = 10;
openshell.datamodel.v1.SandboxReference sandbox_ref = 11;
}
// One event in a sandbox watch stream.
@@ -2241,11 +2217,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;
reserved 1, 10, 11;
reserved "name", "workspace", "workspace_scope";
// The new policy to apply.
//
// Sandbox scope (`global=false`):
@@ -2279,9 +2252,8 @@ message UpdateConfigRequest {
// sandbox metadata as a convenience projection. For setting-only updates, it
// only merges them into sandbox metadata.
map<string, string> annotations = 9;
// 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;
// Required for sandbox-scoped updates and omitted for global updates.
openshell.datamodel.v1.SandboxReference sandbox_ref = 12;
}
message PolicyMergeOperation {
@@ -2343,17 +2315,14 @@ message UpdateConfigResponse {
// Get sandbox policy status request.
message GetSandboxPolicyStatusRequest {
reserved 4;
reserved "workspace";
// Sandbox name (canonical lookup key). Ignored when global is true.
string name = 1;
reserved 1, 4, 5;
reserved "name", "workspace", "workspace_scope";
// The specific policy version to query. 0 means latest.
uint32 version = 2;
// Query global policy revisions instead of a sandbox-scoped one.
bool global = 3;
// Explicit workspace scope for sandbox-scoped queries. Omit only when
// `global` is true; the all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5;
// Required for sandbox-scoped queries and omitted for global queries.
openshell.datamodel.v1.SandboxReference sandbox_ref = 6;
}
// Get sandbox policy status response.
@@ -2366,17 +2335,14 @@ message GetSandboxPolicyStatusResponse {
// List sandbox policies request.
message ListSandboxPoliciesRequest {
reserved 5;
reserved "workspace";
// Sandbox name (canonical lookup key). Ignored when global is true.
string name = 1;
reserved 1, 5, 6;
reserved "name", "workspace", "workspace_scope";
uint32 limit = 2;
uint32 offset = 3;
// List global policy revisions instead of sandbox-scoped ones.
bool global = 4;
// Explicit workspace scope for sandbox-scoped queries. Omit only when
// `global` is true; the all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6;
// Required for sandbox-scoped queries and omitted for global queries.
openshell.datamodel.v1.SandboxReference sandbox_ref = 7;
}
// List sandbox policies response.
@@ -2447,10 +2413,8 @@ enum PolicyStatus {
// Get sandbox logs request (one-shot fetch).
message GetSandboxLogsRequest {
reserved 6;
reserved "workspace";
// Sandbox id.
string sandbox_id = 1;
reserved 1, 6, 7;
reserved "sandbox_id", "workspace", "workspace_scope";
// Maximum number of log lines to return. 0 means use default (2000).
uint32 lines = 2;
// Only include logs with timestamp >= this value (ms since epoch). 0 means no filter.
@@ -2459,8 +2423,7 @@ message GetSandboxLogsRequest {
repeated string sources = 4;
// Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels.
string min_level = 5;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 7;
openshell.datamodel.v1.SandboxReference sandbox_ref = 8;
}
// Batch of log lines pushed from sandbox to server.
@@ -2790,11 +2753,14 @@ message SubmitPolicyAnalysisRequest {
// to watch. Other values are treated as agent-style (no dedup) so a new
// mode does not silently collapse proposals.
string analysis_mode = 3;
// Sandbox name.
// Internal sandbox callback name. This request is not part of the public
// SandboxReference contract; the authenticated sandbox principal is the
// authoritative identity.
string name = 4;
// Anonymous network activity counters.
repeated NetworkActivitySummary network_activity_summaries = 5;
// Workspace scope. Empty defaults to "default".
// Internal callback workspace. The gateway validates it against the
// authenticated sandbox principal.
string workspace = 6;
}
@@ -2813,14 +2779,11 @@ message SubmitPolicyAnalysisResponse {
// Get draft policy for a sandbox.
message GetDraftPolicyRequest {
reserved 3;
reserved "workspace";
// Sandbox name.
string name = 1;
reserved 1, 3, 4;
reserved "name", "workspace", "workspace_scope";
// Optional status filter: "pending", "approved", "rejected", or "" for all.
string status_filter = 2;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4;
openshell.datamodel.v1.SandboxReference sandbox_ref = 5;
}
message GetDraftPolicyResponse {
@@ -2836,17 +2799,14 @@ message GetDraftPolicyResponse {
// Approve a single draft chunk.
message ApproveDraftChunkRequest {
reserved 3;
reserved "workspace";
// Sandbox name.
string name = 1;
reserved 1, 3, 5;
reserved "name", "workspace", "workspace_scope";
// Chunk ID to approve.
string chunk_id = 2;
// Token returned with the reviewed PolicyChunk. Approval fails with
// FAILED_PRECONDITION if live decision inputs no longer match it.
string review_token = 4;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5;
openshell.datamodel.v1.SandboxReference sandbox_ref = 6;
}
message ApproveDraftChunkResponse {
@@ -2858,16 +2818,13 @@ message ApproveDraftChunkResponse {
// Reject a single draft chunk.
message RejectDraftChunkRequest {
reserved 4;
reserved "workspace";
// Sandbox name.
string name = 1;
reserved 1, 4, 5;
reserved "name", "workspace", "workspace_scope";
// Chunk ID to reject.
string chunk_id = 2;
// Optional reason for rejection (fed to LLM context in future analysis).
string reason = 3;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5;
openshell.datamodel.v1.SandboxReference sandbox_ref = 6;
}
message RejectDraftChunkResponse {}
@@ -2879,17 +2836,14 @@ message DraftChunkApproval {
}
message ApproveAllDraftChunksRequest {
reserved 3;
reserved "workspace";
// Sandbox name.
string name = 1;
reserved 1, 3, 5;
reserved "name", "workspace", "workspace_scope";
// Include chunks with security_notes (default false: skips them).
bool include_security_flagged = 2;
// Exact reviewed chunks and tokens. The server validates them against one
// live snapshot, stages compatible operations in order, and writes once.
repeated DraftChunkApproval approvals = 4;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5;
openshell.datamodel.v1.SandboxReference sandbox_ref = 6;
}
message ApproveAllDraftChunksResponse {
@@ -2906,30 +2860,24 @@ message ApproveAllDraftChunksResponse {
// Edit a pending chunk in-place.
message EditDraftChunkRequest {
reserved 4;
reserved "workspace";
// Sandbox name.
string name = 1;
reserved 1, 4, 5;
reserved "name", "workspace", "workspace_scope";
// Chunk ID to edit.
string chunk_id = 2;
// The modified rule (replaces existing proposed_rule).
openshell.sandbox.v1.NetworkPolicyRule proposed_rule = 3;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5;
openshell.datamodel.v1.SandboxReference sandbox_ref = 6;
}
message EditDraftChunkResponse {}
// Reverse an approval (remove merged rule from active policy).
message UndoDraftChunkRequest {
reserved 3;
reserved "workspace";
// Sandbox name.
string name = 1;
reserved 1, 3, 4;
reserved "name", "workspace", "workspace_scope";
// Chunk ID to undo.
string chunk_id = 2;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4;
openshell.datamodel.v1.SandboxReference sandbox_ref = 5;
}
message UndoDraftChunkResponse {
@@ -2941,12 +2889,9 @@ message UndoDraftChunkResponse {
// Clear all pending draft chunks for a sandbox.
message ClearDraftChunksRequest {
reserved 2;
reserved "workspace";
// Sandbox name.
string name = 1;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3;
reserved 1, 2, 3;
reserved "name", "workspace", "workspace_scope";
openshell.datamodel.v1.SandboxReference sandbox_ref = 4;
}
message ClearDraftChunksResponse {
@@ -2956,12 +2901,9 @@ message ClearDraftChunksResponse {
// Get decision history for a sandbox's draft policy.
message GetDraftHistoryRequest {
reserved 2;
reserved "workspace";
// Sandbox name.
string name = 1;
// Explicit workspace scope. The all-workspaces selection is invalid.
openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3;
reserved 1, 2, 3;
reserved "name", "workspace", "workspace_scope";
openshell.datamodel.v1.SandboxReference sandbox_ref = 4;
}
message DraftHistoryEntry {
+6 -2
View File
@@ -5,6 +5,7 @@ syntax = "proto3";
package openshell.sandbox.v1;
import "datamodel.proto";
import "google/protobuf/struct.proto";
// Sandbox-supervisor configuration and policy messages.
@@ -317,8 +318,11 @@ message NetworkBinary {
// Request to get sandbox settings by sandbox ID.
message GetSandboxConfigRequest {
// The sandbox ID.
string sandbox_id = 1;
reserved 1;
reserved "sandbox_id";
// Canonical public sandbox reference. Sandbox-authenticated callers use the
// ID variant; user clients may use either supported variant.
openshell.datamodel.v1.SandboxReference sandbox_ref = 2;
}
// Request to get gateway-global settings.
+17 -5
View File
@@ -47,6 +47,18 @@ def _all_workspaces_scope() -> datamodel_pb2.WorkspaceSelector:
return datamodel_pb2.WorkspaceSelector(all_workspaces=datamodel_pb2.AllWorkspaces())
def _sandbox_reference_by_name(
workspace: str, name: str
) -> datamodel_pb2.SandboxReference:
return datamodel_pb2.SandboxReference(
name=name, workspace_scope=_workspace_scope(workspace)
)
def _sandbox_reference_by_id(sandbox_id: str) -> datamodel_pb2.SandboxReference:
return datamodel_pb2.SandboxReference(id=sandbox_id)
class _ClientCallDetails(_ClientCallDetailsBase, grpc.ClientCallDetails):
pass
@@ -806,7 +818,7 @@ class SandboxClient:
def get(self, sandbox_name: str, *, workspace: str) -> SandboxRef:
response = self._stub.GetSandbox(
openshell_pb2.GetSandboxRequest(
name=sandbox_name, workspace_scope=_workspace_scope(workspace)
sandbox_ref=_sandbox_reference_by_name(workspace, sandbox_name)
),
timeout=self._timeout,
)
@@ -885,7 +897,7 @@ class SandboxClient:
def delete(self, sandbox_name: str, *, workspace: str) -> bool:
response = self._stub.DeleteSandbox(
openshell_pb2.DeleteSandboxRequest(
name=sandbox_name, workspace_scope=_workspace_scope(workspace)
sandbox_ref=_sandbox_reference_by_name(workspace, sandbox_name)
),
timeout=self._timeout,
)
@@ -894,7 +906,7 @@ class SandboxClient:
def stop(self, sandbox_name: str, *, workspace: str) -> SandboxRef:
response = self._stub.StopSandbox(
openshell_pb2.StopSandboxRequest(
name=sandbox_name, workspace_scope=_workspace_scope(workspace)
sandbox_ref=_sandbox_reference_by_name(workspace, sandbox_name)
),
timeout=self._timeout,
)
@@ -903,7 +915,7 @@ class SandboxClient:
def start(self, sandbox_name: str, *, workspace: str) -> SandboxRef:
response = self._stub.StartSandbox(
openshell_pb2.StartSandboxRequest(
name=sandbox_name, workspace_scope=_workspace_scope(workspace)
sandbox_ref=_sandbox_reference_by_name(workspace, sandbox_name)
),
timeout=self._timeout,
)
@@ -996,7 +1008,7 @@ class SandboxClient:
raise SandboxError("command must not be empty")
request = openshell_pb2.ExecSandboxRequest(
sandbox_id=sandbox_id,
sandbox_ref=_sandbox_reference_by_id(sandbox_id),
command=list(command),
workdir=workdir or "",
environment=dict(env or {}),
+18 -7
View File
@@ -43,7 +43,12 @@ from openshell.sandbox import (
def _request_workspace(request: Any) -> str | None:
scope = request.workspace_scope
sandbox_ref = getattr(request, "sandbox_ref", None)
scope = (
sandbox_ref.workspace_scope
if sandbox_ref is not None and sandbox_ref.HasField("workspace_scope")
else request.workspace_scope
)
if scope.WhichOneof("selection") == "workspace":
return cast("str", scope.workspace)
return None
@@ -53,6 +58,12 @@ def _request_selects_all_workspaces(request: Any) -> bool:
return request.workspace_scope.WhichOneof("selection") == "all_workspaces"
def _request_sandbox_name(request: Any) -> str:
sandbox_ref = request.sandbox_ref
assert sandbox_ref.WhichOneof("identifier") == "name"
return cast("str", sandbox_ref.name)
def _client_credentials_fixture() -> dict[str, Any]:
return json.loads(
(
@@ -1993,7 +2004,7 @@ class _FakeSandboxStub:
return SimpleNamespace(
sandbox=_make_sandbox_proto(
"sandbox-1",
request.name,
_request_sandbox_name(request),
workspace=_request_workspace(request) or "default",
)
)
@@ -2017,7 +2028,7 @@ class _FakeSandboxStub:
return SimpleNamespace(
sandbox=_make_sandbox_proto(
"sandbox-1",
request.name,
_request_sandbox_name(request),
phase=openshell_pb2.SANDBOX_PHASE_STOPPED,
workspace=_request_workspace(request) or "default",
)
@@ -2033,7 +2044,7 @@ class _FakeSandboxStub:
return SimpleNamespace(
sandbox=_make_sandbox_proto(
"sandbox-1",
request.name,
_request_sandbox_name(request),
phase=openshell_pb2.SANDBOX_PHASE_STARTING,
workspace=_request_workspace(request) or "default",
)
@@ -2429,13 +2440,13 @@ def test_stop_and_start_forward_workspace_and_return_phase() -> None:
stopped = client.stop("job-1", workspace="team-a")
assert stub.stop_request is not None
assert stub.stop_request.name == "job-1"
assert _request_sandbox_name(stub.stop_request) == "job-1"
assert _request_workspace(stub.stop_request) == "team-a"
assert stopped.phase == openshell_pb2.SANDBOX_PHASE_STOPPED
starting = client.start("job-1", workspace="team-a")
assert stub.start_request is not None
assert stub.start_request.name == "job-1"
assert _request_sandbox_name(stub.start_request) == "job-1"
assert _request_workspace(stub.start_request) == "team-a"
assert starting.phase == openshell_pb2.SANDBOX_PHASE_STARTING
@@ -2460,7 +2471,7 @@ def test_wait_ready_handles_terminal_main_process_results(
return SimpleNamespace(
sandbox=_make_sandbox_proto(
"sandbox-1",
request.name,
_request_sandbox_name(request),
phase=phase,
workspace=_request_workspace(request) or "default",
)
+2 -2
View File
@@ -31,7 +31,7 @@ func (c *configClient) GetSandbox(ctx context.Context, workspace, sandboxName st
}
resp, err := c.client.GetSandboxConfig(ctx, &sbv1.GetSandboxConfigRequest{
SandboxId: sb.ID,
SandboxRef: sandboxReferenceByID(sb.ID),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -59,7 +59,7 @@ func (c *configClient) Update(ctx context.Context, workspace string, update *Con
return nil, &StatusError{Code: ErrorInvalidArgument, Message: convErr.Error()}
}
if !req.GetGlobal() {
req.WorkspaceScope = namedWorkspaceScope(workspace)
req.SandboxRef.WorkspaceScope = namedWorkspaceScope(workspace)
}
resp, err := c.client.UpdateConfig(ctx, req)
if err != nil {
+4 -4
View File
@@ -143,7 +143,7 @@ func TestConfigGetSandbox(t *testing.T) {
// Verify request was forwarded with resolved ID (stubSandboxResolver returns "sb-<name>").
mock.mu.Lock()
assert.Equal(t, "sb-my-sandbox", mock.lastSandboxReq.GetSandboxId())
assert.Equal(t, "sb-my-sandbox", mock.lastSandboxReq.GetSandboxRef().GetId())
mock.mu.Unlock()
// Scalar fields.
@@ -235,7 +235,7 @@ func TestConfigGetSandbox_ResolvesNameToID(t *testing.T) {
// stubSandboxResolver returns ID "sb-<name>" — verify the proto has the resolved ID, not the name.
mock.mu.Lock()
assert.Equal(t, "sb-my-sandbox", mock.lastSandboxReq.GetSandboxId(), "GetSandbox should send resolved sandbox ID, not the name")
assert.Equal(t, "sb-my-sandbox", mock.lastSandboxReq.GetSandboxRef().GetId(), "GetSandbox should send resolved sandbox ID, not the name")
mock.mu.Unlock()
}
@@ -361,7 +361,7 @@ func TestConfigUpdate_SandboxScope(t *testing.T) {
mock.mu.Unlock()
require.NotNil(t, req)
assert.Equal(t, "my-sandbox", req.GetName())
assert.Equal(t, "my-sandbox", req.GetSandboxRef().GetName())
assert.Equal(t, "max_tokens", req.GetSettingKey())
assert.False(t, req.GetGlobal())
assert.Equal(t, uint64(4), req.GetExpectedResourceVersion())
@@ -397,7 +397,7 @@ func TestConfigUpdate_GlobalScope(t *testing.T) {
mock.mu.Unlock()
assert.True(t, req.GetGlobal())
assert.Empty(t, req.GetName())
assert.Nil(t, req.GetSandboxRef())
}
func TestConfigUpdate_DeleteSetting(t *testing.T) {
+2 -2
View File
@@ -314,7 +314,7 @@
//
// Read a policy back from revision history:
//
// revisions, err := client.Policy().List(ctx, "default")
// revisions, err := client.Policy().List(ctx, "default", "my-sandbox")
// if err != nil {
// log.Fatal(err)
// }
@@ -328,7 +328,7 @@
//
// List gateway-global policy revisions (no sandbox name or workspace needed):
//
// revisions, err := client.Policy().List(ctx, "", v1.WithListGlobal(true))
// revisions, err := client.Policy().List(ctx, "", "", v1.WithListGlobal(true))
// if err != nil {
// log.Fatal(err)
// }
+5 -5
View File
@@ -236,7 +236,7 @@ func TestExecRun_WithOptions(t *testing.T) {
mock.mu.Lock()
defer mock.mu.Unlock()
assert.Equal(t, "sb-test-sandbox", mock.lastExecRequest.GetSandboxId())
assert.Equal(t, "sb-test-sandbox", mock.lastExecRequest.GetSandboxRef().GetId())
assert.Equal(t, []string{"ls"}, mock.lastExecRequest.GetCommand())
assert.Equal(t, "/tmp", mock.lastExecRequest.GetWorkdir())
assert.Equal(t, map[string]string{"FOO": "bar"}, mock.lastExecRequest.GetEnvironment())
@@ -380,7 +380,7 @@ func TestExecInteractive(t *testing.T) {
startReq := startInput.GetStart()
require.NotNil(t, startReq)
assert.Equal(t, "sb-test-sandbox", startReq.GetSandboxId())
assert.Equal(t, "sb-test-sandbox", startReq.GetSandboxRef().GetId())
assert.Equal(t, []string{"/bin/bash"}, startReq.GetCommand())
assert.True(t, startReq.GetTty())
assert.Equal(t, uint32(80), startReq.GetCols())
@@ -546,7 +546,7 @@ func TestExecRun_ResolvesNameToID(t *testing.T) {
mock.mu.Lock()
defer mock.mu.Unlock()
// Verify the proto request contains the resolved ID, not the name
assert.Equal(t, "sb-my-sandbox", mock.lastExecRequest.GetSandboxId())
assert.Equal(t, "sb-my-sandbox", mock.lastExecRequest.GetSandboxRef().GetId())
}
func TestExecRun_ResolutionError(t *testing.T) {
@@ -576,7 +576,7 @@ func TestExecStream_ResolvesNameToID(t *testing.T) {
mock.mu.Lock()
defer mock.mu.Unlock()
assert.Equal(t, "sb-my-sandbox", mock.lastExecRequest.GetSandboxId())
assert.Equal(t, "sb-my-sandbox", mock.lastExecRequest.GetSandboxRef().GetId())
}
func TestExecStream_ResolutionError(t *testing.T) {
@@ -609,7 +609,7 @@ func TestExecInteractive_ResolvesNameToID(t *testing.T) {
require.NotEmpty(t, mock.receivedInputs)
startReq := mock.receivedInputs[0].GetStart()
require.NotNil(t, startReq)
assert.Equal(t, "sb-my-sandbox", startReq.GetSandboxId())
assert.Equal(t, "sb-my-sandbox", startReq.GetSandboxRef().GetId())
}
func TestExecInteractive_ResolutionError(t *testing.T) {
+3 -11
View File
@@ -7,7 +7,6 @@ import (
"context"
"maps"
"slices"
"strings"
"sync"
v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1"
@@ -172,9 +171,8 @@ func (c *fakePolicyClient) GetStatus(_ context.Context, workspace, sandboxName s
}
// List returns policy revisions. When the global option is set, it returns
// global revisions; otherwise it returns all sandbox-scoped revisions for the
// given workspace.
func (c *fakePolicyClient) List(_ context.Context, workspace string, opts ...v1.ListPolicyOption) ([]types.SandboxPolicyRevision, error) {
// global revisions; otherwise it returns revisions for the specified sandbox.
func (c *fakePolicyClient) List(_ context.Context, workspace, sandboxName string, opts ...v1.ListPolicyOption) ([]types.SandboxPolicyRevision, error) {
if c.closedFunc() {
return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"}
}
@@ -187,13 +185,7 @@ func (c *fakePolicyClient) List(_ context.Context, workspace string, opts ...v1.
if cfg.Global() {
revisions = slices.Clone(c.globalRevisions)
} else {
// Collect all revisions for sandboxes in this workspace.
prefix := workspace + "/"
for key, revs := range c.sandboxRevisions {
if strings.HasPrefix(key, prefix) {
revisions = append(revisions, revs...)
}
}
revisions = slices.Clone(c.sandboxRevisions[workspace+"/"+sandboxName])
}
if len(revisions) == 0 {
+9 -9
View File
@@ -66,7 +66,7 @@ func TestFakePolicy_GetStatus_EmptyReturnsNotFound(t *testing.T) {
func TestFakePolicy_List_EmptyReturnsNil(t *testing.T) {
c := newFakePolicyClient(func() bool { return false })
revisions, err := c.List(context.Background(), "default")
revisions, err := c.List(context.Background(), "default", "sb-1")
require.NoError(t, err)
assert.Nil(t, revisions)
}
@@ -96,7 +96,7 @@ func TestFakePolicy_List_Global(t *testing.T) {
c.AddRevision("default", "sb-1", types.SandboxPolicyRevision{Version: 1, PolicyHash: "sha256:sb-v1", Status: types.PolicyLoadStatusLoaded})
// List global revisions.
revisions, err := c.List(context.Background(), "", types.WithListGlobal(true))
revisions, err := c.List(context.Background(), "", "", types.WithListGlobal(true))
require.NoError(t, err)
require.Len(t, revisions, 2)
assert.Equal(t, uint32(1), revisions[0].Version)
@@ -113,7 +113,7 @@ func TestFakePolicy_List_Sandbox(t *testing.T) {
c.AddRevision("default", "sb-1", types.SandboxPolicyRevision{Version: 2, PolicyHash: "sha256:sb-v2", Status: types.PolicyLoadStatusPending})
// List sandbox-scoped revisions (no global flag).
revisions, err := c.List(context.Background(), "default")
revisions, err := c.List(context.Background(), "default", "sb-1")
require.NoError(t, err)
require.Len(t, revisions, 2)
assert.Equal(t, "sha256:sb-v1", revisions[0].PolicyHash)
@@ -127,7 +127,7 @@ func TestFakePolicy_List_NoIsolationCrossContamination(t *testing.T) {
c.AddRevision("default", "sb-1", types.SandboxPolicyRevision{Version: 1, PolicyHash: "sha256:sb-v1"})
// Global list returns empty (no global revisions seeded).
revisions, err := c.List(context.Background(), "", types.WithListGlobal(true))
revisions, err := c.List(context.Background(), "", "", types.WithListGlobal(true))
require.NoError(t, err)
assert.Nil(t, revisions)
}
@@ -140,14 +140,14 @@ func TestFakePolicy_List_GlobalWithPagination(t *testing.T) {
c.AddGlobalRevision(types.SandboxPolicyRevision{Version: 3})
// Limit to 2.
revisions, err := c.List(context.Background(), "", types.WithListGlobal(true), types.WithLimit(2))
revisions, err := c.List(context.Background(), "", "", types.WithListGlobal(true), types.WithLimit(2))
require.NoError(t, err)
require.Len(t, revisions, 2)
assert.Equal(t, uint32(1), revisions[0].Version)
assert.Equal(t, uint32(2), revisions[1].Version)
// Offset by 1, limit 2.
revisions, err = c.List(context.Background(), "", types.WithListGlobal(true), types.WithLimit(2), types.WithOffset(1))
revisions, err = c.List(context.Background(), "", "", types.WithListGlobal(true), types.WithLimit(2), types.WithOffset(1))
require.NoError(t, err)
require.Len(t, revisions, 2)
assert.Equal(t, uint32(2), revisions[0].Version)
@@ -236,14 +236,14 @@ func TestFakePolicy_DeepCopyWithPolicy(t *testing.T) {
ctx := context.Background()
// Get global revision and mutate it.
revisions, err := fc.Policy().List(ctx, "", types.WithListGlobal(true))
revisions, err := fc.Policy().List(ctx, "", "", types.WithListGlobal(true))
require.NoError(t, err)
require.Len(t, revisions, 1)
require.NotNil(t, revisions[0].Policy)
revisions[0].Policy.NetworkPolicies["rule-1"] = types.NetworkPolicyRule{Name: "mutated"}
// Verify internal state is not corrupted.
revisions2, err := fc.Policy().List(ctx, "", types.WithListGlobal(true))
revisions2, err := fc.Policy().List(ctx, "", "", types.WithListGlobal(true))
require.NoError(t, err)
assert.Equal(t, "rule-1", revisions2[0].Policy.NetworkPolicies["rule-1"].Name)
@@ -256,7 +256,7 @@ func TestFakePolicy_DeepCopyWithPolicy(t *testing.T) {
func TestFakePolicy_List_ClosedReturnsUnavailable(t *testing.T) {
c := newFakePolicyClient(func() bool { return true })
_, err := c.List(context.Background(), "", types.WithListGlobal(true))
_, err := c.List(context.Background(), "", "", types.WithListGlobal(true))
require.Error(t, err)
assert.True(t, types.IsUnavailable(err))
}
+2 -2
View File
@@ -59,7 +59,7 @@ func (f *fileClient) Upload(ctx context.Context, workspace, sandboxName string,
}
session, err := f.client.CreateSshSession(ctx, &pb.CreateSshSessionRequest{
SandboxId: sb.ID,
SandboxRef: sandboxReferenceByID(sb.ID),
})
if err != nil {
return converter.FromGRPCError(err)
@@ -93,7 +93,7 @@ func (f *fileClient) Download(ctx context.Context, workspace, sandboxName string
}
session, err := f.client.CreateSshSession(ctx, &pb.CreateSshSessionRequest{
SandboxId: sb.ID,
SandboxRef: sandboxReferenceByID(sb.ID),
})
if err != nil {
return converter.FromGRPCError(err)
+4 -4
View File
@@ -109,7 +109,7 @@ func TestFileUpload(t *testing.T) {
err := client.Upload(context.Background(), "default", "test-sandbox", localPath, "/remote/upload.txt")
require.NoError(t, err)
assert.Equal(t, "sb-test-sandbox", mock.lastCreateReq.GetSandboxId())
assert.Equal(t, "sb-test-sandbox", mock.lastCreateReq.GetSandboxRef().GetId())
assert.Equal(t, 1, mock.createCallCount)
}
@@ -164,7 +164,7 @@ func TestFileDownload(t *testing.T) {
err := client.Download(context.Background(), "default", "test-sandbox", "/remote/file.txt", localPath)
require.NoError(t, err)
assert.Equal(t, "sb-test-sandbox", mock.lastCreateReq.GetSandboxId())
assert.Equal(t, "sb-test-sandbox", mock.lastCreateReq.GetSandboxRef().GetId())
assert.Equal(t, 1, mock.createCallCount)
}
@@ -261,7 +261,7 @@ func TestFileUpload_ResolvesNameToID(t *testing.T) {
_ = client.Upload(context.Background(), "default", "my-sandbox", localPath, "/remote/file.txt")
// Verify the proto request contains the resolved ID, not the name
assert.Equal(t, "sb-my-sandbox", mock.lastCreateReq.GetSandboxId())
assert.Equal(t, "sb-my-sandbox", mock.lastCreateReq.GetSandboxRef().GetId())
}
func TestFileUpload_ResolutionError(t *testing.T) {
@@ -307,7 +307,7 @@ func TestFileDownload_ResolvesNameToID(t *testing.T) {
localPath := filepath.Join(t.TempDir(), "downloaded.txt")
_ = client.Download(context.Background(), "default", "my-sandbox", "/remote/file.txt", localPath)
assert.Equal(t, "sb-my-sandbox", mock.lastCreateReq.GetSandboxId())
assert.Equal(t, "sb-my-sandbox", mock.lastCreateReq.GetSandboxRef().GetId())
}
func TestFileDownload_ResolutionError(t *testing.T) {
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1"
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
)
@@ -39,8 +40,10 @@ func ExecChunkFromEvent(event *pb.ExecSandboxEvent) (*types.ExecChunk, int, erro
// ExecRequestToProto builds a proto ExecSandboxRequest for Run/Stream modes.
func ExecRequestToProto(sandboxID string, command []string, opts *types.ExecOptions) *pb.ExecSandboxRequest {
req := &pb.ExecSandboxRequest{
SandboxId: sandboxID,
Command: CopyStringSlice(command),
SandboxRef: &dm.SandboxReference{
Identifier: &dm.SandboxReference_Id{Id: sandboxID},
},
Command: CopyStringSlice(command),
}
if opts != nil {
req.Workdir = opts.WorkDir
@@ -117,7 +117,7 @@ func TestExecRequestToProto(t *testing.T) {
})
require.NotNil(t, req)
assert.Equal(t, "sb-1", req.SandboxId)
assert.Equal(t, "sb-1", req.GetSandboxRef().GetId())
assert.Equal(t, []string{"ls", "-la"}, req.Command)
assert.Equal(t, "/home/user", req.Workdir)
assert.Equal(t, map[string]string{"FOO": "bar"}, req.Environment)
@@ -128,7 +128,7 @@ func TestExecRequestToProto_NilOptions(t *testing.T) {
req := ExecRequestToProto("sb-2", []string{"echo", "hi"}, nil)
require.NotNil(t, req)
assert.Equal(t, "sb-2", req.SandboxId)
assert.Equal(t, "sb-2", req.GetSandboxRef().GetId())
assert.Equal(t, []string{"echo", "hi"}, req.Command)
assert.Empty(t, req.Workdir)
assert.Nil(t, req.Environment)
@@ -141,7 +141,7 @@ func TestExecRequestToProto_Interactive(t *testing.T) {
})
require.NotNil(t, req)
assert.Equal(t, "sb-3", req.SandboxId)
assert.Equal(t, "sb-3", req.GetSandboxRef().GetId())
assert.Equal(t, []string{"/bin/bash"}, req.Command)
assert.Equal(t, "/root", req.Workdir)
assert.Equal(t, map[string]string{"TERM": "xterm"}, req.Environment)
@@ -7,6 +7,7 @@ import (
"fmt"
v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1"
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
sbv1 "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1"
)
@@ -180,7 +181,6 @@ func ConfigUpdateToProto(cu *v1.ConfigUpdate) (*pb.UpdateConfigRequest, error) {
return nil, nil
}
req := &pb.UpdateConfigRequest{
Name: cu.Name,
SettingKey: cu.SettingKey,
SettingValue: SettingValueToProto(cu.SettingValue),
DeleteSetting: cu.DeleteSetting,
@@ -188,6 +188,11 @@ func ConfigUpdateToProto(cu *v1.ConfigUpdate) (*pb.UpdateConfigRequest, error) {
ExpectedResourceVersion: cu.ExpectedResourceVersion,
Annotations: CopyStringMap(cu.Annotations),
}
if !cu.Global {
req.SandboxRef = &dm.SandboxReference{
Identifier: &dm.SandboxReference_Name{Name: cu.Name},
}
}
// Convert typed SDK SandboxPolicy to proto SandboxPolicy.
policy, err := SandboxPolicyToProtoChecked(cu.Policy)
@@ -429,7 +429,7 @@ func TestConfigUpdateToProto(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, req)
assert.Equal(t, "my-sandbox", req.Name)
assert.Equal(t, "my-sandbox", req.GetSandboxRef().GetName())
assert.Equal(t, "timeout", req.SettingKey)
require.NotNil(t, req.SettingValue)
assert.Equal(t, int64(60), req.SettingValue.GetIntValue())
@@ -495,7 +495,7 @@ func TestConfigUpdateToProto_GlobalScope(t *testing.T) {
require.NotNil(t, req)
assert.True(t, req.Global)
assert.Empty(t, req.Name)
assert.Nil(t, req.GetSandboxRef())
}
func TestConfigUpdateToProto_NilSettingValue(t *testing.T) {
@@ -779,7 +779,7 @@ func TestConfigUpdateToProto_WithMergeOperations(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, req)
assert.Equal(t, "my-sandbox", req.GetName())
assert.Equal(t, "my-sandbox", req.GetSandboxRef().GetName())
require.Len(t, req.GetMergeOperations(), 3)
// First: RemoveRule
+1 -1
View File
@@ -111,7 +111,7 @@ type PolicyInterface interface {
ClearDraftChunks(ctx context.Context, workspace, sandboxName string) (*ClearResult, error)
GetDraftHistory(ctx context.Context, workspace, sandboxName string) ([]DraftHistoryEntry, error)
GetStatus(ctx context.Context, workspace, sandboxName string, opts ...GetStatusOption) (*PolicyStatusResult, error)
List(ctx context.Context, workspace string, opts ...ListPolicyOption) ([]SandboxPolicyRevision, error)
List(ctx context.Context, workspace, sandboxName string, opts ...ListPolicyOption) ([]SandboxPolicyRevision, error)
EditDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string, proposedRule *NetworkPolicyRule) error
UndoDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string) (*UndoResult, error)
}
+19 -28
View File
@@ -23,9 +23,8 @@ 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(),
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
StatusFilter: cfg.StatusFilter(),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -35,10 +34,9 @@ func (p *policyClient) GetDraft(ctx context.Context, workspace, sandboxName stri
func (p *policyClient) ApproveDraftChunk(ctx context.Context, workspace, sandboxName, chunkID, reviewToken string) (*ApproveResult, error) {
resp, err := p.client.ApproveDraftChunk(ctx, &pb.ApproveDraftChunkRequest{
Name: sandboxName,
ChunkId: chunkID,
WorkspaceScope: namedWorkspaceScope(workspace),
ReviewToken: reviewToken,
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
ChunkId: chunkID,
ReviewToken: reviewToken,
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -48,10 +46,9 @@ 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,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
ChunkId: chunkID,
Reason: reason,
})
if err != nil {
return converter.FromGRPCError(err)
@@ -69,10 +66,9 @@ func (p *policyClient) ApproveAllDraftChunks(ctx context.Context, workspace, san
})
}
resp, err := p.client.ApproveAllDraftChunks(ctx, &pb.ApproveAllDraftChunksRequest{
Name: sandboxName,
IncludeSecurityFlagged: cfg.IncludeSecurityFlagged(),
WorkspaceScope: namedWorkspaceScope(workspace),
Approvals: approvals,
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -82,8 +78,7 @@ func (p *policyClient) ApproveAllDraftChunks(ctx context.Context, workspace, san
func (p *policyClient) ClearDraftChunks(ctx context.Context, workspace, sandboxName string) (*ClearResult, error) {
resp, err := p.client.ClearDraftChunks(ctx, &pb.ClearDraftChunksRequest{
Name: sandboxName,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -93,8 +88,7 @@ func (p *policyClient) ClearDraftChunks(ctx context.Context, workspace, sandboxN
func (p *policyClient) GetDraftHistory(ctx context.Context, workspace, sandboxName string) ([]DraftHistoryEntry, error) {
resp, err := p.client.GetDraftHistory(ctx, &pb.GetDraftHistoryRequest{
Name: sandboxName,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -115,12 +109,11 @@ func (p *policyClient) GetDraftHistory(ctx context.Context, workspace, sandboxNa
func (p *policyClient) GetStatus(ctx context.Context, workspace, sandboxName string, opts ...GetStatusOption) (*PolicyStatusResult, error) {
cfg := types.ApplyGetStatusOptions(opts)
req := &pb.GetSandboxPolicyStatusRequest{
Name: sandboxName,
Version: cfg.Version(),
Global: cfg.Global(),
}
if !cfg.Global() {
req.WorkspaceScope = namedWorkspaceScope(workspace)
req.SandboxRef = sandboxReferenceByName(workspace, sandboxName)
}
resp, err := p.client.GetSandboxPolicyStatus(ctx, req)
if err != nil {
@@ -129,7 +122,7 @@ func (p *policyClient) GetStatus(ctx context.Context, workspace, sandboxName str
return converter.PolicyStatusResultFromProto(resp), nil
}
func (p *policyClient) List(ctx context.Context, workspace string, opts ...ListPolicyOption) ([]SandboxPolicyRevision, error) {
func (p *policyClient) List(ctx context.Context, workspace, sandboxName string, opts ...ListPolicyOption) ([]SandboxPolicyRevision, error) {
cfg := types.ApplyListPolicyOptions(opts)
req := &pb.ListSandboxPoliciesRequest{
Limit: cfg.Limit(),
@@ -137,7 +130,7 @@ func (p *policyClient) List(ctx context.Context, workspace string, opts ...ListP
Global: cfg.Global(),
}
if !cfg.Global() {
req.WorkspaceScope = namedWorkspaceScope(workspace)
req.SandboxRef = sandboxReferenceByName(workspace, sandboxName)
}
resp, err := p.client.ListSandboxPolicies(ctx, req)
if err != nil {
@@ -158,10 +151,9 @@ 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),
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
ChunkId: chunkID,
ProposedRule: converter.NetworkPolicyRuleToProto(proposedRule),
})
if err != nil {
return converter.FromGRPCError(err)
@@ -171,9 +163,8 @@ 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,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
ChunkId: chunkID,
})
if err != nil {
return nil, converter.FromGRPCError(err)
+27 -27
View File
@@ -235,7 +235,7 @@ func TestPolicyGetDraft(t *testing.T) {
// Verify request was forwarded.
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastGetDraftReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastGetDraftReq.GetSandboxRef().GetName())
assert.Empty(t, mock.lastGetDraftReq.GetStatusFilter())
mock.mu.Unlock()
@@ -321,7 +321,7 @@ func TestPolicyApproveDraftChunk(t *testing.T) {
// Verify request was forwarded.
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastApproveReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastApproveReq.GetSandboxRef().GetName())
assert.Equal(t, "chunk-1", mock.lastApproveReq.GetChunkId())
assert.Equal(t, "token-1", mock.lastApproveReq.GetReviewToken())
mock.mu.Unlock()
@@ -358,7 +358,7 @@ func TestPolicyRejectDraftChunk(t *testing.T) {
// Verify request was forwarded.
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastRejectReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastRejectReq.GetSandboxRef().GetName())
assert.Equal(t, "chunk-2", mock.lastRejectReq.GetChunkId())
assert.Equal(t, "too broad", mock.lastRejectReq.GetReason())
mock.mu.Unlock()
@@ -400,7 +400,7 @@ func TestPolicyApproveAllDraftChunks(t *testing.T) {
// Verify default: security-flagged NOT included.
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastApproveAllReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastApproveAllReq.GetSandboxRef().GetName())
assert.False(t, mock.lastApproveAllReq.GetIncludeSecurityFlagged())
mock.mu.Unlock()
@@ -466,7 +466,7 @@ func TestPolicyClearDraftChunks(t *testing.T) {
// Verify request was forwarded.
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastClearReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastClearReq.GetSandboxRef().GetName())
mock.mu.Unlock()
assert.Equal(t, uint32(4), result.ChunksCleared)
@@ -517,7 +517,7 @@ func TestPolicyGetDraftHistory(t *testing.T) {
// Verify request was forwarded.
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastHistoryReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastHistoryReq.GetSandboxRef().GetName())
mock.mu.Unlock()
assert.Equal(t, "approved", entries[0].EventType)
@@ -583,7 +583,7 @@ func TestPolicyGetStatus(t *testing.T) {
// Verify request was forwarded (no version = latest).
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetSandboxRef().GetName())
assert.Equal(t, uint32(0), mock.lastStatusReq.GetVersion())
mock.mu.Unlock()
@@ -649,8 +649,7 @@ func TestPolicyGetStatus_WithGlobal(t *testing.T) {
// Verify global flag was forwarded in the proto request.
mock.mu.Lock()
assert.True(t, mock.lastStatusReq.GetGlobal())
assert.Empty(t, mock.lastStatusReq.GetName())
assert.Nil(t, mock.lastStatusReq.GetWorkspaceScope())
assert.Nil(t, mock.lastStatusReq.GetSandboxRef())
mock.mu.Unlock()
}
@@ -675,8 +674,7 @@ func TestPolicyGetStatus_WithGlobalIgnoresNonEmptyName(t *testing.T) {
mock.mu.Lock()
assert.True(t, mock.lastStatusReq.GetGlobal())
assert.Equal(t, "some-sandbox", mock.lastStatusReq.GetName())
assert.Nil(t, mock.lastStatusReq.GetWorkspaceScope())
assert.Nil(t, mock.lastStatusReq.GetSandboxRef())
mock.mu.Unlock()
}
@@ -731,8 +729,8 @@ 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.GetWorkspaceScope().GetWorkspace())
assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetName())
assert.Equal(t, "default", mock.lastStatusReq.GetSandboxRef().GetWorkspaceScope().GetWorkspace())
assert.Equal(t, "my-sandbox", mock.lastStatusReq.GetSandboxRef().GetName())
mock.mu.Unlock()
}
@@ -773,14 +771,15 @@ func TestPolicyList(t *testing.T) {
client, cleanup := setupPolicyTest(t, mock)
defer cleanup()
revisions, err := client.List(context.Background(), "default")
revisions, err := client.List(context.Background(), "default", "my-sandbox")
require.NoError(t, err)
require.Len(t, revisions, 2)
// Verify request was forwarded (no pagination options).
mock.mu.Lock()
assert.Equal(t, "default", mock.lastListReq.GetWorkspaceScope().GetWorkspace())
assert.Equal(t, "my-sandbox", mock.lastListReq.GetSandboxRef().GetName())
assert.Equal(t, "default", mock.lastListReq.GetSandboxRef().GetWorkspaceScope().GetWorkspace())
assert.Equal(t, uint32(0), mock.lastListReq.GetLimit())
assert.Equal(t, uint32(0), mock.lastListReq.GetOffset())
mock.mu.Unlock()
@@ -805,7 +804,7 @@ func TestPolicyList_WithPagination(t *testing.T) {
client, cleanup := setupPolicyTest(t, mock)
defer cleanup()
revisions, err := client.List(context.Background(), "default",
revisions, err := client.List(context.Background(), "default", "my-sandbox",
types.WithLimit(10),
types.WithOffset(20),
)
@@ -827,7 +826,7 @@ func TestPolicyList_Empty(t *testing.T) {
client, cleanup := setupPolicyTest(t, mock)
defer cleanup()
revisions, err := client.List(context.Background(), "default")
revisions, err := client.List(context.Background(), "default", "my-sandbox")
require.NoError(t, err)
assert.Nil(t, revisions)
@@ -845,7 +844,7 @@ func TestPolicyList_WithGlobal(t *testing.T) {
defer cleanup()
// List with global flag and empty workspace.
revisions, err := client.List(context.Background(), "", types.WithListGlobal(true))
revisions, err := client.List(context.Background(), "", "", types.WithListGlobal(true))
require.NoError(t, err)
require.Len(t, revisions, 1)
@@ -855,7 +854,7 @@ func TestPolicyList_WithGlobal(t *testing.T) {
// Verify global flag was forwarded in the proto request.
mock.mu.Lock()
assert.True(t, mock.lastListReq.GetGlobal())
assert.Nil(t, mock.lastListReq.GetWorkspaceScope())
assert.Nil(t, mock.lastListReq.GetSandboxRef())
mock.mu.Unlock()
}
@@ -870,14 +869,14 @@ func TestPolicyList_WithGlobalIgnoresWorkspace(t *testing.T) {
client, cleanup := setupPolicyTest(t, mock)
defer cleanup()
revisions, err := client.List(context.Background(), "some-workspace", types.WithListGlobal(true))
revisions, err := client.List(context.Background(), "some-workspace", "", types.WithListGlobal(true))
require.NoError(t, err)
require.Len(t, revisions, 1)
mock.mu.Lock()
assert.True(t, mock.lastListReq.GetGlobal())
assert.Nil(t, mock.lastListReq.GetWorkspaceScope())
assert.Nil(t, mock.lastListReq.GetSandboxRef())
mock.mu.Unlock()
}
@@ -893,7 +892,7 @@ func TestPolicyList_WithGlobalAndPagination(t *testing.T) {
defer cleanup()
// Global flag composes with pagination options.
revisions, err := client.List(context.Background(), "",
revisions, err := client.List(context.Background(), "", "",
types.WithListGlobal(true),
types.WithLimit(10),
types.WithOffset(20),
@@ -920,7 +919,7 @@ func TestPolicyList_WithoutGlobal_PreservesExistingBehavior(t *testing.T) {
client, cleanup := setupPolicyTest(t, mock)
defer cleanup()
revisions, err := client.List(context.Background(), "default")
revisions, err := client.List(context.Background(), "default", "my-sandbox")
require.NoError(t, err)
require.Len(t, revisions, 1)
@@ -928,7 +927,8 @@ 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.GetWorkspaceScope().GetWorkspace())
assert.Equal(t, "my-sandbox", mock.lastListReq.GetSandboxRef().GetName())
assert.Equal(t, "default", mock.lastListReq.GetSandboxRef().GetWorkspaceScope().GetWorkspace())
mock.mu.Unlock()
}
@@ -939,7 +939,7 @@ func TestPolicyList_Error(t *testing.T) {
client, cleanup := setupPolicyTest(t, mock)
defer cleanup()
revisions, err := client.List(context.Background(), "default")
revisions, err := client.List(context.Background(), "default", "my-sandbox")
assert.Nil(t, revisions)
require.Error(t, err)
@@ -966,7 +966,7 @@ func TestPolicyEditDraftChunk(t *testing.T) {
// Verify request was forwarded.
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastEditReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastEditReq.GetSandboxRef().GetName())
assert.Equal(t, "chunk-1", mock.lastEditReq.GetChunkId())
require.NotNil(t, mock.lastEditReq.GetProposedRule())
assert.Equal(t, "allow-https", mock.lastEditReq.GetProposedRule().GetName())
@@ -1005,7 +1005,7 @@ func TestPolicyUndoDraftChunk(t *testing.T) {
// Verify request was forwarded.
mock.mu.Lock()
assert.Equal(t, "my-sandbox", mock.lastUndoReq.GetName())
assert.Equal(t, "my-sandbox", mock.lastUndoReq.GetSandboxRef().GetName())
assert.Equal(t, "chunk-3", mock.lastUndoReq.GetChunkId())
mock.mu.Unlock()
+12 -20
View File
@@ -89,8 +89,7 @@ func validateTemplateCreateSpec(spec *SandboxSpec) error {
func (s *sandboxClient) Get(ctx context.Context, workspace, name string) (*Sandbox, error) {
resp, err := s.client.GetSandbox(ctx, &pb.GetSandboxRequest{
Name: name,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, name),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -136,8 +135,7 @@ func (s *sandboxClient) list(ctx context.Context, req *pb.ListSandboxesRequest,
func (s *sandboxClient) Delete(ctx context.Context, workspace, name string) error {
_, err := s.client.DeleteSandbox(ctx, &pb.DeleteSandboxRequest{
Name: name,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, name),
})
if err != nil {
return converter.FromGRPCError(err)
@@ -147,8 +145,7 @@ func (s *sandboxClient) Delete(ctx context.Context, workspace, name string) erro
func (s *sandboxClient) Stop(ctx context.Context, workspace, name string) (*Sandbox, error) {
resp, err := s.client.StopSandbox(ctx, &pb.StopSandboxRequest{
Name: name,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, name),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -158,8 +155,7 @@ func (s *sandboxClient) Stop(ctx context.Context, workspace, name string) (*Sand
func (s *sandboxClient) Start(ctx context.Context, workspace, name string) (*Sandbox, error) {
resp, err := s.client.StartSandbox(ctx, &pb.StartSandboxRequest{
Name: name,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, name),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -169,10 +165,9 @@ func (s *sandboxClient) Start(ctx context.Context, workspace, name string) (*San
func (s *sandboxClient) AttachProvider(ctx context.Context, workspace, sandboxName, providerName string, expectedResourceVersion uint64) (*AttachProviderResult, error) {
resp, err := s.client.AttachSandboxProvider(ctx, &pb.AttachSandboxProviderRequest{
SandboxName: sandboxName,
ProviderName: providerName,
ExpectedResourceVersion: expectedResourceVersion,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -185,10 +180,9 @@ func (s *sandboxClient) AttachProvider(ctx context.Context, workspace, sandboxNa
func (s *sandboxClient) DetachProvider(ctx context.Context, workspace, sandboxName, providerName string, expectedResourceVersion uint64) (*DetachProviderResult, error) {
resp, err := s.client.DetachSandboxProvider(ctx, &pb.DetachSandboxProviderRequest{
SandboxName: sandboxName,
ProviderName: providerName,
ExpectedResourceVersion: expectedResourceVersion,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -201,8 +195,7 @@ func (s *sandboxClient) DetachProvider(ctx context.Context, workspace, sandboxNa
func (s *sandboxClient) ListProviders(ctx context.Context, workspace, sandboxName string) ([]*Provider, error) {
resp, err := s.client.ListSandboxProviders(ctx, &pb.ListSandboxProvidersRequest{
SandboxName: sandboxName,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -295,7 +288,7 @@ func (s *sandboxClient) Watch(ctx context.Context, workspace, name string, opts
streamCtx, streamCancel := context.WithCancel(ctx)
stream, err := s.client.WatchSandbox(streamCtx, &pb.WatchSandboxRequest{
Id: sb.ID,
SandboxRef: sandboxReferenceByID(sb.ID),
FollowStatus: true,
StopOnTerminal: watchOpts.StopOnTerminal,
})
@@ -368,11 +361,10 @@ 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(),
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByID(sb.ID),
Lines: cfg.Lines(),
Sources: cfg.Sources(),
MinLevel: cfg.MinLevel(),
}
if !cfg.Since().IsZero() {
req.SinceMs = converter.MillisFromTime(cfg.Since())
+32 -18
View File
@@ -54,6 +54,14 @@ func newMockSandboxServer() *mockSandboxServer {
}
}
func sandboxRefName(ref *dm.SandboxReference) string {
return ref.GetName()
}
func sandboxRefID(ref *dm.SandboxReference) string {
return ref.GetId()
}
func (s *mockSandboxServer) CreateSandbox(_ context.Context, req *pb.CreateSandboxRequest) (*pb.SandboxResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -82,9 +90,10 @@ func (s *mockSandboxServer) GetSandbox(_ context.Context, req *pb.GetSandboxRequ
if s.getErr != nil {
return nil, s.getErr
}
sb, ok := s.sandboxes[req.GetName()]
name := sandboxRefName(req.GetSandboxRef())
sb, ok := s.sandboxes[name]
if !ok {
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetName())
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name)
}
cloned := proto.Clone(sb).(*pb.Sandbox)
return &pb.SandboxResponse{Sandbox: cloned}, nil
@@ -117,20 +126,22 @@ func (s *mockSandboxServer) DeleteSandbox(_ context.Context, req *pb.DeleteSandb
if s.deleteErr != nil {
return nil, s.deleteErr
}
_, ok := s.sandboxes[req.GetName()]
name := sandboxRefName(req.GetSandboxRef())
_, ok := s.sandboxes[name]
if !ok {
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetName())
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name)
}
delete(s.sandboxes, req.GetName())
delete(s.sandboxes, name)
return &pb.DeleteSandboxResponse{Deleted: true}, nil
}
func (s *mockSandboxServer) StopSandbox(_ context.Context, req *pb.StopSandboxRequest) (*pb.SandboxResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
sb, ok := s.sandboxes[req.GetName()]
name := sandboxRefName(req.GetSandboxRef())
sb, ok := s.sandboxes[name]
if !ok {
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetName())
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name)
}
sb.Status.Phase = pb.SandboxPhase_SANDBOX_PHASE_STOPPED
return &pb.SandboxResponse{Sandbox: proto.Clone(sb).(*pb.Sandbox)}, nil
@@ -139,9 +150,10 @@ func (s *mockSandboxServer) StopSandbox(_ context.Context, req *pb.StopSandboxRe
func (s *mockSandboxServer) StartSandbox(_ context.Context, req *pb.StartSandboxRequest) (*pb.SandboxResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
sb, ok := s.sandboxes[req.GetName()]
name := sandboxRefName(req.GetSandboxRef())
sb, ok := s.sandboxes[name]
if !ok {
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetName())
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name)
}
sb.Status.Phase = pb.SandboxPhase_SANDBOX_PHASE_STARTING
return &pb.SandboxResponse{Sandbox: proto.Clone(sb).(*pb.Sandbox)}, nil
@@ -153,9 +165,10 @@ func (s *mockSandboxServer) AttachSandboxProvider(_ context.Context, req *pb.Att
if s.attachErr != nil {
return nil, s.attachErr
}
sb, ok := s.sandboxes[req.GetSandboxName()]
name := sandboxRefName(req.GetSandboxRef())
sb, ok := s.sandboxes[name]
if !ok {
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetSandboxName())
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name)
}
sb.Spec.Providers = append(sb.Spec.Providers, req.GetProviderName())
return &pb.AttachSandboxProviderResponse{Sandbox: sb, Attached: true}, nil
@@ -167,9 +180,10 @@ func (s *mockSandboxServer) DetachSandboxProvider(_ context.Context, req *pb.Det
if s.detachErr != nil {
return nil, s.detachErr
}
sb, ok := s.sandboxes[req.GetSandboxName()]
name := sandboxRefName(req.GetSandboxRef())
sb, ok := s.sandboxes[name]
if !ok {
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", req.GetSandboxName())
return nil, status.Errorf(codes.NotFound, "sandbox %q not found", name)
}
return &pb.DetachSandboxProviderResponse{Sandbox: sb, Detached: true}, nil
}
@@ -178,7 +192,7 @@ func (s *mockSandboxServer) ListSandboxProviders(_ context.Context, req *pb.List
if s.listProvErr != nil {
return nil, s.listProvErr
}
provs := s.providers[req.GetSandboxName()]
provs := s.providers[sandboxRefName(req.GetSandboxRef())]
return &pb.ListSandboxProvidersResponse{Providers: provs}, nil
}
@@ -945,12 +959,12 @@ func TestSandboxWatch_ResolvesNameToID(t *testing.T) {
require.NoError(t, err)
defer w.Stop()
// Verify the WatchSandboxRequest.Id contains the resolved ID, not the name
// Verify the WatchSandboxRequest reference contains the resolved ID, not the name.
mock.mu.Lock()
req := mock.watchRequest
mock.mu.Unlock()
require.NotNil(t, req)
assert.Equal(t, "resolved-id-123", req.GetId(), "Watch should send resolved sandbox ID, not the name")
assert.Equal(t, "resolved-id-123", req.GetSandboxRef().GetId(), "Watch should send resolved sandbox ID, not the name")
}
func TestSandboxWatch_ResolutionError(t *testing.T) {
@@ -1161,7 +1175,7 @@ func TestSandboxGetLogs(t *testing.T) {
// Verify name→id resolution: the proto request should contain the sandbox ID
mock.mu.Lock()
assert.Equal(t, "sb-id-123", mock.getLogsRequest.GetSandboxId())
assert.Equal(t, "sb-id-123", mock.getLogsRequest.GetSandboxRef().GetId())
mock.mu.Unlock()
}
@@ -1195,7 +1209,7 @@ func TestSandboxGetLogs_WithOptions(t *testing.T) {
mock.mu.Lock()
req := mock.getLogsRequest
mock.mu.Unlock()
assert.Equal(t, "sb-id-opts", req.GetSandboxId())
assert.Equal(t, "sb-id-opts", req.GetSandboxRef().GetId())
assert.Equal(t, uint32(50), req.GetLines())
assert.Equal(t, since.UnixMilli(), req.GetSinceMs())
assert.Equal(t, []string{"gateway", "sandbox"}, req.GetSources())
+12 -14
View File
@@ -21,11 +21,10 @@ func newServiceClient(conn grpc.ClientConnInterface) *serviceClient {
func (s *serviceClient) Expose(ctx context.Context, workspace, sandboxName, serviceName string, targetPort uint32, domain bool) (*ServiceEndpoint, error) {
resp, err := s.client.ExposeService(ctx, &pb.ExposeServiceRequest{
Sandbox: sandboxName,
Service: serviceName,
TargetPort: targetPort,
Domain: domain,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
Service: serviceName,
TargetPort: targetPort,
Domain: domain,
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -35,9 +34,8 @@ func (s *serviceClient) Expose(ctx context.Context, workspace, sandboxName, serv
func (s *serviceClient) Get(ctx context.Context, workspace, sandboxName, serviceName string) (*ServiceEndpoint, error) {
resp, err := s.client.GetService(ctx, &pb.GetServiceRequest{
Sandbox: sandboxName,
Service: serviceName,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
Service: serviceName,
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -46,9 +44,10 @@ 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,
WorkspaceScope: namedWorkspaceScope(workspace),
req := &pb.ListServicesRequest{WorkspaceScope: namedWorkspaceScope(workspace)}
if sandboxName != "" {
req.SandboxRef = sandboxReferenceByName(workspace, sandboxName)
req.WorkspaceScope = nil
}
return s.list(ctx, req, opts...)
}
@@ -83,9 +82,8 @@ func (s *serviceClient) list(ctx context.Context, req *pb.ListServicesRequest, o
func (s *serviceClient) Delete(ctx context.Context, workspace, sandboxName, serviceName string) error {
_, err := s.client.DeleteService(ctx, &pb.DeleteServiceRequest{
Sandbox: sandboxName,
Service: serviceName,
WorkspaceScope: namedWorkspaceScope(workspace),
SandboxRef: sandboxReferenceByName(workspace, sandboxName),
Service: serviceName,
})
if err != nil {
return converter.FromGRPCError(err)
+12 -9
View File
@@ -55,7 +55,7 @@ func (s *mockServiceServer) ExposeService(_ context.Context, req *pb.ExposeServi
Metadata: &dm.ObjectMeta{
Id: "ep-" + req.GetService(),
},
SandboxName: req.GetSandbox(),
SandboxName: sandboxRefName(req.GetSandboxRef()),
ServiceName: req.GetService(),
TargetPort: req.GetTargetPort(),
Domain: req.GetDomain(),
@@ -65,7 +65,7 @@ func (s *mockServiceServer) ExposeService(_ context.Context, req *pb.ExposeServi
resp.Url = "https://" + req.GetService() + ".example.com"
}
s.endpoints[serviceKey(req.GetSandbox(), req.GetService())] = resp
s.endpoints[serviceKey(sandboxRefName(req.GetSandboxRef()), req.GetService())] = resp
return resp, nil
}
@@ -76,9 +76,10 @@ func (s *mockServiceServer) GetService(_ context.Context, req *pb.GetServiceRequ
return nil, s.getErr
}
ep, ok := s.endpoints[serviceKey(req.GetSandbox(), req.GetService())]
sandboxName := sandboxRefName(req.GetSandboxRef())
ep, ok := s.endpoints[serviceKey(sandboxName, req.GetService())]
if !ok {
return nil, status.Errorf(codes.NotFound, "service %q not found in sandbox %q", req.GetService(), req.GetSandbox())
return nil, status.Errorf(codes.NotFound, "service %q not found in sandbox %q", req.GetService(), sandboxName)
}
return ep, nil
}
@@ -93,8 +94,9 @@ func (s *mockServiceServer) ListServices(_ context.Context, req *pb.ListServices
var services []*pb.ServiceEndpointResponse
for key, ep := range s.endpoints {
prefix := req.GetSandbox() + "/"
if req.GetSandbox() == "" || (len(key) >= len(prefix) && key[:len(prefix)] == prefix) {
sandboxName := sandboxRefName(req.GetSandboxRef())
prefix := sandboxName + "/"
if sandboxName == "" || (len(key) >= len(prefix) && key[:len(prefix)] == prefix) {
services = append(services, ep)
}
}
@@ -108,10 +110,11 @@ func (s *mockServiceServer) DeleteService(_ context.Context, req *pb.DeleteServi
return nil, s.deleteErr
}
key := serviceKey(req.GetSandbox(), req.GetService())
sandboxName := sandboxRefName(req.GetSandboxRef())
key := serviceKey(sandboxName, req.GetService())
_, ok := s.endpoints[key]
if !ok {
return nil, status.Errorf(codes.NotFound, "service %q not found in sandbox %q", req.GetService(), req.GetSandbox())
return nil, status.Errorf(codes.NotFound, "service %q not found in sandbox %q", req.GetService(), sandboxName)
}
delete(s.endpoints, key)
return &pb.DeleteServiceResponse{Deleted: true}, nil
@@ -278,7 +281,7 @@ func TestServiceListAll_SelectsAllWorkspaces(t *testing.T) {
require.NoError(t, err)
assert.Empty(t, endpoints)
require.NotNil(t, mock.lastList)
assert.Empty(t, mock.lastList.GetSandbox())
assert.Nil(t, mock.lastList.GetSandboxRef())
assert.NotNil(t, mock.lastList.GetWorkspaceScope().GetAllWorkspaces())
}
+2 -2
View File
@@ -32,7 +32,7 @@ func newSSHClient(conn grpc.ClientConnInterface, sandboxes SandboxInterface) *ss
func (s *sshClient) CreateSession(ctx context.Context, _, sandboxID string) (*SSHSession, error) {
resp, err := s.client.CreateSshSession(ctx, &pb.CreateSshSessionRequest{
SandboxId: sandboxID,
SandboxRef: sandboxReferenceByID(sandboxID),
})
if err != nil {
return nil, converter.FromGRPCError(err)
@@ -94,7 +94,7 @@ func (s *sshClient) Tunnel(ctx context.Context, workspace, sandboxName string, p
initFrame := &pb.TcpForwardFrame{
Payload: &pb.TcpForwardFrame_Init{
Init: &pb.TcpForwardInit{
SandboxId: sandbox.ID,
SandboxRef: sandboxReferenceByID(sandbox.ID),
ServiceId: cfg.serviceID,
AuthorizationToken: session.Token,
Target: &pb.TcpForwardInit_Ssh{
+5 -4
View File
@@ -51,13 +51,14 @@ func (s *mockSSHServer) CreateSshSession(_ context.Context, req *pb.CreateSshSes
return nil, s.createErr
}
token := "tok-" + req.GetSandboxId()
sandboxID := sandboxRefID(req.GetSandboxRef())
token := "tok-" + sandboxID
if s.nextToken != "" {
token = s.nextToken
}
resp := &pb.CreateSshSessionResponse{
SandboxId: req.GetSandboxId(),
SandboxId: sandboxID,
Token: token,
GatewayHost: "gw.example.com",
GatewayPort: 2222,
@@ -65,7 +66,7 @@ func (s *mockSSHServer) CreateSshSession(_ context.Context, req *pb.CreateSshSes
HostKeyFingerprint: "SHA256:abc123",
ExpiresAtMs: 1700000000000,
}
s.sessions[req.GetSandboxId()] = resp
s.sessions[sandboxID] = resp
s.tokens[token] = true
return resp, nil
}
@@ -330,7 +331,7 @@ func TestSSHTunnel_Success(t *testing.T) {
mock.mu.Unlock()
require.NotNil(t, init)
assert.Equal(t, "sb-123", init.GetSandboxId())
assert.Equal(t, "sb-123", init.GetSandboxRef().GetId())
assert.NotEmpty(t, init.GetAuthorizationToken())
assert.NotNil(t, init.GetSsh(), "target should be SshRelayTarget")
}
+2 -2
View File
@@ -56,8 +56,8 @@ func (t *tcpClient) Forward(ctx context.Context, workspace, sandboxName string,
initFrame := &pb.TcpForwardFrame{
Payload: &pb.TcpForwardFrame_Init{
Init: &pb.TcpForwardInit{
SandboxId: sb.ID,
ServiceId: cfg.serviceID,
SandboxRef: sandboxReferenceByID(sb.ID),
ServiceId: cfg.serviceID,
Target: &pb.TcpForwardInit_Tcp{
Tcp: &pb.TcpRelayTarget{
Host: "127.0.0.1",
+3 -3
View File
@@ -130,7 +130,7 @@ func TestTCPForward_InitFrame(t *testing.T) {
mock.mu.Unlock()
require.NotNil(t, init)
assert.Equal(t, "sb-my-sandbox", init.GetSandboxId())
assert.Equal(t, "sb-my-sandbox", init.GetSandboxRef().GetId())
assert.Empty(t, init.GetServiceId(), "service_id should be empty per FR-007a")
assert.Empty(t, init.GetAuthorizationToken())
@@ -336,7 +336,7 @@ func TestTCPForward_WithServiceID(t *testing.T) {
require.NotNil(t, init)
assert.Equal(t, "audit-svc", init.GetServiceId())
assert.Equal(t, "sb-my-sandbox", init.GetSandboxId())
assert.Equal(t, "sb-my-sandbox", init.GetSandboxRef().GetId())
}
func TestTCPForward_WithoutOptions_BackwardCompat(t *testing.T) {
@@ -416,7 +416,7 @@ func TestTCPForward_ResolvesNameToID(t *testing.T) {
require.NotNil(t, init)
// stubSandboxResolver returns ID "sb-<name>" — verify the proto has the resolved ID, not the name
assert.Equal(t, "sb-my-sandbox", init.GetSandboxId(), "Forward should send resolved sandbox ID, not the name")
assert.Equal(t, "sb-my-sandbox", init.GetSandboxRef().GetId(), "Forward should send resolved sandbox ID, not the name")
}
func TestTCPForward_ResolutionError(t *testing.T) {
+13
View File
@@ -16,3 +16,16 @@ func allWorkspacesScope() *dm.WorkspaceSelector {
Selection: &dm.WorkspaceSelector_AllWorkspaces{AllWorkspaces: &dm.AllWorkspaces{}},
}
}
func sandboxReferenceByName(workspace, name string) *dm.SandboxReference {
return &dm.SandboxReference{
Identifier: &dm.SandboxReference_Name{Name: name},
WorkspaceScope: namedWorkspaceScope(workspace),
}
}
func sandboxReferenceByID(id string) *dm.SandboxReference {
return &dm.SandboxReference{
Identifier: &dm.SandboxReference_Id{Id: id},
}
}
+155 -47
View File
@@ -204,6 +204,102 @@ func (*AllWorkspaces) Descriptor() ([]byte, []int) {
return file_datamodel_proto_rawDescGZIP(), []int{1}
}
// Identifies one existing sandbox in the public API.
//
// Exactly one identifier is required. `id` is the immutable, globally unique
// gateway-generated ObjectMeta.id and MUST NOT include workspace_scope. `name`
// is the human-readable ObjectMeta.name and requires a named workspace_scope;
// all_workspaces is never valid for a sandbox reference.
type SandboxReference struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Identifier:
//
// *SandboxReference_Id
// *SandboxReference_Name
Identifier isSandboxReference_Identifier `protobuf_oneof:"identifier"`
WorkspaceScope *WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SandboxReference) Reset() {
*x = SandboxReference{}
mi := &file_datamodel_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SandboxReference) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SandboxReference) ProtoMessage() {}
func (x *SandboxReference) ProtoReflect() protoreflect.Message {
mi := &file_datamodel_proto_msgTypes[2]
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 SandboxReference.ProtoReflect.Descriptor instead.
func (*SandboxReference) Descriptor() ([]byte, []int) {
return file_datamodel_proto_rawDescGZIP(), []int{2}
}
func (x *SandboxReference) GetIdentifier() isSandboxReference_Identifier {
if x != nil {
return x.Identifier
}
return nil
}
func (x *SandboxReference) GetId() string {
if x != nil {
if x, ok := x.Identifier.(*SandboxReference_Id); ok {
return x.Id
}
}
return ""
}
func (x *SandboxReference) GetName() string {
if x != nil {
if x, ok := x.Identifier.(*SandboxReference_Name); ok {
return x.Name
}
}
return ""
}
func (x *SandboxReference) GetWorkspaceScope() *WorkspaceSelector {
if x != nil {
return x.WorkspaceScope
}
return nil
}
type isSandboxReference_Identifier interface {
isSandboxReference_Identifier()
}
type SandboxReference_Id struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3,oneof"`
}
type SandboxReference_Name struct {
Name string `protobuf:"bytes,2,opt,name=name,proto3,oneof"`
}
func (*SandboxReference_Id) isSandboxReference_Identifier() {}
func (*SandboxReference_Name) isSandboxReference_Identifier() {}
// Kubernetes-style metadata shared by all top-level OpenShell domain objects.
//
// This structure provides consistent metadata (identity, labels, annotations,
@@ -239,7 +335,7 @@ type ObjectMeta struct {
func (x *ObjectMeta) Reset() {
*x = ObjectMeta{}
mi := &file_datamodel_proto_msgTypes[2]
mi := &file_datamodel_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -251,7 +347,7 @@ func (x *ObjectMeta) String() string {
func (*ObjectMeta) ProtoMessage() {}
func (x *ObjectMeta) ProtoReflect() protoreflect.Message {
mi := &file_datamodel_proto_msgTypes[2]
mi := &file_datamodel_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -264,7 +360,7 @@ func (x *ObjectMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use ObjectMeta.ProtoReflect.Descriptor instead.
func (*ObjectMeta) Descriptor() ([]byte, []int) {
return file_datamodel_proto_rawDescGZIP(), []int{2}
return file_datamodel_proto_rawDescGZIP(), []int{3}
}
func (x *ObjectMeta) GetId() string {
@@ -333,7 +429,7 @@ type WorkspaceStatus struct {
func (x *WorkspaceStatus) Reset() {
*x = WorkspaceStatus{}
mi := &file_datamodel_proto_msgTypes[3]
mi := &file_datamodel_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -345,7 +441,7 @@ func (x *WorkspaceStatus) String() string {
func (*WorkspaceStatus) ProtoMessage() {}
func (x *WorkspaceStatus) ProtoReflect() protoreflect.Message {
mi := &file_datamodel_proto_msgTypes[3]
mi := &file_datamodel_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -358,7 +454,7 @@ func (x *WorkspaceStatus) ProtoReflect() protoreflect.Message {
// Deprecated: Use WorkspaceStatus.ProtoReflect.Descriptor instead.
func (*WorkspaceStatus) Descriptor() ([]byte, []int) {
return file_datamodel_proto_rawDescGZIP(), []int{3}
return file_datamodel_proto_rawDescGZIP(), []int{4}
}
func (x *WorkspaceStatus) GetPhase() WorkspacePhase {
@@ -384,7 +480,7 @@ type Workspace struct {
func (x *Workspace) Reset() {
*x = Workspace{}
mi := &file_datamodel_proto_msgTypes[4]
mi := &file_datamodel_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -396,7 +492,7 @@ func (x *Workspace) String() string {
func (*Workspace) ProtoMessage() {}
func (x *Workspace) ProtoReflect() protoreflect.Message {
mi := &file_datamodel_proto_msgTypes[4]
mi := &file_datamodel_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -409,7 +505,7 @@ func (x *Workspace) ProtoReflect() protoreflect.Message {
// Deprecated: Use Workspace.ProtoReflect.Descriptor instead.
func (*Workspace) Descriptor() ([]byte, []int) {
return file_datamodel_proto_rawDescGZIP(), []int{4}
return file_datamodel_proto_rawDescGZIP(), []int{5}
}
func (x *Workspace) GetMetadata() *ObjectMeta {
@@ -442,7 +538,7 @@ type CredentialHandle struct {
func (x *CredentialHandle) Reset() {
*x = CredentialHandle{}
mi := &file_datamodel_proto_msgTypes[5]
mi := &file_datamodel_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -454,7 +550,7 @@ func (x *CredentialHandle) String() string {
func (*CredentialHandle) ProtoMessage() {}
func (x *CredentialHandle) ProtoReflect() protoreflect.Message {
mi := &file_datamodel_proto_msgTypes[5]
mi := &file_datamodel_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -467,7 +563,7 @@ func (x *CredentialHandle) ProtoReflect() protoreflect.Message {
// Deprecated: Use CredentialHandle.ProtoReflect.Descriptor instead.
func (*CredentialHandle) Descriptor() ([]byte, []int) {
return file_datamodel_proto_rawDescGZIP(), []int{5}
return file_datamodel_proto_rawDescGZIP(), []int{6}
}
func (x *CredentialHandle) GetDriver() string {
@@ -518,7 +614,7 @@ type Provider struct {
func (x *Provider) Reset() {
*x = Provider{}
mi := &file_datamodel_proto_msgTypes[6]
mi := &file_datamodel_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -530,7 +626,7 @@ func (x *Provider) String() string {
func (*Provider) ProtoMessage() {}
func (x *Provider) ProtoReflect() protoreflect.Message {
mi := &file_datamodel_proto_msgTypes[6]
mi := &file_datamodel_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -543,7 +639,7 @@ func (x *Provider) ProtoReflect() protoreflect.Message {
// Deprecated: Use Provider.ProtoReflect.Descriptor instead.
func (*Provider) Descriptor() ([]byte, []int) {
return file_datamodel_proto_rawDescGZIP(), []int{6}
return file_datamodel_proto_rawDescGZIP(), []int{7}
}
func (x *Provider) GetMetadata() *ObjectMeta {
@@ -604,7 +700,13 @@ const file_datamodel_proto_rawDesc = "" +
"\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" +
"\rAllWorkspaces\"\x9c\x01\n" +
"\x10SandboxReference\x12\x10\n" +
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x12\x14\n" +
"\x04name\x18\x02 \x01(\tH\x00R\x04name\x12R\n" +
"\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeB\f\n" +
"\n" +
"identifier\"\xeb\x03\n" +
"\n" +
"ObjectMeta\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
@@ -671,43 +773,45 @@ func file_datamodel_proto_rawDescGZIP() []byte {
}
var file_datamodel_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_datamodel_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
var file_datamodel_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
var file_datamodel_proto_goTypes = []any{
(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
(*SandboxReference)(nil), // 3: openshell.datamodel.v1.SandboxReference
(*ObjectMeta)(nil), // 4: openshell.datamodel.v1.ObjectMeta
(*WorkspaceStatus)(nil), // 5: openshell.datamodel.v1.WorkspaceStatus
(*Workspace)(nil), // 6: openshell.datamodel.v1.Workspace
(*CredentialHandle)(nil), // 7: openshell.datamodel.v1.CredentialHandle
(*Provider)(nil), // 8: openshell.datamodel.v1.Provider
nil, // 9: openshell.datamodel.v1.ObjectMeta.LabelsEntry
nil, // 10: openshell.datamodel.v1.ObjectMeta.AnnotationsEntry
nil, // 11: openshell.datamodel.v1.CredentialHandle.MetadataEntry
nil, // 12: openshell.datamodel.v1.Provider.CredentialsEntry
nil, // 13: openshell.datamodel.v1.Provider.ConfigEntry
nil, // 14: openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry
nil, // 15: openshell.datamodel.v1.Provider.CredentialHandlesEntry
}
var file_datamodel_proto_depIdxs = []int32{
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
1, // 1: openshell.datamodel.v1.SandboxReference.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector
9, // 2: openshell.datamodel.v1.ObjectMeta.labels:type_name -> openshell.datamodel.v1.ObjectMeta.LabelsEntry
10, // 3: openshell.datamodel.v1.ObjectMeta.annotations:type_name -> openshell.datamodel.v1.ObjectMeta.AnnotationsEntry
0, // 4: openshell.datamodel.v1.WorkspaceStatus.phase:type_name -> openshell.datamodel.v1.WorkspacePhase
4, // 5: openshell.datamodel.v1.Workspace.metadata:type_name -> openshell.datamodel.v1.ObjectMeta
5, // 6: openshell.datamodel.v1.Workspace.status:type_name -> openshell.datamodel.v1.WorkspaceStatus
11, // 7: openshell.datamodel.v1.CredentialHandle.metadata:type_name -> openshell.datamodel.v1.CredentialHandle.MetadataEntry
4, // 8: openshell.datamodel.v1.Provider.metadata:type_name -> openshell.datamodel.v1.ObjectMeta
12, // 9: openshell.datamodel.v1.Provider.credentials:type_name -> openshell.datamodel.v1.Provider.CredentialsEntry
13, // 10: openshell.datamodel.v1.Provider.config:type_name -> openshell.datamodel.v1.Provider.ConfigEntry
14, // 11: openshell.datamodel.v1.Provider.credential_expires_at_ms:type_name -> openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry
15, // 12: openshell.datamodel.v1.Provider.credential_handles:type_name -> openshell.datamodel.v1.Provider.CredentialHandlesEntry
7, // 13: openshell.datamodel.v1.Provider.CredentialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle
14, // [14:14] is the sub-list for method output_type
14, // [14:14] is the sub-list for method input_type
14, // [14:14] is the sub-list for extension type_name
14, // [14:14] is the sub-list for extension extendee
0, // [0:14] is the sub-list for field type_name
}
func init() { file_datamodel_proto_init() }
@@ -719,13 +823,17 @@ func file_datamodel_proto_init() {
(*WorkspaceSelector_Workspace)(nil),
(*WorkspaceSelector_AllWorkspaces)(nil),
}
file_datamodel_proto_msgTypes[2].OneofWrappers = []any{
(*SandboxReference_Id)(nil),
(*SandboxReference_Name)(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: 14,
NumMessages: 15,
NumExtensions: 0,
NumServices: 0,
},
File diff suppressed because it is too large Load Diff
+71 -66
View File
@@ -10,6 +10,7 @@
package sandboxv1
import (
datamodelv1 "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
structpb "google.golang.org/protobuf/types/known/structpb"
@@ -1497,8 +1498,9 @@ func (x *NetworkBinary) GetHarness() bool {
// Request to get sandbox settings by sandbox ID.
type GetSandboxConfigRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The sandbox ID.
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
// Canonical public sandbox reference. Sandbox-authenticated callers use the
// ID variant; user clients may use either supported variant.
SandboxRef *datamodelv1.SandboxReference `protobuf:"bytes,2,opt,name=sandbox_ref,json=sandboxRef,proto3" json:"sandbox_ref,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1533,11 +1535,11 @@ func (*GetSandboxConfigRequest) Descriptor() ([]byte, []int) {
return file_sandbox_proto_rawDescGZIP(), []int{16}
}
func (x *GetSandboxConfigRequest) GetSandboxId() string {
func (x *GetSandboxConfigRequest) GetSandboxRef() *datamodelv1.SandboxReference {
if x != nil {
return x.SandboxId
return x.SandboxRef
}
return ""
return nil
}
// Request to get gateway-global settings.
@@ -2069,7 +2071,7 @@ var File_sandbox_proto protoreflect.FileDescriptor
const file_sandbox_proto_rawDesc = "" +
"\n" +
"\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xa8\x05\n" +
"\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x0fdatamodel.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xa8\x05\n" +
"\rSandboxPolicy\x12\x18\n" +
"\aversion\x18\x01 \x01(\rR\aversion\x12F\n" +
"\n" +
@@ -2199,10 +2201,11 @@ const file_sandbox_proto_rawDesc = "" +
"\x03any\x18\x02 \x03(\tR\x03any\"A\n" +
"\rNetworkBinary\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x1c\n" +
"\aharness\x18\x02 \x01(\bB\x02\x18\x01R\aharness\"8\n" +
"\x17GetSandboxConfigRequest\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x19\n" +
"\aharness\x18\x02 \x01(\bB\x02\x18\x01R\aharness\"v\n" +
"\x17GetSandboxConfigRequest\x12I\n" +
"\vsandbox_ref\x18\x02 \x01(\v2(.openshell.datamodel.v1.SandboxReferenceR\n" +
"sandboxRefJ\x04\b\x01\x10\x02R\n" +
"sandbox_id\"\x19\n" +
"\x17GetGatewayConfigRequest\"\x82\x02\n" +
"\x18GetGatewayConfigResponse\x12X\n" +
"\bsettings\x18\x01 \x03(\v2<.openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntryR\bsettings\x12+\n" +
@@ -2271,41 +2274,42 @@ func file_sandbox_proto_rawDescGZIP() []byte {
var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
var file_sandbox_proto_goTypes = []any{
(SettingScope)(0), // 0: openshell.sandbox.v1.SettingScope
(PolicySource)(0), // 1: openshell.sandbox.v1.PolicySource
(*SandboxPolicy)(nil), // 2: openshell.sandbox.v1.SandboxPolicy
(*FilesystemPolicy)(nil), // 3: openshell.sandbox.v1.FilesystemPolicy
(*LandlockPolicy)(nil), // 4: openshell.sandbox.v1.LandlockPolicy
(*ProcessPolicy)(nil), // 5: openshell.sandbox.v1.ProcessPolicy
(*NetworkPolicyRule)(nil), // 6: openshell.sandbox.v1.NetworkPolicyRule
(*NetworkMiddlewareConfig)(nil), // 7: openshell.sandbox.v1.NetworkMiddlewareConfig
(*MiddlewareEndpointSelector)(nil), // 8: openshell.sandbox.v1.MiddlewareEndpointSelector
(*NetworkCredentialBinding)(nil), // 9: openshell.sandbox.v1.NetworkCredentialBinding
(*NetworkEndpoint)(nil), // 10: openshell.sandbox.v1.NetworkEndpoint
(*McpOptions)(nil), // 11: openshell.sandbox.v1.McpOptions
(*GraphqlOperation)(nil), // 12: openshell.sandbox.v1.GraphqlOperation
(*L7DenyRule)(nil), // 13: openshell.sandbox.v1.L7DenyRule
(*L7Rule)(nil), // 14: openshell.sandbox.v1.L7Rule
(*L7Allow)(nil), // 15: openshell.sandbox.v1.L7Allow
(*L7QueryMatcher)(nil), // 16: openshell.sandbox.v1.L7QueryMatcher
(*NetworkBinary)(nil), // 17: openshell.sandbox.v1.NetworkBinary
(*GetSandboxConfigRequest)(nil), // 18: openshell.sandbox.v1.GetSandboxConfigRequest
(*GetGatewayConfigRequest)(nil), // 19: openshell.sandbox.v1.GetGatewayConfigRequest
(*GetGatewayConfigResponse)(nil), // 20: openshell.sandbox.v1.GetGatewayConfigResponse
(*SettingValue)(nil), // 21: openshell.sandbox.v1.SettingValue
(*EffectiveSetting)(nil), // 22: openshell.sandbox.v1.EffectiveSetting
(*GetSandboxConfigResponse)(nil), // 23: openshell.sandbox.v1.GetSandboxConfigResponse
(*SupervisorMiddlewareService)(nil), // 24: openshell.sandbox.v1.SupervisorMiddlewareService
nil, // 25: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry
nil, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry
nil, // 27: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry
nil, // 28: openshell.sandbox.v1.L7DenyRule.QueryEntry
nil, // 29: openshell.sandbox.v1.L7DenyRule.ParamsEntry
nil, // 30: openshell.sandbox.v1.L7Allow.QueryEntry
nil, // 31: openshell.sandbox.v1.L7Allow.ParamsEntry
nil, // 32: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry
nil, // 33: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry
(*structpb.Struct)(nil), // 34: google.protobuf.Struct
(SettingScope)(0), // 0: openshell.sandbox.v1.SettingScope
(PolicySource)(0), // 1: openshell.sandbox.v1.PolicySource
(*SandboxPolicy)(nil), // 2: openshell.sandbox.v1.SandboxPolicy
(*FilesystemPolicy)(nil), // 3: openshell.sandbox.v1.FilesystemPolicy
(*LandlockPolicy)(nil), // 4: openshell.sandbox.v1.LandlockPolicy
(*ProcessPolicy)(nil), // 5: openshell.sandbox.v1.ProcessPolicy
(*NetworkPolicyRule)(nil), // 6: openshell.sandbox.v1.NetworkPolicyRule
(*NetworkMiddlewareConfig)(nil), // 7: openshell.sandbox.v1.NetworkMiddlewareConfig
(*MiddlewareEndpointSelector)(nil), // 8: openshell.sandbox.v1.MiddlewareEndpointSelector
(*NetworkCredentialBinding)(nil), // 9: openshell.sandbox.v1.NetworkCredentialBinding
(*NetworkEndpoint)(nil), // 10: openshell.sandbox.v1.NetworkEndpoint
(*McpOptions)(nil), // 11: openshell.sandbox.v1.McpOptions
(*GraphqlOperation)(nil), // 12: openshell.sandbox.v1.GraphqlOperation
(*L7DenyRule)(nil), // 13: openshell.sandbox.v1.L7DenyRule
(*L7Rule)(nil), // 14: openshell.sandbox.v1.L7Rule
(*L7Allow)(nil), // 15: openshell.sandbox.v1.L7Allow
(*L7QueryMatcher)(nil), // 16: openshell.sandbox.v1.L7QueryMatcher
(*NetworkBinary)(nil), // 17: openshell.sandbox.v1.NetworkBinary
(*GetSandboxConfigRequest)(nil), // 18: openshell.sandbox.v1.GetSandboxConfigRequest
(*GetGatewayConfigRequest)(nil), // 19: openshell.sandbox.v1.GetGatewayConfigRequest
(*GetGatewayConfigResponse)(nil), // 20: openshell.sandbox.v1.GetGatewayConfigResponse
(*SettingValue)(nil), // 21: openshell.sandbox.v1.SettingValue
(*EffectiveSetting)(nil), // 22: openshell.sandbox.v1.EffectiveSetting
(*GetSandboxConfigResponse)(nil), // 23: openshell.sandbox.v1.GetSandboxConfigResponse
(*SupervisorMiddlewareService)(nil), // 24: openshell.sandbox.v1.SupervisorMiddlewareService
nil, // 25: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry
nil, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry
nil, // 27: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry
nil, // 28: openshell.sandbox.v1.L7DenyRule.QueryEntry
nil, // 29: openshell.sandbox.v1.L7DenyRule.ParamsEntry
nil, // 30: openshell.sandbox.v1.L7Allow.QueryEntry
nil, // 31: openshell.sandbox.v1.L7Allow.ParamsEntry
nil, // 32: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry
nil, // 33: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry
(*structpb.Struct)(nil), // 34: google.protobuf.Struct
(*datamodelv1.SandboxReference)(nil), // 35: openshell.datamodel.v1.SandboxReference
}
var file_sandbox_proto_depIdxs = []int32{
3, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy
@@ -2327,27 +2331,28 @@ var file_sandbox_proto_depIdxs = []int32{
15, // 16: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow
30, // 17: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry
31, // 18: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry
32, // 19: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry
21, // 20: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue
0, // 21: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope
2, // 22: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy
33, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry
1, // 24: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource
24, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService
6, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule
7, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig
12, // 28: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation
16, // 29: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
16, // 30: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
16, // 31: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
16, // 32: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
21, // 33: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue
22, // 34: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting
35, // [35:35] is the sub-list for method output_type
35, // [35:35] is the sub-list for method input_type
35, // [35:35] is the sub-list for extension type_name
35, // [35:35] is the sub-list for extension extendee
0, // [0:35] is the sub-list for field type_name
35, // 19: openshell.sandbox.v1.GetSandboxConfigRequest.sandbox_ref:type_name -> openshell.datamodel.v1.SandboxReference
32, // 20: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry
21, // 21: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue
0, // 22: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope
2, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy
33, // 24: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry
1, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource
24, // 26: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService
6, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule
7, // 28: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig
12, // 29: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation
16, // 30: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
16, // 31: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
16, // 32: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
16, // 33: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher
21, // 34: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue
22, // 35: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting
36, // [36:36] is the sub-list for method output_type
36, // [36:36] is the sub-list for method input_type
36, // [36:36] is the sub-list for extension type_name
36, // [36:36] is the sub-list for extension extendee
0, // [0:36] is the sub-list for field type_name
}
func init() { file_sandbox_proto_init() }
+57 -27
View File
@@ -58,20 +58,34 @@ const enc = (s: string) => new TextEncoder().encode(s);
type ScopedRequest = {
workspaceScope?: { selection?: { case?: string; value?: unknown } };
sandboxRef?: {
identifier?: { case?: string; value?: unknown };
workspaceScope?: { selection?: { case?: string; value?: unknown } };
};
};
function selectedWorkspace(req: ScopedRequest): string | undefined {
const selection = req.workspaceScope?.selection;
const selection = (req.sandboxRef?.workspaceScope ?? req.workspaceScope)?.selection;
return selection?.case === 'workspace' && typeof selection.value === 'string' ? selection.value : undefined;
}
function requestSandboxName(req: ScopedRequest): string | undefined {
const identifier = req.sandboxRef?.identifier;
return identifier?.case === 'name' && typeof identifier.value === 'string' ? identifier.value : undefined;
}
function requestSandboxId(req: ScopedRequest): string | undefined {
const identifier = req.sandboxRef?.identifier;
return identifier?.case === 'id' && typeof identifier.value === 'string' ? identifier.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[] } = {};
let execReq: ScopedRequest & { tty?: boolean; command?: string[] } = {};
const sandbox = client({
getSandbox: () => readySandbox('sb', 'sb-id-1'),
// eslint-disable-next-line require-yield
@@ -85,7 +99,7 @@ describe('exec / execStream', () => {
});
const result = await sandbox.exec('sb', ['/bin/sh', '-c', 'echo hi']);
expect(execReq.sandboxId).toBe('sb-id-1');
expect(requestSandboxId(execReq)).toBe('sb-id-1');
expect(execReq.tty).toBe(false);
expect(execReq.command).toEqual(['/bin/sh', '-c', 'echo hi']);
expect(result.exitCode).toBe(3);
@@ -352,13 +366,14 @@ describe('create', () => {
},
getSandbox: (req) => {
const workspace = selectedWorkspace(req);
if (req.name === 'exec') observed.execGet = workspace;
else if (req.name === 'interactive') observed.interactiveGet = workspace;
else if (req.name === 'ssh') observed.sshGet = workspace;
else if (req.name === 'forward') observed.forwardGet = workspace;
else if (req.name === 'config' && workspace) observed.configGets.push(workspace);
const name = requestSandboxName(req);
if (name === 'exec') observed.execGet = workspace;
else if (name === 'interactive') observed.interactiveGet = workspace;
else if (name === 'ssh') observed.sshGet = workspace;
else if (name === 'forward') observed.forwardGet = workspace;
else if (name === 'config' && workspace) observed.configGets.push(workspace);
else observed.get = req;
return readySandbox(req.name, `${req.name}-id`, 7n, undefined, workspace ?? 'default');
return readySandbox(name ?? '', `${name}-id`, 7n, undefined, workspace ?? 'default');
},
listSandboxes: (req) => {
observed.list = req;
@@ -384,16 +399,26 @@ describe('create', () => {
attachSandboxProvider: (req) => {
observed.attach = req;
return {
sandbox: readySandbox(req.sandboxName, 'attach-id', 7n, undefined, selectedWorkspace(req) ?? 'default')
.sandbox,
sandbox: readySandbox(
requestSandboxName(req) ?? '',
'attach-id',
7n,
undefined,
selectedWorkspace(req) ?? 'default',
).sandbox,
attached: true,
};
},
detachSandboxProvider: (req) => {
observed.detach = req;
return {
sandbox: readySandbox(req.sandboxName, 'detach-id', 7n, undefined, selectedWorkspace(req) ?? 'default')
.sandbox,
sandbox: readySandbox(
requestSandboxName(req) ?? '',
'detach-id',
7n,
undefined,
selectedWorkspace(req) ?? 'default',
).sandbox,
detached: true,
};
},
@@ -425,7 +450,7 @@ describe('create', () => {
yield { payload: { case: 'exit', value: { exitCode: 0 } } };
},
createSshSession: (req) => ({
sandboxId: req.sandboxId,
sandboxId: requestSandboxId(req) ?? '',
token: 'tok',
gatewayHost: 'gw',
gatewayPort: 443,
@@ -756,7 +781,7 @@ describe('Pushable', () => {
describe('execInteractive', () => {
it('sends start first with tty/cols/rows, streams output, and resolves done', async () => {
const cases: string[] = [];
let started: { tty?: boolean; cols?: number; rows?: number; sandboxId?: string } | undefined;
let started: (ScopedRequest & { tty?: boolean; cols?: number; rows?: number }) | undefined;
const sandbox = client({
getSandbox: () => readySandbox('sb', 'sb-id-9'),
execSandboxInteractive: async function* (requests) {
@@ -800,7 +825,7 @@ describe('execInteractive', () => {
expect(started?.tty).toBe(true);
expect(started?.cols).toBe(120);
expect(started?.rows).toBe(40);
expect(started?.sandboxId).toBe('sb-id-9');
expect(requestSandboxId(started ?? {})).toBe('sb-id-9');
expect(out.join('')).toContain('ready\n');
expect(out.join('')).toContain('echo hi');
});
@@ -864,7 +889,7 @@ describe('exec done settlement', () => {
describe('providers', () => {
it('attach/detach assemble the request and map the changed flag + sandbox ref', async () => {
let attachReq: {
sandboxName?: string;
sandboxRef?: ScopedRequest['sandboxRef'];
providerName?: string;
expectedResourceVersion?: bigint;
} = {};
@@ -884,7 +909,7 @@ describe('providers', () => {
});
const attach = await sandbox.attachProvider('sb', 'claude');
expect(attachReq.sandboxName).toBe('sb');
expect(requestSandboxName(attachReq)).toBe('sb');
expect(attachReq.providerName).toBe('claude');
expect(attachReq.expectedResourceVersion).toBe(0n);
expect(attach.changed).toBe(true);
@@ -961,7 +986,7 @@ describe('config / policy', () => {
it('setPolicy sends global=false + version pin and (wait) polls until the hash matches', async () => {
let updateReq: {
name?: string;
sandboxRef?: ScopedRequest['sandboxRef'];
global?: boolean;
expectedResourceVersion?: bigint;
policy?: unknown;
@@ -1002,7 +1027,7 @@ describe('config / policy', () => {
},
{ wait: true, expectedResourceVersion: '7' },
);
expect(updateReq.name).toBe('sb');
expect(requestSandboxName(updateReq)).toBe('sb');
expect(updateReq.global).toBe(false);
expect(updateReq.expectedResourceVersion).toBe(7n);
expect(updateReq.policy).toBeDefined();
@@ -1035,7 +1060,7 @@ describe('config / policy', () => {
it('setSetting upserts a single sandbox-scoped setting (global=false)', async () => {
let req: {
name?: string;
sandboxRef?: ScopedRequest['sandboxRef'];
settingKey?: string;
global?: boolean;
settingValue?: unknown;
@@ -1054,7 +1079,7 @@ describe('config / policy', () => {
const result = await sandbox.setSetting('sb', 'feature.enabled', {
value: { case: 'boolValue', value: true },
});
expect(req.name).toBe('sb');
expect(requestSandboxName(req)).toBe('sb');
expect(req.settingKey).toBe('feature.enabled');
expect(req.global).toBe(false);
expect(req.settingValue).toMatchObject({
@@ -1171,9 +1196,9 @@ describe('ssh sessions', () => {
describe('forward', () => {
it('binds a local port and relays bytes both ways, minting + revoking a token', async () => {
let sshReq: { sandboxId?: string } = {};
let sshReq: ScopedRequest = {};
let revokedToken: string | undefined;
let initFrame: { sandboxId?: string; authorizationToken?: string; target?: unknown } | undefined;
let initFrame: (ScopedRequest & { authorizationToken?: string; target?: unknown }) | undefined;
const sandbox = client({
getSandbox: () => readySandbox('sb', 'sb-id-forward'),
createSshSession: (req) => {
@@ -1224,8 +1249,8 @@ describe('forward', () => {
});
expect(echoed).toBe('ping-through-forward');
expect(sshReq.sandboxId).toBe('sb-id-forward');
expect(initFrame?.sandboxId).toBe('sb-id-forward');
expect(requestSandboxId(sshReq)).toBe('sb-id-forward');
expect(requestSandboxId(initFrame ?? {})).toBe('sb-id-forward');
expect(initFrame?.authorizationToken).toBe('fwd-tok');
expect(initFrame?.target).toMatchObject({
case: 'tcp',
@@ -1469,7 +1494,12 @@ describe('raw escape hatch', () => {
// raw returns the full generated message: the enum stays numeric, where the
// curated get() would lowercase status.phase to 'ready'.
const resp = await sandbox.raw.getSandbox({ name: 'sb' });
const resp = await sandbox.raw.getSandbox({
sandboxRef: {
identifier: { case: 'name', value: 'sb' },
workspaceScope: { selection: { case: 'workspace', value: 'default' } },
},
});
expect(resp.sandbox?.status?.phase).toBe(SandboxPhase.READY);
expect(resp.sandbox?.metadata?.name).toBe('sb');
+27 -18
View File
@@ -506,6 +506,17 @@ function listWorkspaceScope(options?: WorkspaceListScope | null): MessageInitSha
return options?.allWorkspaces ? { selection: { case: 'allWorkspaces', value: {} } } : workspaceScope(options);
}
function sandboxReferenceByName(name: string, options?: SandboxWorkspaceOptions | null) {
return {
identifier: { case: 'name' as const, value: name },
workspaceScope: workspaceScope(options),
};
}
function sandboxReferenceById(id: string) {
return { identifier: { case: 'id' as const, value: id } };
}
function requestCallOptions(options?: SandboxCallOptions | null): CallOptions | undefined {
if (!options) return undefined;
const { workspace: _workspace, ...callOptions } = options;
@@ -800,7 +811,7 @@ export class SandboxClient {
async get(name: string, options?: SandboxCallOptions | null): Promise<SandboxRef> {
try {
const resp = await this.grpc.getSandbox(
{ name, workspaceScope: workspaceScope(options) },
{ sandboxRef: sandboxReferenceByName(name, options) },
requestCallOptions(options),
);
return sandboxRef(resp.sandbox);
@@ -825,7 +836,7 @@ export class SandboxClient {
async delete(name: string, options?: SandboxWorkspaceOptions | null): Promise<boolean> {
try {
const resp = await this.grpc.deleteSandbox({ name, workspaceScope: workspaceScope(options) });
const resp = await this.grpc.deleteSandbox({ sandboxRef: sandboxReferenceByName(name, options) });
return resp.deleted;
} catch (e) {
throw fromConnect(e);
@@ -901,7 +912,7 @@ export class SandboxClient {
});
const stream = this.grpc.execSandbox(
{
sandboxId: sandbox.id,
sandboxRef: sandboxReferenceById(sandbox.id),
command,
workdir: options?.workdir ?? '',
environment: options?.environment ?? {},
@@ -984,7 +995,7 @@ export class SandboxClient {
payload: {
case: 'start',
value: {
sandboxId,
sandboxRef: sandboxReferenceById(sandboxId),
command,
workdir: options?.workdir ?? '',
environment: options?.environment ?? {},
@@ -1204,7 +1215,7 @@ export class SandboxClient {
const input = new Pushable<MessageInitShape<typeof TcpForwardFrameSchema>>();
input.onDrain = () => socket.resume();
try {
const session = await this.grpc.createSshSession({ sandboxId }, { signal });
const session = await this.grpc.createSshSession({ sandboxRef: sandboxReferenceById(sandboxId) }, { signal });
// Defense-in-depth: the token feeds forwardTcp authorization, so hold it
// to the same trust-boundary contract as createSshSession. A violation
// tears down this one socket via the catch below.
@@ -1214,7 +1225,7 @@ export class SandboxClient {
payload: {
case: 'init',
value: {
sandboxId,
sandboxRef: sandboxReferenceById(sandboxId),
serviceId: `service-forward:${name}:${targetHost}:${targetPort}`,
target: {
case: 'tcp',
@@ -1277,7 +1288,7 @@ export class SandboxClient {
async createSshSession(name: string, options?: SandboxWorkspaceOptions | null): Promise<SshSession> {
try {
const sandbox = await this.get(name, options);
const resp = await this.grpc.createSshSession({ sandboxId: sandbox.id });
const resp = await this.grpc.createSshSession({ sandboxRef: sandboxReferenceById(sandbox.id) });
// Reject any response outside the proto trust-boundary contract before
// handing these values to the caller (they feed OpenSSH ProxyCommand).
validateSshResponse(resp, sandbox.id);
@@ -1311,10 +1322,9 @@ export class SandboxClient {
): Promise<ProviderChange> {
try {
const resp = await this.grpc.attachSandboxProvider({
sandboxName: name,
sandboxRef: sandboxReferenceByName(name, options),
providerName: provider,
expectedResourceVersion: versionPin(options?.expectedResourceVersion),
workspaceScope: workspaceScope(options),
});
return { sandbox: sandboxRef(resp.sandbox), changed: resp.attached };
} catch (e) {
@@ -1329,10 +1339,9 @@ export class SandboxClient {
): Promise<ProviderChange> {
try {
const resp = await this.grpc.detachSandboxProvider({
sandboxName: name,
sandboxRef: sandboxReferenceByName(name, options),
providerName: provider,
expectedResourceVersion: versionPin(options?.expectedResourceVersion),
workspaceScope: workspaceScope(options),
});
return { sandbox: sandboxRef(resp.sandbox), changed: resp.detached };
} catch (e) {
@@ -1343,8 +1352,7 @@ export class SandboxClient {
async listProviders(name: string, options?: SandboxWorkspaceOptions | null): Promise<ProviderRef[]> {
try {
const resp = await this.grpc.listSandboxProviders({
sandboxName: name,
workspaceScope: workspaceScope(options),
sandboxRef: sandboxReferenceByName(name, options),
});
return resp.providers.map((p) => providerRef(p));
} catch (e) {
@@ -1355,7 +1363,10 @@ export class SandboxClient {
async getConfig(name: string, options?: SandboxCallOptions | null): Promise<SandboxConfig> {
try {
const sandbox = await this.get(name, options);
const resp = await this.grpc.getSandboxConfig({ sandboxId: sandbox.id }, requestCallOptions(options));
const resp = await this.grpc.getSandboxConfig(
{ sandboxRef: sandboxReferenceById(sandbox.id) },
requestCallOptions(options),
);
return sandboxConfig(resp);
} catch (e) {
throw e instanceof SdkError ? e : fromConnect(e);
@@ -1373,11 +1384,10 @@ export class SandboxClient {
): Promise<UpdateConfigResult> {
try {
const resp = await this.grpc.updateConfig({
name,
sandboxRef: sandboxReferenceByName(name, options),
policy,
global: false,
expectedResourceVersion: versionPin(options?.expectedResourceVersion),
workspaceScope: workspaceScope(options),
});
const result = updateConfigResult(resp);
if (options?.wait)
@@ -1398,11 +1408,10 @@ export class SandboxClient {
): Promise<UpdateConfigResult> {
try {
const resp = await this.grpc.updateConfig({
name,
sandboxRef: sandboxReferenceByName(name, options),
settingKey: key,
settingValue: value,
global: false,
workspaceScope: workspaceScope(options),
});
return updateConfigResult(resp);
} catch (e) {
+3
View File
@@ -798,6 +798,9 @@ openshell service get my-app web
openshell service delete my-app web
```
Use `openshell service list --all-workspaces` for a Platform Admin view across
workspaces. A sandbox name and `--all-workspaces` are mutually exclusive.
Prefer loopback binds unless the user explicitly needs LAN-visible local access.
---
+6
View File
@@ -56,6 +56,12 @@ LINE_REWRITES = {
"from . import datamodel_pb2 as datamodel__pb2",
),
],
"python/openshell/_proto/sandbox_pb2.py": [
(
r"^import datamodel_pb2 as datamodel__pb2$",
"from . import datamodel_pb2 as datamodel__pb2",
),
],
"python/openshell/_proto/sandbox_pb2_grpc.py": [
(
r"^import sandbox_pb2 as sandbox__pb2$",