Skip to content

Commit 1d92000

Browse files
committed
fix(cli): preserve provider mutation storage uncertainty
Recognize the gateway's exact structured storage-uncertainty reason for provider attach, detach, and update. Explain that the change may already be saved and must be reconciled before retrying, without exposing server messages or metadata. Preserve uncertainty ahead of generic retry hints. Exercise saved mutations through the CLI and verify single submission, redaction, missing receipt handling, and untrusted error-detail rejection. Document the recovery guidance for users and the public CLI skill. Signed-off-by: Shiju <shiju@nvidia.com>
1 parent 8177a08 commit 1d92000

5 files changed

Lines changed: 298 additions & 5 deletions

File tree

‎architecture/gateway.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,8 @@ Each receipt projects a common configuration operation in the `config_update_ope
710710

711711
Provider mutation and operation-result writes are separate. A result-storage failure can follow a saved mutation and returns structured uncertainty without a rollback or safe-retry claim. A failed initial snapshot remains failed rather than acquiring a different target during a later lookup. Operations contain only identities, revisions, timestamps, and closed reason categories.
712712

713+
The CLI recognizes the gateway's `CONFIG_OPERATION_STORAGE_UNCERTAIN` error reason and domain for attach, detach, and update. It reports fixed guidance to inspect and reconcile the saved change before retrying, while withholding arbitrary server messages and error metadata. An uncertain mutation never starts a readiness wait or automatic replay.
714+
713715
Provider receipts, installation status, and common operations represent absolute times with protobuf `Timestamp`; report intervals and evidence lifetimes use protobuf `Duration`. Receipt identity compares the full canonical timestamp without truncating nanoseconds. An absent observation or completion time represents missing evidence or an unfinished operation, independently of the Unix epoch.
714716

715717
Provider installation reports belong to the existing `ConnectSupervisor` session. Each report names that session, has an increasing sequence, and expires unless the supervisor reports again. Reconnection or disconnect invalidates prior observations; stored change records survive a gateway restart, but runtime evidence does not. Replaying an identical report cannot extend its lifetime.

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

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use openshell_core::proto::{
2626
ProviderProfileDiagnostic, ProviderProfileImportItem, RotateProviderCredentialRequest,
2727
UpdateProviderProfilesRequest, UpdateProviderRequest,
2828
};
29+
use openshell_core::rpc_error::{ERROR_DOMAIN, decode_details};
2930
use openshell_core::{ObjectId, ObjectName, ObjectWorkspace};
3031
use openshell_providers::{
3132
ProviderTypeProfile, RealDiscoveryContext, detect_provider_from_command, discover_from_profile,
@@ -37,6 +38,30 @@ use std::io::IsTerminal;
3738
use std::path::{Path, PathBuf};
3839
use tonic::{Code, Status};
3940

41+
fn provider_mutation_is_uncertain(status: &Status) -> bool {
42+
// Only a validated gateway ErrorInfo identifies a possibly saved mutation.
43+
// Message text, metadata, foreign domains, and malformed details are untrusted.
44+
decode_details(status).is_some_and(|details| {
45+
details.error_info().is_some_and(|info| {
46+
info.domain == ERROR_DOMAIN && info.reason == "CONFIG_OPERATION_STORAGE_UNCERTAIN"
47+
})
48+
})
49+
}
50+
51+
fn provider_mutation_error(status: &Status, operation: &str) -> miette::Report {
52+
if provider_mutation_is_uncertain(status) {
53+
// Emit fixed guidance without chaining the server's potentially sensitive
54+
// message or metadata. An error does not establish that the write rolled back.
55+
miette!(
56+
"provider change may already be saved (CONFIG_OPERATION_STORAGE_UNCERTAIN); \
57+
readiness receipt could not be recorded. Do not blindly retry the mutation; \
58+
check provider and sandbox status and reconcile the saved change first."
59+
)
60+
} else {
61+
miette!("provider {operation} failed ({})", status.code())
62+
}
63+
}
64+
4065
fn proto_timestamp_ms(timestamp: Option<&prost_types::Timestamp>) -> i64 {
4166
timestamp
4267
.and_then(|value| openshell_core::time::timestamp_to_millis(value).ok())
@@ -123,13 +148,16 @@ pub async fn sandbox_provider_attach(
123148
.await
124149
{
125150
Ok(response) => response.into_inner(),
126-
Err(status) if status.code() == Code::Aborted => {
151+
// Explicit post-save uncertainty takes precedence over a generic retry hint.
152+
Err(status)
153+
if status.code() == Code::Aborted && !provider_mutation_is_uncertain(&status) =>
154+
{
127155
return Err(miette::miette!(
128156
"Failed to attach provider: sandbox was modified by another operation.\n\
129157
Please retry the command."
130158
));
131159
}
132-
Err(error) => return Err(miette!("provider attachment failed ({})", error.code())),
160+
Err(error) => return Err(provider_mutation_error(&error, "attachment")),
133161
};
134162

135163
let receipt = response.receipt.ok_or_else(|| {
@@ -190,13 +218,16 @@ pub async fn sandbox_provider_detach(
190218
.await
191219
{
192220
Ok(response) => response.into_inner(),
193-
Err(status) if status.code() == Code::Aborted => {
221+
// Explicit post-save uncertainty takes precedence over a generic retry hint.
222+
Err(status)
223+
if status.code() == Code::Aborted && !provider_mutation_is_uncertain(&status) =>
224+
{
194225
return Err(miette::miette!(
195226
"Failed to detach provider: sandbox was modified by another operation.\n\
196227
Please retry the command."
197228
));
198229
}
199-
Err(error) => return Err(miette!("provider detachment failed ({})", error.code())),
230+
Err(error) => return Err(provider_mutation_error(&error, "detachment")),
200231
};
201232

202233
let receipt = response.receipt.ok_or_else(|| miette!("gateway did not return a provider receipt; saved detachment cannot establish revocation"))?;
@@ -2405,7 +2436,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> {
24052436
clear_credential_expiration_keys,
24062437
})
24072438
.await
2408-
.map_err(|error| miette!("provider update failed ({})", error.code()))?;
2439+
.map_err(|error| provider_mutation_error(&error, "update"))?;
24092440

24102441
let response = response.into_inner();
24112442
if response.mutation_id.is_empty() {

0 commit comments

Comments
 (0)