Skip to content

Commit eb653d0

Browse files
committed
feat(api)!: use sandbox names as canonical references
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 3dd5ce3 commit eb653d0

84 files changed

Lines changed: 2752 additions & 2860 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/tui-development/SKILL.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -479,10 +479,10 @@ use openshell_core::proto::{
479479

480480
### Proto field gotchas
481481

482-
- `DeleteSandboxRequest` uses the `name` field (not `id`):
482+
- `DeleteSandboxRequest` uses the canonical sandbox name and an explicit workspace:
483483
```rust
484484
let req = openshell_core::proto::DeleteSandboxRequest {
485-
name: sandbox_name,
485+
sandbox: sandbox_name,
486486
workspace_scope: Some(workspace_selector(workspace)),
487487
allow_missing: true,
488488
};
@@ -493,11 +493,12 @@ use openshell_core::proto::{
493493
- `WatchSandboxRequest` has extra fields beyond what you might need — always use `..Default::default()`:
494494
```rust
495495
let req = openshell_core::proto::WatchSandboxRequest {
496-
id: sandbox_id,
496+
sandbox: sandbox_name,
497497
follow_status: false,
498498
follow_logs: true,
499499
follow_events: false,
500500
log_tail_lines: 0,
501+
workspace_scope: Some(workspace_selector(workspace)),
501502
..Default::default()
502503
};
503504
```
@@ -506,7 +507,7 @@ use openshell_core::proto::{
506507
Select one workspace with `Some(workspace_selector(name))`. List requests that
507508
explicitly support cross-workspace access also accept
508509
`Some(all_workspaces_selector())`; do not use that marker on other requests.
509-
- `GetSandboxLogsRequest` fields: `sandbox_id`, `lines` (u32), `since_time` (`Option<prost_types::Timestamp>`),
510+
- `GetSandboxLogsRequest` fields: `sandbox`, `lines` (u32), `since_time` (`Option<prost_types::Timestamp>`),
510511
`sources` (Vec<String>), `min_level` (String), `workspace_scope`.
511512
- `ListSandboxesRequest` fields: `page_size` (i32), `page_token` (String),
512513
`label_selector` (String), `workspace_scope`.
@@ -517,10 +518,10 @@ use openshell_core::proto::{
517518
- Paginated list responses return `next_page_token`. Continue with the same
518519
request parameters and that token until it is empty; changing filters or
519520
scope invalidates the token.
520-
- `UpdateConfigRequest` fields include `name` (String, sandbox name or empty for
521-
global), `setting_key`, `setting_value`, `delete_setting` (bool), `global`
522-
(bool), and `workspace_scope`. Sandbox-scoped updates require a named selector;
523-
gateway-global updates must leave `workspace_scope` as `None`.
521+
- `UpdateConfigRequest` fields include `sandbox` (String, canonical sandbox name),
522+
`setting_key`, `setting_value`, `delete_setting` (bool), `global` (bool), and
523+
`workspace_scope`. Sandbox-scoped updates require `sandbox` and a named selector;
524+
gateway-global updates leave `sandbox` empty and `workspace_scope` as `None`.
524525
- Most resource requests require an explicit named `workspace_scope`, including
525526
the `default` workspace. An omitted selector is not an implicit default.
526527

‎architecture/gateway.md‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,20 @@ cross-origin or sibling-subdomain request.
110110

111111
Public workspace-scoped RPCs carry a typed `WorkspaceSelector`. A request must
112112
select one non-empty workspace explicitly; `default` is an ordinary explicit
113-
name, not an omitted-value fallback. Sandbox, sandbox template, provider, and
114-
service list RPCs also accept an all-workspaces marker after Platform Admin
115-
authorization. Single-workspace handlers reject that marker. Platform-global
116-
policy operations require the selector to be absent, while workspace policy
117-
operations require it. The gateway authorizes the selected scope before
118-
performing resource lookup so malformed, unsupported, and unauthorized scopes
119-
have consistent behavior across resource types.
113+
name, not an omitted-value fallback. Every public sandbox-scoped RPC identifies
114+
the sandbox with a string `sandbox` and carries its workspace selector as
115+
a separate request field. Canonical sandbox IDs remain internal metadata used
116+
at authentication, persistence, and compute-driver boundaries; public callers
117+
do not use them as sandbox references. The gateway resolves the name to the
118+
persisted sandbox record only after authorizing the selected workspace. A
119+
sandbox principal is instead resolved by the immutable ID in its authenticated
120+
identity, then checked against the requested name and workspace. Missing and
121+
unauthorized references use the same response within each principal class so
122+
the resolver does not expose an object-existence oracle. Sandbox, sandbox
123+
template, provider, and service list RPCs also accept an all-workspaces marker
124+
after Platform Admin authorization. Single-workspace handlers reject that
125+
marker. Platform-global policy operations require `sandbox` and
126+
`workspace_scope` to be absent, while sandbox policy operations require both.
120127

121128
Docker and Podman report the local address through which their sandboxes can
122129
reach the gateway. When the primary listener covers that address, the gateway

‎crates/openshell-cli/src/commands/provider.rs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pub async fn sandbox_provider_list(
6464
let mut client = grpc_client(server, tls).await?;
6565
let response = client
6666
.list_sandbox_providers(ListSandboxProvidersRequest {
67-
sandbox_name: name.to_string(),
67+
sandbox: (name).to_string(),
6868
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
6969
})
7070
.await
@@ -96,7 +96,7 @@ pub async fn sandbox_provider_attach(
9696
// Fetch current sandbox to get resource_version for CAS
9797
let sandbox = client
9898
.get_sandbox(GetSandboxRequest {
99-
name: name.to_string(),
99+
sandbox: (name).to_string(),
100100
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
101101
})
102102
.await
@@ -110,10 +110,10 @@ pub async fn sandbox_provider_attach(
110110
let response = match client
111111
.attach_sandbox_provider(AttachSandboxProviderRequest {
112112
request_id: String::new(),
113-
sandbox_name: name.to_string(),
113+
sandbox: (name).to_string(),
114+
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
114115
provider_name: provider.to_string(),
115116
expected_resource_version: resource_version,
116-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
117117
})
118118
.await
119119
{
@@ -153,7 +153,7 @@ pub async fn sandbox_provider_detach(
153153
// Fetch current sandbox to get resource_version for CAS
154154
let sandbox = client
155155
.get_sandbox(GetSandboxRequest {
156-
name: name.to_string(),
156+
sandbox: (name).to_string(),
157157
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
158158
})
159159
.await
@@ -167,10 +167,10 @@ pub async fn sandbox_provider_detach(
167167
let response = match client
168168
.detach_sandbox_provider(DetachSandboxProviderRequest {
169169
request_id: String::new(),
170-
sandbox_name: name.to_string(),
170+
sandbox: (name).to_string(),
171+
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
171172
provider_name: provider.to_string(),
172173
expected_resource_version: resource_version,
173-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
174174
})
175175
.await
176176
{

‎crates/openshell-cli/src/main.rs‎

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ enum Commands {
644644
/// Two mutually exclusive modes:
645645
///
646646
/// **Token mode** (used internally by `sandbox connect`):
647-
/// `openshell ssh-proxy --gateway <url> --sandbox-id <id> --token <token>`
647+
/// `openshell ssh-proxy --gateway <url> --sandbox <name> --token <token>`
648648
///
649649
/// **Name mode** (for use in `~/.ssh/config`):
650650
/// `openshell ssh-proxy --gateway <name> --name <sandbox-name>`
@@ -655,9 +655,9 @@ enum Commands {
655655
#[arg(long, short = 'g')]
656656
gateway: Option<String>,
657657

658-
/// Sandbox id. Required in token mode.
658+
/// Sandbox name. Required in token mode.
659659
#[arg(long)]
660-
sandbox_id: Option<String>,
660+
sandbox: Option<String>,
661661

662662
/// SSH session token. Required in token mode.
663663
#[arg(long)]
@@ -2141,7 +2141,7 @@ enum ServiceCommands {
21412141
page_token: String,
21422142

21432143
/// List services across all workspaces (overrides --workspace).
2144-
#[arg(long)]
2144+
#[arg(long, conflicts_with = "sandbox")]
21452145
all_workspaces: bool,
21462146

21472147
/// Output format.
@@ -3785,23 +3785,24 @@ async fn run_async() -> Result<()> {
37853785
}
37863786
Some(Commands::SshProxy {
37873787
gateway,
3788-
sandbox_id,
3788+
sandbox,
37893789
token,
37903790
server,
37913791
gateway_name,
37923792
name,
37933793
}) => {
3794-
match (gateway, sandbox_id, token, server, gateway_name, name) {
3794+
match (gateway, sandbox, token, server, gateway_name, name) {
37953795
// Token mode (existing behavior): pre-created session credentials.
3796-
(Some(gw), Some(sid), Some(tok), _, gateway_name_opt, _) => {
3796+
(Some(gw), Some(sandbox), Some(tok), _, gateway_name_opt, _) => {
37973797
let mut effective_tls = match gateway_name_opt {
37983798
Some(ref g) => tls.with_gateway_name(g),
37993799
None => tls,
38003800
};
38013801
if let Some(ref g) = gateway_name_opt {
38023802
apply_auth(&mut effective_tls, g)?;
38033803
}
3804-
run::sandbox_ssh_proxy(&gw, &sid, &tok, &effective_tls).await?;
3804+
run::sandbox_ssh_proxy(&gw, &sandbox, &cli.workspace, &tok, &effective_tls)
3805+
.await?;
38053806
}
38063807
// Name mode with --gateway-name: resolve endpoint from metadata.
38073808
(_, _, _, server_override, Some(g), Some(n)) => {
@@ -3827,7 +3828,7 @@ async fn run_async() -> Result<()> {
38273828
}
38283829
_ => {
38293830
return Err(miette::miette!(
3830-
"provide either --gateway/--sandbox-id/--token or --gateway-name/--name (or --server/--name)"
3831+
"provide either --gateway/--sandbox/--token or --gateway-name/--name (or --server/--name)"
38313832
));
38323833
}
38333834
}
@@ -4477,19 +4478,22 @@ mod tests {
44774478
"ssh-proxy",
44784479
"--gateway",
44794480
"https://gw.example.com:8080/proxy/connect",
4480-
"--sandbox-id",
4481-
"sbx-123",
4481+
"--sandbox",
4482+
"my-box",
4483+
"--workspace",
4484+
"team-a",
44824485
"--token",
44834486
"tok-abc",
44844487
"--gateway-name",
44854488
"my-gateway",
44864489
])
44874490
.expect("TUI proxy command flags must be accepted by the CLI");
4491+
assert_eq!(cli.workspace, "team-a");
44884492

44894493
match cli.command {
44904494
Some(Commands::SshProxy {
44914495
gateway,
4492-
sandbox_id,
4496+
sandbox,
44934497
token,
44944498
gateway_name,
44954499
..
@@ -4499,7 +4503,7 @@ mod tests {
44994503
Some("https://gw.example.com:8080/proxy/connect"),
45004504
"gateway URL must land in SshProxy.gateway, not the global flag"
45014505
);
4502-
assert_eq!(sandbox_id.as_deref(), Some("sbx-123"));
4506+
assert_eq!(sandbox.as_deref(), Some("my-box"));
45034507
assert_eq!(token.as_deref(), Some("tok-abc"));
45044508
assert_eq!(gateway_name.as_deref(), Some("my-gateway"));
45054509
}
@@ -6091,6 +6095,19 @@ mod tests {
60916095
}
60926096
}
60936097

6098+
#[test]
6099+
fn service_list_rejects_sandbox_with_all_workspaces() {
6100+
let result = Cli::try_parse_from([
6101+
"openshell",
6102+
"service",
6103+
"list",
6104+
"my-sandbox",
6105+
"--all-workspaces",
6106+
]);
6107+
6108+
assert!(result.is_err());
6109+
}
6110+
60946111
#[test]
60956112
fn service_get_accepts_optional_service_name() {
60966113
let cli = Cli::try_parse_from(["openshell", "service", "get", "my-sandbox", "api"])

0 commit comments

Comments
 (0)