Skip to content

Commit bd37630

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

101 files changed

Lines changed: 3915 additions & 4319 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
@@ -114,13 +114,20 @@ cross-origin or sibling-subdomain request.
114114

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

125132
Docker and Podman report the local address through which their sandboxes can
126133
reach the gateway. When the primary listener covers that address, the gateway
@@ -826,8 +833,8 @@ channel and returns the exec with an error. Once a command reports its exit
826833
status, the gateway also bounds how long it waits for the trailing channel close.
827834

828835
`ForwardTcp` is the client-facing byte stream for SSH and service forwarding.
829-
The first frame is a `TcpForwardInit` that carries the sandbox ID, an
830-
authorization token from `CreateSshSession`, and an explicit target:
836+
The first frame is a `TcpForwardInit` that carries the workspace-scoped sandbox
837+
name, an authorization token from `CreateSshSession`, and an explicit target:
831838
`target.ssh` for the sandbox SSH socket or `target.tcp` for a loopback service
832839
inside the sandbox. The gateway validates the token and sandbox readiness,
833840
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
@@ -92,8 +92,8 @@ pub async fn sandbox_provider_list(
9292
let mut client = grpc_client(server, tls).await?;
9393
let response = client
9494
.list_sandbox_providers(ListSandboxProvidersRequest {
95-
sandbox_name: name.to_string(),
96-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
95+
sandbox: (name).to_string(),
96+
workspace: (workspace).to_string(),
9797
})
9898
.await
9999
.into_diagnostic()?;
@@ -127,8 +127,8 @@ pub async fn sandbox_provider_attach(
127127
// Fetch current sandbox to get resource_version for CAS
128128
let sandbox = client
129129
.get_sandbox(GetSandboxRequest {
130-
name: name.to_string(),
131-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
130+
sandbox: (name).to_string(),
131+
workspace: (workspace).to_string(),
132132
})
133133
.await
134134
.map_err(|status| miette!("provider attachment lookup failed ({})", status.code()))?
@@ -141,10 +141,10 @@ pub async fn sandbox_provider_attach(
141141
let response = match client
142142
.attach_sandbox_provider(AttachSandboxProviderRequest {
143143
request_id: String::new(),
144-
sandbox_name: name.to_string(),
144+
sandbox: (name).to_string(),
145+
workspace: (workspace).to_string(),
145146
provider_name: provider.to_string(),
146147
expected_resource_version: resource_version,
147-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
148148
})
149149
.await
150150
{
@@ -198,8 +198,8 @@ pub async fn sandbox_provider_detach(
198198
// Fetch current sandbox to get resource_version for CAS
199199
let sandbox = client
200200
.get_sandbox(GetSandboxRequest {
201-
name: name.to_string(),
202-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
201+
sandbox: (name).to_string(),
202+
workspace: (workspace).to_string(),
203203
})
204204
.await
205205
.map_err(|status| miette!("provider detachment lookup failed ({})", status.code()))?
@@ -212,10 +212,10 @@ pub async fn sandbox_provider_detach(
212212
let response = match client
213213
.detach_sandbox_provider(DetachSandboxProviderRequest {
214214
request_id: String::new(),
215-
sandbox_name: name.to_string(),
215+
sandbox: (name).to_string(),
216+
workspace: (workspace).to_string(),
216217
provider_name: provider.to_string(),
217218
expected_resource_version: resource_version,
218-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
219219
})
220220
.await
221221
{
@@ -550,7 +550,7 @@ async fn auto_create_provider(
550550
profile_workspace: workspace.to_string(),
551551
credential_handles: HashMap::new(),
552552
}),
553-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
553+
workspace: (workspace).to_string(),
554554
};
555555

556556
let response = client.create_provider(request).await.map_err(|status| {
@@ -599,7 +599,7 @@ async fn auto_create_provider(
599599
profile_workspace: workspace.to_string(),
600600
credential_handles: HashMap::new(),
601601
}),
602-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
602+
workspace: (workspace).to_string(),
603603
};
604604

605605
match client.create_provider(request).await {
@@ -735,7 +735,7 @@ async fn rollback_provider_create_after_gcloud_adc_failure(
735735
request_id: String::new(),
736736
allow_missing: true,
737737
name: provider_name.to_string(),
738-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
738+
workspace: (workspace).to_string(),
739739
})
740740
.await
741741
{
@@ -1202,7 +1202,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
12021202
profile_workspace: profile_workspace.to_string(),
12031203
credential_handles: HashMap::new(),
12041204
}),
1205-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1205+
workspace: (workspace).to_string(),
12061206
})
12071207
.await
12081208
.into_diagnostic()?;
@@ -1233,7 +1233,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
12331233
"refresh_token".to_string(),
12341234
],
12351235
expiration_time: None,
1236-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1236+
workspace: (workspace).to_string(),
12371237
})
12381238
.await
12391239
{
@@ -1252,7 +1252,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
12521252
request_id: String::new(),
12531253
provider: provider_name.clone(),
12541254
credential_key: adc_credential_key,
1255-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1255+
workspace: (workspace).to_string(),
12561256
})
12571257
.await
12581258
{
@@ -1293,7 +1293,7 @@ pub async fn provider_get(
12931293
let response = client
12941294
.get_provider(GetProviderRequest {
12951295
name: name.to_string(),
1296-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1296+
workspace: (workspace).to_string(),
12971297
})
12981298
.await
12991299
.into_diagnostic()?;
@@ -1826,7 +1826,7 @@ pub async fn provider_refresh_status(
18261826
.get_provider_refresh_status(GetProviderRefreshStatusRequest {
18271827
provider: name.to_string(),
18281828
credential_key: credential_key.unwrap_or_default().to_string(),
1829-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1829+
workspace: (workspace).to_string(),
18301830
})
18311831
.await
18321832
.into_diagnostic()?
@@ -1912,7 +1912,7 @@ pub async fn provider_refresh_config(
19121912
.map(openshell_core::time::timestamp_from_millis)
19131913
.transpose()
19141914
.into_diagnostic()?,
1915-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1915+
workspace: (workspace).to_string(),
19161916
})
19171917
.await
19181918
.into_diagnostic()?
@@ -1942,7 +1942,7 @@ pub async fn provider_rotate(
19421942
request_id: String::new(),
19431943
provider: name.to_string(),
19441944
credential_key: credential_key.to_string(),
1945-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1945+
workspace: (workspace).to_string(),
19461946
})
19471947
.await
19481948
.into_diagnostic()?
@@ -1981,7 +1981,7 @@ pub async fn provider_refresh_delete(
19811981
allow_missing: true,
19821982
provider: name.to_string(),
19831983
credential_key: credential_key.to_string(),
1984-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
1984+
workspace: (workspace).to_string(),
19851985
})
19861986
.await
19871987
.into_diagnostic()?
@@ -2358,7 +2358,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> {
23582358
let existing = match client
23592359
.get_provider(GetProviderRequest {
23602360
name: name.to_string(),
2361-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
2361+
workspace: (workspace).to_string(),
23622362
})
23632363
.await
23642364
{
@@ -2454,7 +2454,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> {
24542454
})
24552455
.collect::<Result<HashMap<_, _>, _>>()
24562456
.into_diagnostic()?,
2457-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
2457+
workspace: (workspace).to_string(),
24582458
clear_credential_expiration_keys,
24592459
})
24602460
.await
@@ -2496,7 +2496,7 @@ pub async fn provider_delete(
24962496
request_id: String::new(),
24972497
allow_missing: true,
24982498
name: name.clone(),
2499-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
2499+
workspace: (workspace).to_string(),
25002500
})
25012501
.await
25022502
{

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ fn validate_mutation_receipts(
124124
|| !sandbox_ids.insert(desired.sandbox_id.as_str())
125125
|| expected
126126
.sandbox
127-
.is_some_and(|(name, id)| desired.sandbox_name != name || desired.sandbox_id != id)
127+
.is_some_and(|(name, id)| desired.sandbox != name || desired.sandbox_id != id)
128128
|| provider.is_some_and(|provider| {
129129
desired.provider_id != provider.id
130130
|| desired.provider_resource_version != provider.resource_version
@@ -274,10 +274,10 @@ pub async fn sandbox_provider_status(
274274
provider_status(
275275
&mut client,
276276
GetSandboxProviderStatusRequest {
277-
sandbox_name: name.to_string(),
277+
sandbox: name.to_string(),
278278
provider_name: provider.to_string(),
279279
receipt_id: receipt_id.to_string(),
280-
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
280+
workspace: workspace.to_string(),
281281
},
282282
),
283283
)
@@ -348,7 +348,7 @@ fn receipt_json(receipt: &ProviderMutationReceipt) -> serde_json::Value {
348348
let desired = receipt.desired.as_ref().map(|desired| {
349349
serde_json::json!({
350350
"sandbox_id": desired.sandbox_id,
351-
"sandbox_name": desired.sandbox_name,
351+
"sandbox": desired.sandbox,
352352
"attachment_epoch": desired.attachment_epoch,
353353
"provider_id": desired.provider_id,
354354
"provider_resource_version": desired.provider_resource_version.to_string(),
@@ -415,7 +415,7 @@ fn print_statuses(mutation_id: &str, results: &[DisplayStatus], output: &str) ->
415415
let sandbox = receipt
416416
.desired
417417
.as_ref()
418-
.map_or("unknown", |desired| desired.sandbox_name.as_str());
418+
.map_or("unknown", |desired| desired.sandbox.as_str());
419419
println!(
420420
"{} / {}: {} ({})",
421421
sandbox,
@@ -486,7 +486,7 @@ mod tests {
486486
persisted_time: Some(openshell_core::time::timestamp_from_millis(1).unwrap()),
487487
desired: Some(ProviderDesiredIdentity {
488488
sandbox_id: "sandbox-id".to_string(),
489-
sandbox_name: "sandbox".to_string(),
489+
sandbox: "sandbox".to_string(),
490490
provider_id: "provider-id".to_string(),
491491
..Default::default()
492492
}),

0 commit comments

Comments
 (0)