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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Build output
/target/
e2e/rust/target/
target/
debug/
release/

Expand Down
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ Operators can configure a gateway-wide gRPC request rate limit. The limit is
applied only to gRPC API traffic after protocol multiplexing; health, metrics,
and local sandbox-service HTTP routes are not rate limited by this control.

Gateway interceptors run in one middleware layer on the `openshell.v1.OpenShell`
gRPC service after authentication and before tonic dispatches to individual
handlers. At startup the gateway calls each configured interceptor's `Describe`
RPC, validates declared bindings against the compiled OpenShell descriptor set,
and builds an immutable execution plan. Unary OpenShell requests that are not
streaming, supervisor-facing, read-only, or introspection methods are decoded
through the descriptor set into protobuf JSON, evaluated through configured
phases, and re-encoded before the handler sees the request. This keeps
interception centralized: adding an interceptable unary RPC does not require
method-specific gateway instrumentation.

Interceptor manifests can also vend provider profile catalogs. The gateway
always starts with the in-tree built-in catalog source, then merges any
interceptor-declared sources. An authoritative interceptor catalog becomes the
visible provider profile source of truth for that gateway and hides built-in
and user-imported profiles from profile resolution, while append catalogs add
static profiles alongside the built-in/user catalog.

Supported auth modes:

| Mode | Use |
Expand Down Expand Up @@ -220,7 +238,7 @@ modes:
write. Client-facing operations that carry an `expected_resource_version`
field use this mode: `AttachSandboxProvider`, `DetachSandboxProvider`,
`UpdateProvider`, `UpdateProviderProfiles`, and `UpdateConfig` (policy
backfill path).
backfill and sandbox annotation updates).

**Lists.** The `list_messages` and `list_messages_with_selector` helpers decode
protobuf payloads from list results and hydrate `resource_version` from the
Expand Down
79 changes: 35 additions & 44 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1928,6 +1928,7 @@ pub async fn sandbox_create(
}),
name: name.unwrap_or_default().to_string(),
labels,
annotations: HashMap::new(),
};

