Skip to content

Commit 90c5d02

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

96 files changed

Lines changed: 3768 additions & 4127 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: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -479,12 +479,13 @@ 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,
486-
workspace_scope: Some(workspace_selector(workspace)),
485+
sandbox: sandbox_name,
486+
workspace: workspace.to_string(),
487487
allow_missing: true,
488+
..Default::default()
488489
};
489490
```
490491
- Delete responses carry `DeletionOutcome`: distinguish `Accepted` (cleanup
@@ -493,21 +494,23 @@ use openshell_core::proto::{
493494
- `WatchSandboxRequest` has extra fields beyond what you might need — always use `..Default::default()`:
494495
```rust
495496
let req = openshell_core::proto::WatchSandboxRequest {
496-
id: sandbox_id,
497+
sandbox: sandbox_name,
497498
follow_status: false,
498499
follow_logs: true,
499500
follow_events: false,
500501
log_tail_lines: 0,
502+
workspace: workspace.to_string(),
501503
..Default::default()
502504
};
503505
```
504506
- `SandboxLogLine` proto fields: `sandbox_id`, `event_time` (`Option<prost_types::Timestamp>`), `level`, `target`, `message`, `source`, `fields` (`HashMap<String, String>`).
505-
- Workspace-scoped request fields use `workspace_scope: Option<WorkspaceSelector>`.
506-
Select one workspace with `Some(workspace_selector(name))`. List requests that
507-
explicitly support cross-workspace access also accept
508-
`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-
`sources` (Vec<String>), `min_level` (String), `workspace_scope`.
507+
- Single-resource requests use a `workspace: String` field with the canonical
508+
workspace name. Collection list requests use
509+
`workspace_scope: Option<WorkspaceSelector>` so callers can select one
510+
workspace with `Some(workspace_selector(name))` or, where authorized,
511+
`Some(all_workspaces_selector())`.
512+
- `GetSandboxLogsRequest` fields: `sandbox`, `lines` (u32), `since_time` (`Option<prost_types::Timestamp>`),
513+
`sources` (Vec<String>), `min_level` (String), `workspace`.
511514
- `ListSandboxesRequest` fields: `page_size` (i32), `page_token` (String),
512515
`label_selector` (String), `workspace_scope`.
513516
- `ListProvidersRequest` fields: `page_size` (i32), `page_token` (String),
@@ -517,12 +520,12 @@ use openshell_core::proto::{
517520
- Paginated list responses return `next_page_token`. Continue with the same
518521
request parameters and that token until it is empty; changing filters or
519522
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`.
524-
- Most resource requests require an explicit named `workspace_scope`, including
525-
the `default` workspace. An omitted selector is not an implicit default.
523+
- `UpdateConfigRequest` fields include `sandbox` (String, canonical sandbox name),
524+
`setting_key`, `setting_value`, `delete_setting` (bool), `global` (bool), and
525+
`workspace`. Sandbox-scoped updates require canonical `sandbox` and `workspace`
526+
names; gateway-global updates leave both fields empty.
527+
- Most single-resource requests require an explicit canonical `workspace` name,
528+
including the `default` workspace. An empty value is not an implicit default.
526529

527530
### gRPC timeouts
528531

‎architecture/gateway.md‎

Lines changed: 16 additions & 9 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 canonical workspace name in
115+
a separate string `workspace` 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 collection list RPCs use `workspace_scope` so
124+
they can also accept an all-workspaces marker after Platform Admin
125+
authorization. Platform-global policy operations require `sandbox` and
126+
`workspace` to be empty, 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
@@ -793,8 +800,8 @@ channel and returns the exec with an error. Once a command reports its exit
793800
status, the gateway also bounds how long it waits for the trailing channel close.
794801

795802
`ForwardTcp` is the client-facing byte stream for SSH and service forwarding.
796-
The first frame is a `TcpForwardInit` that carries the sandbox ID, an
797-
authorization token from `CreateSshSession`, and an explicit target:
803+
The first frame is a `TcpForwardInit` that carries the workspace-scoped sandbox
804+
name, an authorization token from `CreateSshSession`, and an explicit target:
798805
`target.ssh` for the sandbox SSH socket or `target.tcp` for a loopback service
799806
inside the sandbox. The gateway validates the token and sandbox readiness,
800807
sends a targeted `RelayOpen` to the supervisor, then bridges

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

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ 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(),
68-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
67+
sandbox: (name).to_string(),
68+
workspace: (workspace).to_string(),
6969
})
7070
.await
7171
.into_diagnostic()?;
@@ -96,8 +96,8 @@ 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(),
100-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
99+
sandbox: (name).to_string(),
100+
workspace: (workspace).to_string(),
101101
})
102102
.await
103103
.into_diagnostic()?
@@ -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: (workspace).to_string(),
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,8 +153,8 @@ 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(),
157-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
156+
sandbox: (name).to_string(),
157+
workspace: (workspace).to_string(),
158158
})
159159
.await
160160
.into_diagnostic()?
@@ -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: (workspace).to_string(),
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
{
@@ -498,7 +498,7 @@ async fn auto_create_provider(
498498
profile_workspace: workspace.to_string(),
499499
credential_handles: HashMap::new(),
500500
}),
501-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
501+
workspace: (workspace).to_string(),
502502
};
503503

504504
let response = client.create_provider(request).await.map_err(|status| {
@@ -547,7 +547,7 @@ async fn auto_create_provider(
547547
profile_workspace: workspace.to_string(),
548548
credential_handles: HashMap::new(),
549549
}),
550-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
550+
workspace: (workspace).to_string(),
551551
};
552552

553553
match client.create_provider(request).await {
@@ -683,7 +683,7 @@ async fn rollback_provider_create_after_gcloud_adc_failure(
683683
request_id: String::new(),
684684
allow_missing: true,
685685
name: provider_name.to_string(),
686-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
686+
workspace: (workspace).to_string(),
687687
})
688688
.await
689689
{
@@ -1138,7 +1138,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
11381138
profile_workspace: profile_workspace.to_string(),
11391139
credential_handles: HashMap::new(),
11401140
}),
1141-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1141+
workspace: (workspace).to_string(),
11421142
})
11431143
.await
11441144
.into_diagnostic()?;
@@ -1169,7 +1169,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
11691169
"refresh_token".to_string(),
11701170
],
11711171
expiration_time: None,
1172-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1172+
workspace: (workspace).to_string(),
11731173
})
11741174
.await
11751175
{
@@ -1188,7 +1188,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
11881188
request_id: String::new(),
11891189
provider: provider_name.clone(),
11901190
credential_key: adc_credential_key,
1191-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1191+
workspace: (workspace).to_string(),
11921192
})
11931193
.await
11941194
{
@@ -1229,7 +1229,7 @@ pub async fn provider_get(
12291229
let response = client
12301230
.get_provider(GetProviderRequest {
12311231
name: name.to_string(),
1232-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1232+
workspace: (workspace).to_string(),
12331233
})
12341234
.await
12351235
.into_diagnostic()?;
@@ -1762,7 +1762,7 @@ pub async fn provider_refresh_status(
17621762
.get_provider_refresh_status(GetProviderRefreshStatusRequest {
17631763
provider: name.to_string(),
17641764
credential_key: credential_key.unwrap_or_default().to_string(),
1765-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1765+
workspace: (workspace).to_string(),
17661766
})
17671767
.await
17681768
.into_diagnostic()?
@@ -1848,7 +1848,7 @@ pub async fn provider_refresh_config(
18481848
.map(openshell_core::time::timestamp_from_millis)
18491849
.transpose()
18501850
.into_diagnostic()?,
1851-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1851+
workspace: (workspace).to_string(),
18521852
})
18531853
.await
18541854
.into_diagnostic()?
@@ -1878,7 +1878,7 @@ pub async fn provider_rotate(
18781878
request_id: String::new(),
18791879
provider: name.to_string(),
18801880
credential_key: credential_key.to_string(),
1881-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1881+
workspace: (workspace).to_string(),
18821882
})
18831883
.await
18841884
.into_diagnostic()?
@@ -1917,7 +1917,7 @@ pub async fn provider_refresh_delete(
19171917
allow_missing: true,
19181918
provider: name.to_string(),
19191919
credential_key: credential_key.to_string(),
1920-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1920+
workspace: (workspace).to_string(),
19211921
})
19221922
.await
19231923
.into_diagnostic()?
@@ -2288,7 +2288,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> {
22882288
let existing = match client
22892289
.get_provider(GetProviderRequest {
22902290
name: name.to_string(),
2291-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
2291+
workspace: (workspace).to_string(),
22922292
})
22932293
.await
22942294
{
@@ -2384,7 +2384,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> {
23842384
})
23852385
.collect::<Result<HashMap<_, _>, _>>()
23862386
.into_diagnostic()?,
2387-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
2387+
workspace: (workspace).to_string(),
23882388
clear_credential_expiration_keys,
23892389
})
23902390
.await
@@ -2417,7 +2417,7 @@ pub async fn provider_delete(
24172417
request_id: String::new(),
24182418
allow_missing: true,
24192419
name: name.clone(),
2420-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
2420+
workspace: (workspace).to_string(),
24212421
})
24222422
.await
24232423
{

0 commit comments

Comments
 (0)