Skip to content
Draft
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
21 changes: 13 additions & 8 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,18 @@ 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. Every public sandbox-scoped RPC identifies
the sandbox with a string `sandbox` and carries its workspace selector as
a separate request field. Canonical sandbox IDs remain internal metadata used
at authentication, persistence, and compute-driver boundaries; public callers
do not use them as sandbox references. The gateway resolves the name to the
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that the workspace, sandbox name are the unique identifier for the sandbox? (If I recall correctly, I saw an issue where users in different workspaces can delete / access a sandbox if they create one with the same name. Does this solve that?).

after Platform Admin authorization. Single-workspace handlers reject that
marker. Platform-global policy operations require `sandbox` and
`workspace_scope` to be absent, while sandbox policy operations require both.

Docker and Podman report the local address through which their sandboxes can
reach the gateway. When the primary listener covers that address, the gateway
Expand Down Expand Up @@ -329,7 +334,7 @@ Compute-driver, credential-driver, gateway-interceptor, and
supervisor-middleware services are compiled contracts for internal extension
boundaries, not public gateway RPCs. The current public inventory has 74
methods, 278 messages, and 12 enums
(`0f14943574349d02bdc61076c8c5a59a98b627325564ef1a6d21d7941825dc46`).
(`6c803d61db1b667d78a6fd7e781316e681bf9093eaef7010f07cb3b9aa672ccb`).

Storage-only messages live in the private, versioned
`openshell.storage.v1` package under `crates/openshell-server/proto`. The server
Expand Down
14 changes: 7 additions & 7 deletions crates/openshell-cli/src/commands/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ 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(),
sandbox: (name).to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
Expand Down Expand Up @@ -90,7 +90,7 @@ 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(),
sandbox: (name).to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
Expand All @@ -103,10 +103,10 @@ pub async fn sandbox_provider_attach(

let response = match client
.attach_sandbox_provider(AttachSandboxProviderRequest {
sandbox_name: name.to_string(),
sandbox: (name).to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
provider_name: provider.to_string(),
expected_resource_version: resource_version,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand Down Expand Up @@ -146,7 +146,7 @@ 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(),
sandbox: (name).to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
Expand All @@ -159,10 +159,10 @@ pub async fn sandbox_provider_detach(

let response = match client
.detach_sandbox_provider(DetachSandboxProviderRequest {
sandbox_name: name.to_string(),
sandbox: (name).to_string(),
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
provider_name: provider.to_string(),
expected_resource_version: resource_version,
workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)),
})
.await
{
Expand Down
39 changes: 26 additions & 13 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ enum Commands {
/// Two mutually exclusive modes:
///
/// **Token mode** (used internally by `sandbox connect`):
/// `openshell ssh-proxy --gateway <url> --sandbox-id <id> --token <token>`
/// `openshell ssh-proxy --gateway <url> --sandbox <name> --token <token>`
///
/// **Name mode** (for use in `~/.ssh/config`):
/// `openshell ssh-proxy --gateway <name> --name <sandbox-name>`
Expand All @@ -655,9 +655,9 @@ enum Commands {
#[arg(long, short = 'g')]
gateway: Option<String>,

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

/// SSH session token. Required in token mode.
#[arg(long)]
Expand Down Expand Up @@ -2141,7 +2141,7 @@ enum ServiceCommands {
page_token: String,

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

/// Output format.
Expand Down Expand Up @@ -3785,23 +3785,23 @@ async fn run_async() -> Result<()> {
}
Some(Commands::SshProxy {
gateway,
sandbox_id,
sandbox,
token,
server,
gateway_name,
name,
}) => {
match (gateway, sandbox_id, token, server, gateway_name, name) {
match (gateway, sandbox, token, server, gateway_name, name) {
// Token mode (existing behavior): pre-created session credentials.
(Some(gw), Some(sid), Some(tok), _, gateway_name_opt, _) => {
(Some(gw), Some(sandbox), Some(tok), _, gateway_name_opt, _) => {
let mut effective_tls = match gateway_name_opt {
Some(ref g) => tls.with_gateway_name(g),
None => tls,
};
if let Some(ref g) = gateway_name_opt {
apply_auth(&mut effective_tls, g)?;
}
run::sandbox_ssh_proxy(&gw, &sid, &tok, &effective_tls).await?;
run::sandbox_ssh_proxy(&gw, &sandbox, &tok, &effective_tls).await?;
}
// Name mode with --gateway-name: resolve endpoint from metadata.
(_, _, _, server_override, Some(g), Some(n)) => {
Expand All @@ -3827,7 +3827,7 @@ async fn run_async() -> Result<()> {
}
_ => {
return Err(miette::miette!(
"provide either --gateway/--sandbox-id/--token or --gateway-name/--name (or --server/--name)"
"provide either --gateway/--sandbox/--token or --gateway-name/--name (or --server/--name)"
));
}
}
Expand Down Expand Up @@ -4477,8 +4477,8 @@ mod tests {
"ssh-proxy",
"--gateway",
"https://gw.example.com:8080/proxy/connect",
"--sandbox-id",
"sbx-123",
"--sandbox",
"my-box",
"--token",
"tok-abc",
"--gateway-name",
Expand All @@ -4489,7 +4489,7 @@ mod tests {
match cli.command {
Some(Commands::SshProxy {
gateway,
sandbox_id,
sandbox,
token,
gateway_name,
..
Expand All @@ -4499,7 +4499,7 @@ mod tests {
Some("https://gw.example.com:8080/proxy/connect"),
"gateway URL must land in SshProxy.gateway, not the global flag"
);
assert_eq!(sandbox_id.as_deref(), Some("sbx-123"));
assert_eq!(sandbox.as_deref(), Some("my-box"));
assert_eq!(token.as_deref(), Some("tok-abc"));
assert_eq!(gateway_name.as_deref(), Some("my-gateway"));
}
Expand Down Expand Up @@ -6066,6 +6066,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"])
Expand Down
Loading
Loading