let response = match client.create_sandbox(request).await {
Expand Down Expand Up @@ -1970,13 +1971,9 @@ pub async fn sandbox_create(
match client
.update_config(UpdateConfigRequest {
name: sandbox_name.clone(),
policy: None,
setting_key: settings::PROPOSAL_APPROVAL_MODE_KEY.to_string(),
setting_value: Some(setting),
delete_setting: false,
global: false,
merge_operations: vec![],
expected_resource_version: 0,
..Default::default()
})
.await
{
Expand Down Expand Up @@ -2683,6 +2680,17 @@ pub async fn sandbox_get(
}
}

if let Some(metadata) = &sandbox.metadata
&& !metadata.annotations.is_empty()
{
println!(" {} ", "Annotations:".dimmed());
let mut annotations: Vec<_> = metadata.annotations.iter().collect();
annotations.sort_by_key(|(k, _)| *k);
for (key, value) in annotations {
println!(" {key}: {value}");
}
}

let policy_from_global = config.policy_source == PolicySource::Global as i32;
println!(
" {} {}",
Expand Down Expand Up @@ -3427,10 +3435,15 @@ pub async fn sandbox_list(
fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {
let meta = sandbox.metadata.as_ref();
let labels = meta.map_or_else(|| serde_json::json!({}), |m| serde_json::json!(m.labels));
let annotations = meta.map_or_else(
|| serde_json::json!({}),
|m| serde_json::json!(m.annotations),
);
serde_json::json!({
"id": sandbox.object_id(),
"name": sandbox.object_name(),
"labels": labels,
"annotations": annotations,
"resource_version": meta.map_or(0, |m| m.resource_version),
"created_at": format_epoch_ms(meta.map_or(0, |m| m.created_at_ms)),
"phase": phase_name(sandbox.phase()),
Expand Down Expand Up @@ -3883,6 +3896,7 @@ async fn auto_create_provider(
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: provider_type.to_string(),
credentials: discovered.credentials.clone(),
Expand Down Expand Up @@ -3925,6 +3939,7 @@ async fn auto_create_provider(
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: provider_type.to_string(),
credentials: discovered.credentials.clone(),
Expand Down Expand Up @@ -4670,13 +4685,14 @@ pub async fn provider_create_with_options(
};

let adc_credential_key = if from_gcloud_adc {
let profile =
openshell_providers::get_default_profile(&provider_type).ok_or_else(|| {
let profile = fetch_provider_profile(&mut client, &provider_type)
.await
.map_err(|err| {
miette::miette!(
"--from-gcloud-adc requires a built-in provider profile, \
but '{provider_type}' has none"
"--from-gcloud-adc is not supported for '{provider_type}' providers ({err})"
)
})?;
let profile = ProviderTypeProfile::from_proto(&profile);
let adc_cred = profile.adc_credential().ok_or_else(|| {
miette::miette!(
"--from-gcloud-adc is not supported for '{provider_type}' providers \
Expand Down Expand Up @@ -4764,6 +4780,7 @@ pub async fn provider_create_with_options(
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: provider_type.clone(),
credentials: credential_map,
Expand Down Expand Up @@ -5695,6 +5712,7 @@ pub async fn provider_update(
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: String::new(),
credentials: credential_map,
Expand Down Expand Up @@ -6306,12 +6324,8 @@ pub async fn sandbox_policy_set_global(
.update_config(UpdateConfigRequest {
name: String::new(),
policy: Some(policy),
setting_key: String::new(),
setting_value: None,
delete_setting: false,
global: true,
merge_operations: vec![],
expected_resource_version: 0,
..Default::default()
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -6504,13 +6518,10 @@ pub async fn gateway_setting_set(
let response = client
.update_config(UpdateConfigRequest {
name: String::new(),
policy: None,
setting_key: key.to_string(),
setting_value: Some(setting_value),
delete_setting: false,
global: true,
merge_operations: vec![],
expected_resource_version: 0,
..Default::default()
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -6539,13 +6550,9 @@ pub async fn sandbox_setting_set(
let response = client
.update_config(UpdateConfigRequest {
name: name.to_string(),
policy: None,
setting_key: key.to_string(),
setting_value: Some(setting_value),
delete_setting: false,
global: false,
merge_operations: vec![],
expected_resource_version: 0,
..Default::default()
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -6574,13 +6581,10 @@ pub async fn gateway_setting_delete(
let response = client
.update_config(UpdateConfigRequest {
name: String::new(),
policy: None,
setting_key: key.to_string(),
setting_value: None,
delete_setting: true,
global: true,
merge_operations: vec![],
expected_resource_version: 0,
..Default::default()
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -6609,13 +6613,9 @@ pub async fn sandbox_setting_delete(
let response = client
.update_config(UpdateConfigRequest {
name: name.to_string(),
policy: None,
setting_key: key.to_string(),
setting_value: None,
delete_setting: true,
global: false,
merge_operations: vec![],
expected_resource_version: 0,
..Default::default()
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -6669,12 +6669,7 @@ pub async fn sandbox_policy_set(
.update_config(UpdateConfigRequest {
name: name.to_string(),
policy: Some(policy),
setting_key: String::new(),
setting_value: None,
delete_setting: false,
global: false,
merge_operations: vec![],
expected_resource_version: 0,
..Default::default()
})
.await
.into_diagnostic()?;
Expand Down Expand Up @@ -6843,13 +6838,8 @@ pub async fn sandbox_policy_update(
let response = client
.update_config(UpdateConfigRequest {
name: name.to_string(),
policy: None,
setting_key: String::new(),
setting_value: None,
delete_setting: false,
global: false,
merge_operations: plan.merge_operations,
expected_resource_version: 0,
..Default::default()
})
.await
.into_diagnostic()?
Expand Down Expand Up @@ -9600,6 +9590,7 @@ mod tests {
resource_version: 42,
created_at_ms: 1_234_567_890_000,
labels,
annotations: std::collections::HashMap::new(),
};

let provider = Provider {
Expand Down
2 changes: 2 additions & 0 deletions crates/openshell-cli/tests/ensure_providers_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ impl TestOpenShell {
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: provider_type.to_string(),
credentials: HashMap::new(),
Expand Down Expand Up @@ -349,6 +350,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: existing_metadata.created_at_ms,
labels: existing_metadata.labels,
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: existing.r#type,
credentials: merge(existing.credentials, provider.credentials),
Expand Down
41 changes: 24 additions & 17 deletions crates/openshell-cli/tests/provider_commands_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: 0,
labels: HashMap::new(),
resource_version: 1,
annotations: HashMap::new(),
}),
spec: None,
status: None,
Expand Down Expand Up @@ -341,21 +342,23 @@ impl OpenShell for TestOpenShell {
.provider
.ok_or_else(|| Status::invalid_argument("provider is required"))?;
if provider.credentials.is_empty() {
let bootstrap_allowed =
if let Some(profile) = openshell_providers::get_default_profile(&provider.r#type) {
profile.allows_empty_provider_credentials()
} else {
self.state
.profiles
.lock()
.await
.get(&provider.r#type)
.cloned()
.is_some_and(|profile| {
openshell_providers::ProviderTypeProfile::from_proto(&profile)
.allows_empty_provider_credentials()
})
};
let bootstrap_allowed = if let Some(profile) = openshell_providers::builtin_profiles()
.iter()
.find(|profile| profile.id == provider.r#type)
{
profile.allows_empty_provider_credentials()
} else {
self.state
.profiles
.lock()
.await
.get(&provider.r#type)
.cloned()
.is_some_and(|profile| {
openshell_providers::ProviderTypeProfile::from_proto(&profile)
.allows_empty_provider_credentials()
})
};
if !bootstrap_allowed {
return Err(Status::invalid_argument(
"provider.credentials must not be empty",
Expand Down Expand Up @@ -412,7 +415,7 @@ impl OpenShell for TestOpenShell {
&self,
_request: tonic::Request<openshell_core::proto::ListProviderProfilesRequest>,
) -> Result<Response<openshell_core::proto::ListProviderProfilesResponse>, Status> {
let mut profiles = openshell_providers::default_profiles()
let mut profiles = openshell_providers::builtin_profiles()
.iter()
.map(openshell_providers::ProviderTypeProfile::to_proto)
.collect::<Vec<_>>();
Expand All @@ -427,7 +430,10 @@ impl OpenShell for TestOpenShell {
request: tonic::Request<openshell_core::proto::GetProviderProfileRequest>,
) -> Result<Response<openshell_core::proto::ProviderProfileResponse>, Status> {
let id = request.into_inner().id;
let profile = if let Some(profile) = openshell_providers::get_default_profile(&id) {
let profile = if let Some(profile) = openshell_providers::builtin_profiles()
.iter()
.find(|profile| profile.id == id)
{
profile.to_proto()
} else {
self.state
Expand Down Expand Up @@ -602,6 +608,7 @@ impl OpenShell for TestOpenShell {
created_at_ms: existing_metadata.created_at_ms,
labels: existing_metadata.labels,
resource_version: 0,
annotations: HashMap::new(),
}),
r#type: existing.r#type,
credentials: merge(existing.credentials, provider.credentials),
Expand Down
Loading
Loading