Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions .agents/skills/tui-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,14 +469,20 @@ Proto types come from `openshell-core` which generates them from `OUT_DIR` via `

```rust
use openshell_core::proto::openshell_client::OpenShellClient;
use openshell_core::proto::{ListSandboxesRequest, GetSandboxLogsRequest, ...};
use openshell_core::proto::{
all_workspaces_selector, workspace_selector, GetSandboxLogsRequest,
ListSandboxesRequest, ...
};
```

### Proto field gotchas

- `DeleteSandboxRequest` uses the `name` field (not `id`):
```rust
let req = openshell_core::proto::DeleteSandboxRequest { name: sandbox_name };
let req = openshell_core::proto::DeleteSandboxRequest {
name: sandbox_name,
workspace_scope: Some(workspace_selector(workspace)),
};
```
- `WatchSandboxRequest` has extra fields beyond what you might need — always use `..Default::default()`:
```rust
Expand All @@ -490,12 +496,24 @@ use openshell_core::proto::{ListSandboxesRequest, GetSandboxLogsRequest, ...};
};
```
- `SandboxLogLine` proto fields: `sandbox_id`, `timestamp_ms`, `level`, `target`, `message`, `source`, `fields` (HashMap<String, String>).
- `GetSandboxLogsRequest` fields: `sandbox_id`, `lines` (u32), `since_ms` (i64), `sources` (Vec<String>), `min_level` (String), `workspace` (String).
- `ListSandboxesRequest` fields: `limit` (i64), `offset` (i64), `label_selector` (String), `workspace` (String), `all_workspaces` (bool).
- `ListProvidersRequest` fields: `limit` (i64), `offset` (i64), `workspace` (String), `all_workspaces` (bool).
- `ListWorkspacesRequest` fields: `limit` (i64), `offset` (i64), `label_selector` (String).
- `UpdateConfigRequest` fields: `name` (String, sandbox name or empty for global), `setting_key`, `setting_value`, `delete_setting` (bool), `global` (bool), `workspace`.
- Most resource requests include a `workspace` field that scopes the operation to the current workspace.
- Workspace-scoped request fields use `workspace_scope: Option<WorkspaceSelector>`.
Select one workspace with `Some(workspace_selector(name))`. List requests that
explicitly support cross-workspace access also accept
`Some(all_workspaces_selector())`; do not use that marker on other requests.
- `GetSandboxLogsRequest` fields: `sandbox_id`, `lines` (u32), `since_ms` (i64),
`sources` (Vec<String>), `min_level` (String), `workspace_scope`.
- `ListSandboxesRequest` fields: `limit` (u32), `offset` (u32),
`label_selector` (String), `workspace_scope`.
- `ListProvidersRequest` fields: `limit` (u32), `offset` (u32),
`workspace_scope`.
- `ListWorkspacesRequest` fields: `limit` (u32), `offset` (u32),
`label_selector` (String).
- `UpdateConfigRequest` fields include `name` (String, sandbox name or empty for
global), `setting_key`, `setting_value`, `delete_setting` (bool), `global`
(bool), and `workspace_scope`. Sandbox-scoped updates require a named selector;
gateway-global updates must leave `workspace_scope` as `None`.
- Most resource requests require an explicit named `workspace_scope`, including
the `default` workspace. An omitted selector is not an implicit default.

### gRPC timeouts

Expand Down
10 changes: 10 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ health, metrics, or tunnel routes. The plaintext service router also rejects
browser requests whose Fetch Metadata, Origin, or Referer headers indicate a
cross-origin or sibling-subdomain request.

Public workspace-scoped RPCs carry a typed `WorkspaceSelector`. A request must
select one non-empty workspace explicitly; `default` is an ordinary explicit
name, not an omitted-value fallback. Sandbox, sandbox template, provider, and
service list RPCs also accept an all-workspaces marker after Platform Admin
authorization. Single-workspace handlers reject that marker. Platform-global
policy operations require the selector to be absent, while workspace policy
operations require it. The gateway authorizes the selected scope before
performing resource lookup so malformed, unsupported, and unauthorized scopes
have consistent behavior across resource types.

Docker and Podman report the local address through which their sandboxes can
reach the gateway. When the primary listener covers that address, the gateway
reuses it; sandbox JWT authentication and its RPC allowlist remain the callback
Expand Down
50 changes: 24 additions & 26 deletions crates/openshell-cli/src/commands/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub async fn sandbox_provider_list(
let response = client
.list_sandbox_providers(ListSandboxProvidersRequest {
sandbox_name: name.to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
Expand Down Expand Up @@ -91,7 +91,7 @@ pub async fn sandbox_provider_attach(
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?
Expand All @@ -106,7 +106,7 @@ pub async fn sandbox_provider_attach(
sandbox_name: name.to_string(),
provider_name: provider.to_string(),
expected_resource_version: resource_version,
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand Down Expand Up @@ -147,7 +147,7 @@ pub async fn sandbox_provider_detach(
let sandbox = client
.get_sandbox(GetSandboxRequest {
name: name.to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?
Expand All @@ -162,7 +162,7 @@ pub async fn sandbox_provider_detach(
sandbox_name: name.to_string(),
provider_name: provider.to_string(),
expected_resource_version: resource_version,
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand Down Expand Up @@ -305,8 +305,7 @@ pub async fn ensure_required_providers(
.list_providers(ListProvidersRequest {
limit,
offset,
workspace: workspace.to_string(),
all_workspaces: false,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
Expand Down Expand Up @@ -490,7 +489,7 @@ async fn auto_create_provider(
profile_workspace: workspace.to_string(),
credential_handles: HashMap::new(),
}),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
};

let response = client.create_provider(request).await.map_err(|status| {
Expand Down Expand Up @@ -538,7 +537,7 @@ async fn auto_create_provider(
profile_workspace: workspace.to_string(),
credential_handles: HashMap::new(),
}),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
};

match client.create_provider(request).await {
Expand Down Expand Up @@ -672,7 +671,7 @@ async fn rollback_provider_create_after_gcloud_adc_failure(
match client
.delete_provider(DeleteProviderRequest {
name: provider_name.to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand Down Expand Up @@ -1119,7 +1118,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
profile_workspace: profile_workspace.to_string(),
credential_handles: HashMap::new(),
}),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
Expand Down Expand Up @@ -1149,7 +1148,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
"refresh_token".to_string(),
],
expires_at_ms: None,
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand All @@ -1167,7 +1166,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) ->
.rotate_provider_credential(RotateProviderCredentialRequest {
provider: provider_name.clone(),
credential_key: adc_credential_key,
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand Down Expand Up @@ -1208,7 +1207,7 @@ pub async fn provider_get(
let response = client
.get_provider(GetProviderRequest {
name: name.to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
Expand Down Expand Up @@ -1339,12 +1338,11 @@ pub async fn provider_list(
.list_providers(ListProvidersRequest {
limit,
offset,
workspace: if all_workspaces {
String::new()
workspace_scope: Some(if all_workspaces {
openshell_core::proto::all_workspaces_selector()
} else {
workspace.to_string()
},
all_workspaces,
openshell_core::proto::workspace_selector(workspace)
}),
})
.await
.into_diagnostic()?;
Expand Down Expand Up @@ -1713,7 +1711,7 @@ pub async fn provider_refresh_status(
.get_provider_refresh_status(GetProviderRefreshStatusRequest {
provider: name.to_string(),
credential_key: credential_key.unwrap_or_default().to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -1794,7 +1792,7 @@ pub async fn provider_refresh_config(
material,
secret_material_keys,
expires_at_ms: input.credential_expires_at_ms,
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -1823,7 +1821,7 @@ pub async fn provider_rotate(
.rotate_provider_credential(RotateProviderCredentialRequest {
provider: name.to_string(),
credential_key: credential_key.to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -1860,7 +1858,7 @@ pub async fn provider_refresh_delete(
.delete_provider_refresh(DeleteProviderRefreshRequest {
provider: name.to_string(),
credential_key: credential_key.to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -2236,7 +2234,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> {
let existing = match client
.get_provider(GetProviderRequest {
name: name.to_string(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand Down Expand Up @@ -2319,7 +2317,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> {
credential_handles: HashMap::new(),
}),
credential_expires_at_ms,
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
.into_diagnostic()?;
Expand Down Expand Up @@ -2349,7 +2347,7 @@ pub async fn provider_delete(
let response = match client
.delete_provider(DeleteProviderRequest {
name: name.clone(),
workspace: workspace.to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand Down
10 changes: 6 additions & 4 deletions crates/openshell-cli/src/completers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ pub fn complete_sandbox_names(_prefix: &OsStr) -> Vec<CompletionCandidate> {
limit: 200,
offset: 0,
label_selector: String::new(),
workspace: workspace_from_args(),
all_workspaces: false,
workspace_scope: Some(openshell_core::proto::workspace_selector(
workspace_from_args(),
)),
})
.await
.ok()?;
Expand All @@ -64,8 +65,9 @@ pub fn complete_provider_names(_prefix: &OsStr) -> Vec<CompletionCandidate> {
.list_providers(ListProvidersRequest {
limit: 200,
offset: 0,
workspace: workspace_from_args(),
all_workspaces: false,
workspace_scope: Some(openshell_core::proto::workspace_selector(
workspace_from_args(),
)),
})
.await
.ok()?;
Expand Down
Loading
Loading