Skip to content

Commit c26d4e8

Browse files
fix(grpc): allow credential rotation when legacy provider.type exceeds current limit (#1350)
Closes #1347. Reported by @KodeDaemon in the NVIDIA Developer Discord after upgrading NemoClaw 0.0.38 -> 0.0.39 left the inference provider stranded with `provider.type exceeds maximum length (79 > 64)` on every credential update. `update_provider_record` rebuilt the merged Provider from `existing.r#type` and ran `validate_provider_fields` over it. The CLI's `provider update` sends `r#type: ""`, so the existing value is preserved without modification, but the validator still measured it. Any record whose stored type predates current MAX_PROVIDER_TYPE_LEN (or was written by a path that bypassed validation, e.g. the TUI form which forwards r#type to the request without going through normalize_provider_type) became unupdateable: the only escape was `provider delete` + recreate, which loses any provider.config entries the caller never sees. Split the validator into create-time (validate_provider_fields, full check including immutable name/type) and update-time (validate_provider_mutable_fields, checks only credentials/config maps). The update path validates only what the caller is mutating; immutable fields carried forward from existing are trusted because they passed validation when stored. Tests: new regression in `update_provider_allows_credential_rotation_on_legacy_oversized_type` inserts a Provider with a 79-char type directly via `store.put_message` (bypassing gRPC validation), updates a credential, asserts the update succeeds and the original type is preserved. Existing `update_provider_validates_merged_result` still covers the case where incoming credential entries push the merged map past limits. Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com> Co-authored-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
1 parent eea9751 commit c26d4e8

2 files changed

Lines changed: 67 additions & 5 deletions

File tree

crates/openshell-server/src/grpc/provider.rs

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use prost::Message;
1616
use tonic::Status;
1717
use tracing::warn;
1818

19-
use super::validation::validate_provider_fields;
19+
use super::validation::{validate_provider_fields, validate_provider_mutable_fields};
2020
use super::{
2121
MAX_MAP_KEY_LEN, MAX_MAP_VALUE_LEN, MAX_PAGE_SIZE, MAX_PROVIDER_CONFIG_ENTRIES, clamp_limit,
2222
};
@@ -218,9 +218,14 @@ pub(super) async fn update_provider_record(
218218
provider.credential_expires_at_ms,
219219
);
220220

221-
// Validate BEFORE writing to prevent persisting invalid state
221+
// Validate BEFORE writing to prevent persisting invalid state.
222+
// Validate only the mutable fields (credentials/config) plus metadata and
223+
// attached-sandbox invariants. The immutable name/type are carried forward
224+
// from `existing` and re-running full `validate_provider_fields` here would
225+
// strand legacy records whose stored type predates current limits. See
226+
// #1347.
222227
super::validation::validate_object_metadata(candidate.metadata.as_ref(), "provider")?;
223-
validate_provider_fields(&candidate)?;
228+
validate_provider_mutable_fields(&candidate)?;
224229
validate_provider_update_against_attached_sandboxes(store, &candidate).await?;
225230

226231
// Serialize labels for storage
@@ -1602,8 +1607,8 @@ fn telemetry_provider_profile(provider_type: &str) -> TelemetryProviderProfile {
16021607
#[cfg(test)]
16031608
mod tests {
16041609
use super::*;
1605-
use crate::grpc::MAX_MAP_KEY_LEN;
16061610
use crate::grpc::test_support::test_server_state;
1611+
use crate::grpc::{MAX_MAP_KEY_LEN, MAX_PROVIDER_TYPE_LEN};
16071612
use crate::persistence::test_store;
16081613
use openshell_core::proto::{
16091614
DeleteProviderProfileRequest, GetProviderProfileRequest, ImportProviderProfilesRequest,
@@ -3557,6 +3562,52 @@ mod tests {
35573562
assert_eq!(err.code(), Code::InvalidArgument);
35583563
}
35593564

3565+
/// Regression for #1347: a credential rotation must not fail just because the
3566+
/// existing record's `type` field exceeds the current `MAX_PROVIDER_TYPE_LEN`.
3567+
/// The caller never touches `type`; re-validating it strands legacy records.
3568+
#[tokio::test]
3569+
async fn update_provider_allows_credential_rotation_on_legacy_oversized_type() {
3570+
let store = test_store().await;
3571+
3572+
let oversized_type = "x".repeat(MAX_PROVIDER_TYPE_LEN + 15);
3573+
let legacy = Provider {
3574+
metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta {
3575+
id: uuid::Uuid::new_v4().to_string(),
3576+
name: "legacy-oversized-type".to_string(),
3577+
created_at_ms: 0,
3578+
labels: HashMap::new(),
3579+
resource_version: 0,
3580+
}),
3581+
r#type: oversized_type.clone(),
3582+
credentials: std::iter::once(("API_TOKEN".to_string(), "old".to_string())).collect(),
3583+
config: HashMap::new(),
3584+
credential_expires_at_ms: HashMap::new(),
3585+
};
3586+
store.put_message(&legacy).await.unwrap();
3587+
3588+
let updated = update_provider_record(
3589+
&store,
3590+
Provider {
3591+
metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta {
3592+
id: String::new(),
3593+
name: "legacy-oversized-type".to_string(),
3594+
created_at_ms: 0,
3595+
labels: HashMap::new(),
3596+
resource_version: 0,
3597+
}),
3598+
r#type: String::new(),
3599+
credentials: std::iter::once(("API_TOKEN".to_string(), "new".to_string()))
3600+
.collect(),
3601+
config: HashMap::new(),
3602+
credential_expires_at_ms: HashMap::new(),
3603+
},
3604+
)
3605+
.await
3606+
.unwrap();
3607+
3608+
assert_eq!(updated.r#type, oversized_type);
3609+
}
3610+
35603611
#[tokio::test]
35613612
async fn resolve_provider_env_empty_list_returns_empty() {
35623613
let store = test_store().await;

crates/openshell-server/src/grpc/validation.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ pub(super) fn validate_string_map(
239239
// Provider field validation
240240
// ---------------------------------------------------------------------------
241241

242-
/// Validate field sizes on a `Provider` before persisting.
242+
/// Validate field sizes on a `Provider` before persisting a new record.
243243
pub(super) fn validate_provider_fields(provider: &Provider) -> Result<(), Status> {
244244
let name_len = provider.metadata.as_ref().map_or(0, |m| m.name.len());
245245
if name_len > MAX_NAME_LEN {
@@ -253,6 +253,17 @@ pub(super) fn validate_provider_fields(provider: &Provider) -> Result<(), Status
253253
provider.r#type.len()
254254
)));
255255
}
256+
validate_provider_mutable_fields(provider)
257+
}
258+
259+
/// Validate field sizes on a `Provider` before persisting an update.
260+
///
261+
/// Skips the immutable `name` and `type` fields, which are carried forward from
262+
/// the existing record. Re-checking them would block credential rotation on any
263+
/// legacy record whose stored `name`/`type` predates current limits (or was
264+
/// written by a path that bypassed validation), even though the caller never
265+
/// touches those fields. See #1347.
266+
pub(super) fn validate_provider_mutable_fields(provider: &Provider) -> Result<(), Status> {
256267
validate_string_map(
257268
&provider.credentials,
258269
MAX_PROVIDER_CREDENTIALS_ENTRIES,

0 commit comments

Comments
 (0)