diff --git a/Cargo.lock b/Cargo.lock index 011730760d..53e99e4cbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4247,6 +4247,7 @@ dependencies = [ "glob", "openshell-core", "openshell-policy", + "prost-types", "serde", "serde_json", "serde_yml", diff --git a/architecture/gateway.md b/architecture/gateway.md index 0430d95159..2aef1985db 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -336,6 +336,14 @@ This keeps the gateway data model portable across storage backends and leaves room for future stores that can provide the same object, label, version, and scope semantics. +Public protobuf APIs represent absolute times with `google.protobuf.Timestamp` +and elapsed time with `google.protobuf.Duration`. The integer +`created_at_ms` and `updated_at_ms` database columns are intentionally internal +bookkeeping values, not part of that public convention. On startup, both +storage backends transactionally rewrite legacy scalar time fields inside +protobuf payloads before serving requests. A malformed affected payload aborts +and rolls back startup migration rather than silently dropping a value. + The SQLite adapter tightens the on-disk database file to mode `0o600` on every connect so that provider API keys, SSH session tokens, and sandbox metadata are not readable by other local users on shared hosts. The same restriction is diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index b89f4f9371..1d7b1faa02 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -73,6 +73,18 @@ use std::process::Command; use std::time::{Duration, Instant}; use tonic::{Code, Status}; +fn proto_timestamp_ms(timestamp: Option<&prost_types::Timestamp>) -> i64 { + timestamp + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or_default() +} + +fn proto_duration_seconds(duration: Option<&prost_types::Duration>) -> u64 { + duration + .and_then(|value| openshell_core::time::duration_to_std(value).ok()) + .map_or(0, |value| value.as_secs()) +} + // Re-export SSH functions for backward compatibility pub use crate::ssh::{Editor, print_ssh_config}; pub use crate::ssh::{ @@ -705,7 +717,7 @@ pub async fn sandbox_create( log_tail_lines: 200, event_tail: 50, stop_on_terminal: false, - log_since_ms: 0, + since_time: None, log_sources: vec!["gateway".to_string()], log_min_level: String::new(), }) @@ -1565,7 +1577,12 @@ pub async fn sandbox_exec_grpc( command: command.to_vec(), workdir: workdir.unwrap_or_default().to_string(), environment: environment.clone(), - timeout_seconds, + execution_timeout: Some( + openshell_core::time::duration_from_std(Duration::from_secs( + timeout_seconds.into(), + )) + .into_diagnostic()?, + ), stdin: stdin_payload, tty, no_login_shell, @@ -1938,7 +1955,12 @@ async fn sandbox_exec_interactive_grpc( workdir: workdir.unwrap_or_default().to_string(), environment: environment.clone(), no_login_shell, - timeout_seconds, + execution_timeout: Some( + openshell_core::time::duration_from_std(Duration::from_secs( + timeout_seconds.into(), + )) + .into_diagnostic()?, + ), stdin: Vec::new(), tty: true, cols: u32::from(cols), @@ -2161,7 +2183,12 @@ pub async fn sandbox_list( Ok(SandboxPhase::Deleting) => phase.dimmed().to_string(), _ => phase.to_string(), }; - let created = format_epoch_ms(sandbox.metadata.as_ref().map_or(0, |m| m.created_at_ms)); + let created = format_epoch_ms( + sandbox + .metadata + .as_ref() + .map_or(0, |m| proto_timestamp_ms(m.created_time.as_ref())), + ); if all_workspaces { println!( "{: serde_json::Value { "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)), + "created_at": format_epoch_ms(meta.map_or(0, |m| proto_timestamp_ms(m.created_time.as_ref()))), "phase": phase_name(sandbox.phase()), "current_policy_version": sandbox.current_policy_version(), "exit_code": sandbox.status.as_ref().and_then(|status| status.exit_code), @@ -2604,7 +2631,7 @@ async fn wait_for_lifecycle_phase( log_tail_lines: 0, event_tail: 0, stop_on_terminal: false, - log_since_ms: 0, + since_time: None, log_sources: Vec::new(), log_min_level: String::new(), }) @@ -2852,17 +2879,17 @@ async fn auto_create_provider( metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: exact_name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: discovered.credentials.clone(), config: discovered.config.clone(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: workspace.to_string(), credential_handles: HashMap::new(), }), @@ -2900,17 +2927,17 @@ async fn auto_create_provider( metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.clone(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: discovered.credentials.clone(), config: discovered.config.clone(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: workspace.to_string(), credential_handles: HashMap::new(), }), @@ -3826,17 +3853,24 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) -> metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.clone(), credentials: credential_map, config: config_map, - credential_expires_at_ms: oidc_credential_expires_at_ms, + credential_expiration_times: oidc_credential_expires_at_ms + .into_iter() + .map(|(key, value)| { + openshell_core::time::timestamp_from_millis(value) + .map(|timestamp| (key, timestamp)) + }) + .collect::, _>>() + .into_diagnostic()?, profile_workspace: profile_workspace.to_string(), credential_handles: HashMap::new(), }), @@ -3869,7 +3903,7 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) -> "client_secret".to_string(), "refresh_token".to_string(), ], - expires_at_ms: None, + expiration_time: None, workspace: workspace.to_string(), }) .await @@ -4009,19 +4043,26 @@ fn provider_to_json(provider: &Provider) -> serde_json::Value { serde_json::json!(meta.resource_version), ); } - if meta.created_at_ms != 0 { + if meta.created_time.is_some() { obj.insert( "created_at".to_string(), - serde_json::json!(format_epoch_ms(meta.created_at_ms)), + serde_json::json!(format_epoch_ms(proto_timestamp_ms( + meta.created_time.as_ref() + ))), ); } } // Credential expiration times (only if present) - if !provider.credential_expires_at_ms.is_empty() { + if !provider.credential_expiration_times.is_empty() { + let expirations: HashMap<_, _> = provider + .credential_expiration_times + .iter() + .map(|(key, value)| (key, proto_timestamp_ms(Some(value)))) + .collect(); obj.insert( "credential_expires_at_ms".to_string(), - serde_json::json!(provider.credential_expires_at_ms), + serde_json::json!(expirations), ); } @@ -4498,7 +4539,11 @@ pub async fn provider_refresh_config( strategy: strategy as i32, material, secret_material_keys, - expires_at_ms: input.credential_expires_at_ms, + expiration_time: input + .credential_expires_at_ms + .map(openshell_core::time::timestamp_from_millis) + .transpose() + .into_diagnostic()?, workspace: workspace.to_string(), }) .await @@ -4614,22 +4659,17 @@ fn refresh_status_row(status: &ProviderCredentialRefreshStatus) -> String { provider_refresh_strategy_name(strategy), status.status, provider_refresh_recovery_action_name(recovery_action), - format_optional_epoch_ms(status.expires_at_ms), - format_refresh_next_at_ms(status.next_refresh_at_ms), - format_optional_epoch_ms(status.last_refresh_at_ms), + format_optional_epoch_ms(proto_timestamp_ms(status.expiration_time.as_ref())), + status.next_refresh_time.as_ref().map_or_else( + || "manual".to_string(), + |value| format_optional_epoch_ms(proto_timestamp_ms(Some(value))), + ), + format_optional_epoch_ms(proto_timestamp_ms(status.last_refresh_time.as_ref())), status.failure_code, truncate_status_field(&status.last_error, 72), ) } -fn format_refresh_next_at_ms(next_refresh_at_ms: i64) -> String { - if next_refresh_at_ms == i64::MAX { - "-".to_string() - } else { - format_optional_epoch_ms(next_refresh_at_ms) - } -} - fn provider_refresh_recovery_action_name( action: ProviderCredentialRefreshRecoveryAction, ) -> &'static str { @@ -4993,21 +5033,28 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: credential_map, config: config_map, - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }), - credential_expires_at_ms, + credential_expiration_times: credential_expires_at_ms + .into_iter() + .map(|(key, value)| { + openshell_core::time::timestamp_from_millis(value) + .map(|timestamp| (key, timestamp)) + }) + .collect::, _>>() + .into_diagnostic()?, workspace: workspace.to_string(), }) .await @@ -5119,11 +5166,11 @@ pub async fn workspace_get(server: &str, name: &str, tls: &TlsOptions) -> Result "Resource version:".dimmed(), meta.resource_version ); - if meta.created_at_ms != 0 { + if meta.created_time.is_some() { println!( " {} {}", "Created:".dimmed(), - format_epoch_ms(meta.created_at_ms) + format_epoch_ms(proto_timestamp_ms(meta.created_time.as_ref())) ); } if !meta.labels.is_empty() { @@ -5189,10 +5236,9 @@ pub async fn workspace_list( for workspace in &workspaces { let status = workspace_phase_display(workspace); - let created = workspace - .metadata - .as_ref() - .map_or_else(String::new, |m| format_epoch_ms(m.created_at_ms)); + let created = workspace.metadata.as_ref().map_or_else(String::new, |m| { + format_epoch_ms(proto_timestamp_ms(m.created_time.as_ref())) + }); let labels = workspace.metadata.as_ref().map_or_else(String::new, |m| { m.labels .iter() @@ -5389,10 +5435,12 @@ fn workspace_to_json(workspace: &openshell_core::proto::Workspace) -> serde_json "resource_version".to_string(), serde_json::json!(meta.resource_version), ); - if meta.created_at_ms != 0 { + if meta.created_time.is_some() { obj.insert( "created_at".to_string(), - serde_json::json!(format_epoch_ms(meta.created_at_ms)), + serde_json::json!(format_epoch_ms(proto_timestamp_ms( + meta.created_time.as_ref() + ))), ); } if !meta.labels.is_empty() { @@ -5438,7 +5486,10 @@ pub async fn gateway_inference_set( route_name: route_name.to_string(), verify: false, no_verify, - timeout_secs, + request_timeout: Some( + openshell_core::time::duration_from_std(Duration::from_secs(timeout_secs)) + .into_diagnostic()?, + ), workspace: workspace.to_string(), }) .await; @@ -5462,7 +5513,7 @@ pub async fn gateway_inference_set( println!(" {} {}", "Provider:".dimmed(), configured.provider_name); println!(" {} {}", "Model:".dimmed(), configured.model_id); println!(" {} {}", "Version:".dimmed(), configured.version); - print_timeout(configured.timeout_secs); + print_timeout(proto_duration_seconds(configured.request_timeout.as_ref())); if configured.validation_performed { println!(" {}", "Validated Endpoints:".dimmed()); for endpoint in configured.validated_endpoints { @@ -5503,7 +5554,8 @@ pub async fn gateway_inference_update( let provider = provider_name.unwrap_or(¤t.provider_name); let model = model_id.unwrap_or(¤t.model_id); - let timeout = timeout_secs.unwrap_or(current.timeout_secs); + let timeout = + timeout_secs.unwrap_or_else(|| proto_duration_seconds(current.request_timeout.as_ref())); let progress = if std::io::stdout().is_terminal() { let spinner = ProgressBar::new_spinner(); @@ -5525,7 +5577,10 @@ pub async fn gateway_inference_update( route_name: route_name.to_string(), verify: false, no_verify, - timeout_secs: timeout, + request_timeout: Some( + openshell_core::time::duration_from_std(Duration::from_secs(timeout)) + .into_diagnostic()?, + ), workspace: workspace.to_string(), }) .await; @@ -5549,7 +5604,7 @@ pub async fn gateway_inference_update( println!(" {} {}", "Provider:".dimmed(), configured.provider_name); println!(" {} {}", "Model:".dimmed(), configured.model_id); println!(" {} {}", "Version:".dimmed(), configured.version); - print_timeout(configured.timeout_secs); + print_timeout(proto_duration_seconds(configured.request_timeout.as_ref())); if configured.validation_performed { println!(" {}", "Validated Endpoints:".dimmed()); for endpoint in configured.validated_endpoints { @@ -5589,7 +5644,7 @@ pub async fn gateway_inference_get( println!(" {} {}", "Provider:".dimmed(), configured.provider_name); println!(" {} {}", "Model:".dimmed(), configured.model_id); println!(" {} {}", "Version:".dimmed(), configured.version); - print_timeout(configured.timeout_secs); + print_timeout(proto_duration_seconds(configured.request_timeout.as_ref())); } else { // Show both routes by default. print_inference_route(&mut client, "Inference", "", workspace).await; @@ -5650,7 +5705,7 @@ async fn print_inference_route( println!(" {} {}", "Provider:".dimmed(), configured.provider_name); println!(" {} {}", "Model:".dimmed(), configured.model_id); println!(" {} {}", "Version:".dimmed(), configured.version); - print_timeout(configured.timeout_secs); + print_timeout(proto_duration_seconds(configured.request_timeout.as_ref())); } Err(e) if e.code() == Code::NotFound => { println!("{}", format!("{label}:").cyan().bold()); @@ -6635,11 +6690,21 @@ where writeln!(stdout, "Hash: {}", rev.policy_hash).into_diagnostic()?; writeln!(stdout, "Status: {status:?}").into_diagnostic()?; writeln!(stdout, "Active: {}", inner.active_version).into_diagnostic()?; - if rev.created_at_ms > 0 { - writeln!(stdout, "Created: {} ms", rev.created_at_ms).into_diagnostic()?; + if let Some(created_time) = rev.created_time.as_ref() { + writeln!( + stdout, + "Created: {} ms", + proto_timestamp_ms(Some(created_time)) + ) + .into_diagnostic()?; } - if rev.loaded_at_ms > 0 { - writeln!(stdout, "Loaded: {} ms", rev.loaded_at_ms).into_diagnostic()?; + if let Some(loaded_time) = rev.loaded_time.as_ref() { + writeln!( + stdout, + "Loaded: {} ms", + proto_timestamp_ms(Some(loaded_time)) + ) + .into_diagnostic()?; } if !rev.load_error.is_empty() { writeln!(stdout, "Error: {}", rev.load_error).into_diagnostic()?; @@ -6817,11 +6882,14 @@ pub async fn sandbox_policy_get_global( println!("Version: {}", rev.version); println!("Hash: {}", rev.policy_hash); println!("Status: {status:?}"); - if rev.created_at_ms > 0 { - println!("Created: {} ms", rev.created_at_ms); + if let Some(created_time) = rev.created_time.as_ref() { + println!( + "Created: {} ms", + proto_timestamp_ms(Some(created_time)) + ); } - if rev.loaded_at_ms > 0 { - println!("Loaded: {} ms", rev.loaded_at_ms); + if let Some(loaded_time) = rev.loaded_time.as_ref() { + println!("Loaded: {} ms", proto_timestamp_ms(Some(loaded_time))); } if view.includes_policy() { @@ -6877,16 +6945,16 @@ fn policy_revision_to_json( serde_json::json!(active_version), ); } - if rev.created_at_ms > 0 { + if rev.created_time.is_some() { obj.insert( "created_at_ms".to_string(), - serde_json::json!(rev.created_at_ms), + serde_json::json!(proto_timestamp_ms(rev.created_time.as_ref())), ); } - if rev.loaded_at_ms > 0 { + if rev.loaded_time.is_some() { obj.insert( "loaded_at_ms".to_string(), - serde_json::json!(rev.loaded_at_ms), + serde_json::json!(proto_timestamp_ms(rev.loaded_time.as_ref())), ); } if !rev.load_error.is_empty() { @@ -7001,7 +7069,7 @@ fn print_policy_revision_table(revisions: &[openshell_core::proto::SandboxPolicy rev.version, hash_short, format!("{status:?}"), - rev.created_at_ms, + proto_timestamp_ms(rev.created_time.as_ref()), error_short, ); } @@ -7069,7 +7137,8 @@ pub async fn sandbox_logs( log_tail_lines: lines, event_tail: 0, stop_on_terminal: false, - log_since_ms: since_ms, + since_time: openshell_core::time::optional_timestamp_from_legacy_millis(since_ms) + .into_diagnostic()?, log_sources: source_filter, log_min_level: level.to_uppercase(), }) @@ -7091,7 +7160,8 @@ pub async fn sandbox_logs( .get_sandbox_logs(GetSandboxLogsRequest { sandbox_id: sandbox.object_id().to_string(), lines, - since_ms, + since_time: openshell_core::time::optional_timestamp_from_legacy_millis(since_ms) + .into_diagnostic()?, sources: source_filter, min_level: level.to_uppercase(), workspace: workspace.to_string(), @@ -7122,8 +7192,9 @@ fn print_log_line(log: &openshell_core::proto::SandboxLogLine) { } else { &log.source }; - let secs = log.timestamp_ms / 1000; - let millis = log.timestamp_ms % 1000; + let timestamp_ms = proto_timestamp_ms(log.event_time.as_ref()); + let secs = timestamp_ms / 1000; + let millis = timestamp_ms % 1000; if log.fields.is_empty() { println!( "[{secs}.{millis:03}] [{source:<7}] [{:<5}] [{}] {}", @@ -7251,8 +7322,8 @@ pub async fn sandbox_draft_get( " {} {} (first seen {}, last seen {})", "Hits:".dimmed(), chunk.hit_count, - format_epoch_ms(chunk.first_seen_ms), - format_epoch_ms(chunk.last_seen_ms), + format_epoch_ms(proto_timestamp_ms(chunk.first_seen_time.as_ref())), + format_epoch_ms(proto_timestamp_ms(chunk.last_seen_time.as_ref())), ); } println!(); @@ -7444,7 +7515,7 @@ pub async fn sandbox_draft_history( println!( " {} {} [{}] {}", - format_timestamp_ms(entry.timestamp_ms).dimmed(), + format_timestamp_ms(proto_timestamp_ms(entry.event_time.as_ref())).dimmed(), event_colored, entry.chunk_id.get(..8).unwrap_or(&entry.chunk_id), entry.description, @@ -7739,7 +7810,7 @@ mod tests { "https://api.custom.example".to_string(), )) .collect(), - credential_expires_at_ms: std::collections::HashMap::new(), + credential_expiration_times: std::collections::HashMap::new(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }], @@ -7788,15 +7859,15 @@ mod tests { credential_key: "MS_GRAPH_ACCESS_TOKEN".to_string(), strategy: ProviderCredentialRefreshStrategy::Oauth2ClientCredentials as i32, status: "error".to_string(), - expires_at_ms: 1_767_225_600_000, - next_refresh_at_ms: i64::MAX, - last_refresh_at_ms: 1_767_225_000_000, + expiration_time: openshell_core::time::timestamp_from_millis(1_767_225_600_000).ok(), + next_refresh_time: None, + last_refresh_time: openshell_core::time::timestamp_from_millis(1_767_225_000_000).ok(), last_error: "token endpoint returned a very long error message that should be truncated for table readability" .to_string(), recovery_action: ProviderCredentialRefreshRecoveryAction::Reauthorize as i32, failure_code: "oauth_rotated_refresh_token_handle_missing".to_string(), provider_error_subtype: "invalid_rapt".to_string(), - last_error_at_ms: 1_767_225_000_000, + last_error_time: openshell_core::time::timestamp_from_millis(1_767_225_000_000).ok(), }); assert!(row.contains("my-graph")); @@ -8271,7 +8342,7 @@ mod tests { status: "False".to_string(), reason: "Unschedulable".to_string(), message: "Another GPU sandbox may already be using the available GPU.".to_string(), - last_transition_time: String::new(), + transition_time: None, }], ..Default::default() }; @@ -8292,7 +8363,7 @@ mod tests { status: "True".to_string(), reason: "Scheduled".to_string(), message: "Sandbox scheduled".to_string(), - last_transition_time: String::new(), + transition_time: None, }], ..Default::default() }; @@ -8692,7 +8763,7 @@ mod tests { r#type: "anthropic".to_string(), credentials: std::collections::HashMap::new(), config: std::collections::HashMap::new(), - credential_expires_at_ms: std::collections::HashMap::new(), + credential_expiration_times: std::collections::HashMap::new(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }; @@ -8716,7 +8787,7 @@ mod tests { r#type: "anthropic".to_string(), credentials, config: std::collections::HashMap::new(), - credential_expires_at_ms: std::collections::HashMap::new(), + credential_expiration_times: std::collections::HashMap::new(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }; @@ -8755,7 +8826,7 @@ mod tests { r#type: "custom".to_string(), credentials: std::collections::HashMap::new(), config, - credential_expires_at_ms: std::collections::HashMap::new(), + credential_expiration_times: std::collections::HashMap::new(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }; @@ -8787,7 +8858,7 @@ mod tests { r#type: "anthropic".to_string(), credentials: std::collections::HashMap::new(), config: std::collections::HashMap::new(), // Empty config - credential_expires_at_ms: std::collections::HashMap::new(), + credential_expiration_times: std::collections::HashMap::new(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }; @@ -8809,11 +8880,11 @@ mod tests { id: "prov-123".to_string(), name: "test-provider".to_string(), resource_version: 42, - created_at_ms: 1_234_567_890_000, + created_time: openshell_core::time::timestamp_from_millis(1_234_567_890_000).ok(), labels, annotations: std::collections::HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }; let provider = Provider { @@ -8821,7 +8892,7 @@ mod tests { r#type: "anthropic".to_string(), credentials: std::collections::HashMap::new(), config: std::collections::HashMap::new(), - credential_expires_at_ms: std::collections::HashMap::new(), + credential_expiration_times: std::collections::HashMap::new(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }; @@ -8848,7 +8919,7 @@ mod tests { r#type: "anthropic".to_string(), credentials: std::collections::HashMap::new(), config: std::collections::HashMap::new(), - credential_expires_at_ms: std::collections::HashMap::new(), + credential_expiration_times: std::collections::HashMap::new(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }; @@ -8879,7 +8950,15 @@ mod tests { r#type: "oauth".to_string(), credentials: std::collections::HashMap::new(), config: std::collections::HashMap::new(), - credential_expires_at_ms, + credential_expiration_times: credential_expires_at_ms + .into_iter() + .map(|(key, value)| { + ( + key, + openshell_core::time::timestamp_from_millis(value).unwrap(), + ) + }) + .collect(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }; @@ -8897,7 +8976,7 @@ mod tests { let metadata = ObjectMeta { id: "prov-123".to_string(), name: "test-provider".to_string(), - created_at_ms: 1_609_459_200_000, // 2021-01-01 00:00:00 + created_time: openshell_core::time::timestamp_from_millis(1_609_459_200_000).ok(), // 2021-01-01 00:00:00 ..Default::default() }; @@ -8906,7 +8985,7 @@ mod tests { r#type: "anthropic".to_string(), credentials: std::collections::HashMap::new(), config: std::collections::HashMap::new(), - credential_expires_at_ms: std::collections::HashMap::new(), + credential_expiration_times: std::collections::HashMap::new(), profile_workspace: String::new(), credential_handles: std::collections::HashMap::new(), }; @@ -8928,7 +9007,7 @@ mod tests { id: "sb-123".to_string(), name: "test-sb".to_string(), resource_version: 5, - created_at_ms: 1_609_459_200_000, + created_time: openshell_core::time::timestamp_from_millis(1_609_459_200_000).ok(), ..Default::default() }), ..Default::default() diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index 9999a7c083..332608f655 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -61,17 +61,17 @@ impl TestOpenShell { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("id-{name}"), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -382,38 +382,34 @@ impl OpenShell for TestOpenShell { } base }; - let merge_expiry = |mut base: HashMap, incoming: HashMap| { - if incoming.is_empty() { - return base; - } - for (k, v) in incoming { - if v <= 0 { - base.remove(&k); - } else { - base.insert(k, v); + let merge_expiry = + |mut base: HashMap, + incoming: HashMap| { + if incoming.is_empty() { + return base; } - } - base - }; + base.extend(incoming); + base + }; let existing_metadata = existing.metadata.clone().unwrap_or_default(); let provider_metadata = provider.metadata.clone().unwrap_or_default(); let updated = Provider { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: existing_metadata.id, name: provider_metadata.name, - created_at_ms: existing_metadata.created_at_ms, + created_time: existing_metadata.created_time, labels: existing_metadata.labels, resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: existing.r#type, credentials: merge(existing.credentials, provider.credentials), config: merge(existing.config, provider.config), - credential_expires_at_ms: merge_expiry( - existing.credential_expires_at_ms, - provider.credential_expires_at_ms, + credential_expiration_times: merge_expiry( + existing.credential_expiration_times, + provider.credential_expiration_times, ), profile_workspace: existing.profile_workspace, credential_handles: if provider.credential_handles.is_empty() { diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 84fb8f9163..8639b28263 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -172,12 +172,12 @@ impl OpenShell for TestOpenShell { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("sb-{name}"), name, - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 1, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: None, status: None, @@ -651,38 +651,34 @@ impl OpenShell for TestOpenShell { } base }; - let merge_expiry = |mut base: HashMap, incoming: HashMap| { - if incoming.is_empty() { - return base; - } - for (k, v) in incoming { - if v <= 0 { - base.remove(&k); - } else { - base.insert(k, v); + let merge_expiry = + |mut base: HashMap, + incoming: HashMap| { + if incoming.is_empty() { + return base; } - } - base - }; + base.extend(incoming); + base + }; let existing_metadata = existing.metadata.clone().unwrap_or_default(); let provider_metadata = provider.metadata.clone().unwrap_or_default(); let updated = Provider { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: existing_metadata.id, name: provider_metadata.name, - created_at_ms: existing_metadata.created_at_ms, + created_time: existing_metadata.created_time, labels: existing_metadata.labels, resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: existing.r#type, credentials: merge(existing.credentials, provider.credentials), config: merge(existing.config, provider.config), - credential_expires_at_ms: merge_expiry( - existing.credential_expires_at_ms, - provider.credential_expires_at_ms, + credential_expiration_times: merge_expiry( + existing.credential_expiration_times, + provider.credential_expiration_times, ), profile_workspace: existing.profile_workspace, credential_handles: if provider.credential_handles.is_empty() { @@ -743,7 +739,10 @@ impl OpenShell for TestOpenShell { credential_key: request.credential_key.clone(), material: request.material.clone(), secret_material_keys: request.secret_material_keys.clone(), - expires_at_ms: request.expires_at_ms, + expires_at_ms: request + .expiration_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()), }); let configure_failure = self .state @@ -764,14 +763,14 @@ impl OpenShell for TestOpenShell { credential_key: request.credential_key.clone(), strategy: request.strategy, status: "configured".to_string(), - expires_at_ms: request.expires_at_ms.unwrap_or_default(), - next_refresh_at_ms: 0, - last_refresh_at_ms: 0, + expiration_time: request.expiration_time, + next_refresh_time: None, + last_refresh_time: None, last_error: String::new(), recovery_action: 0, failure_code: String::new(), provider_error_subtype: String::new(), - last_error_at_ms: 0, + last_error_time: None, }; drop(providers); self.state @@ -810,9 +809,9 @@ impl OpenShell for TestOpenShell { .get_mut(&(provider_name.clone(), credential_key.clone())) .ok_or_else(|| Status::not_found("provider refresh state not found"))?; status.status = "refreshed".to_string(); - status.last_refresh_at_ms = 1; - status.next_refresh_at_ms = 3_600_000; - status.expires_at_ms = 3_600_000; + status.last_refresh_time = openshell_core::time::timestamp_from_millis(1).ok(); + status.next_refresh_time = openshell_core::time::timestamp_from_millis(3_600_000).ok(); + status.expiration_time = openshell_core::time::timestamp_from_millis(3_600_000).ok(); let status = status.clone(); drop(refresh_statuses); let mut providers = self.state.providers.lock().await; @@ -822,9 +821,10 @@ impl OpenShell for TestOpenShell { provider .credentials .insert(credential_key.clone(), format!("minted-{credential_key}")); - provider - .credential_expires_at_ms - .insert(credential_key, 3_600_000); + provider.credential_expiration_times.insert( + credential_key, + openshell_core::time::timestamp_from_millis(3_600_000).unwrap(), + ); Ok(Response::new(RotateProviderCredentialResponse { status: Some(status), })) @@ -2132,7 +2132,7 @@ async fn provider_update_from_existing_uses_profile_discovery_when_v2_enabled() r#type: "custom-update-discovery".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 118daff902..aafdded566 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -120,12 +120,12 @@ impl OpenShell for TestOpenShell { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("id-{sandbox_name}"), name: sandbox_name, - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Sandbox::default() }; @@ -158,12 +158,12 @@ impl OpenShell for TestOpenShell { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("id-{name}"), name, - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Sandbox::default() }; @@ -449,12 +449,12 @@ impl OpenShell for TestOpenShell { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: sandbox_id.clone(), name: sandbox_id.trim_start_matches("id-").to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Sandbox::default() }; @@ -467,7 +467,7 @@ impl OpenShell for TestOpenShell { status: "False".to_string(), reason: "ProcessExited".to_string(), message: "VM process exited with status 0".to_string(), - last_transition_time: String::new(), + transition_time: None, }], ..Default::default() }), @@ -517,7 +517,7 @@ impl OpenShell for TestOpenShell { .send(Ok(SandboxStreamEvent { payload: Some(sandbox_stream_event::Payload::Log(SandboxLogLine { sandbox_id: sandbox_id.clone(), - timestamp_ms: 0, + event_time: None, level: "INFO".to_string(), target: "test".to_string(), message: message.to_string(), diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index ee72728aa9..e6d78ebbd9 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -119,12 +119,12 @@ impl OpenShell for TestOpenShell { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "test-id".to_string(), name, - created_at_ms: 0, + created_time: None, labels: std::collections::HashMap::new(), resource_version: 0, annotations: std::collections::HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Default::default() }), @@ -473,8 +473,8 @@ impl OpenShell for TestOpenShell { version: 7, policy_hash: "sha256:test-policy".to_string(), status: PolicyStatus::Loaded.into(), - created_at_ms: 1_700_000_000_000, - loaded_at_ms: 1_700_000_000_500, + created_time: openshell_core::time::timestamp_from_millis(1_700_000_000_000).ok(), + loaded_time: openshell_core::time::timestamp_from_millis(1_700_000_000_500).ok(), policy: Some(policy), ..Default::default() }), diff --git a/crates/openshell-core/src/forward.rs b/crates/openshell-core/src/forward.rs index a17edbdee2..c0504d57e5 100644 --- a/crates/openshell-core/src/forward.rs +++ b/crates/openshell-core/src/forward.rs @@ -1093,7 +1093,7 @@ mod tests { gateway_host: "gateway.example.com".to_string(), gateway_port: 443, host_key_fingerprint: String::new(), - expires_at_ms: 0, + expiration_time: None, } } diff --git a/crates/openshell-core/src/grpc_client.rs b/crates/openshell-core/src/grpc_client.rs index 54f0db6902..dc75545986 100644 --- a/crates/openshell-core/src/grpc_client.rs +++ b/crates/openshell-core/src/grpc_client.rs @@ -31,6 +31,7 @@ use crate::proto::{ UpdateConfigRequest, inference_client::InferenceClient, open_shell_client::OpenShellClient, }; use crate::sandbox_env; +use crate::time::{duration_to_std, timestamp_to_millis}; use miette::{IntoDiagnostic, Result, WrapErr}; use openshell_extension_core::{BearerTokenSlot, ExtensionCredentialStore}; use tonic::Status; @@ -483,10 +484,11 @@ async fn refresh_extension_credentials_with_client( "gateway returned an unexpected or duplicate extension credential" )); } - validated.insert( - credential.service_name, - (credential.token, credential.expires_at_ms), - ); + let expiration_time = credential.expiration_time.as_ref().ok_or_else(|| { + miette::miette!("gateway returned an extension credential without an expiration time") + })?; + let expires_at_ms = timestamp_to_millis(expiration_time).into_diagnostic()?; + validated.insert(credential.service_name, (credential.token, expires_at_ms)); } if validated.len() != expected.len() { return Err(miette::miette!( @@ -850,10 +852,19 @@ pub async fn fetch_provider_environment( .into_diagnostic()?; let inner = response.into_inner(); + let credential_expires_at_ms = inner + .credential_expiration_times + .iter() + .map(|(name, expiration_time)| { + timestamp_to_millis(expiration_time) + .map(|value| (name.clone(), value)) + .into_diagnostic() + }) + .collect::>>()?; Ok(ProviderEnvironmentResult { environment: inner.environment, provider_env_revision: inner.provider_env_revision, - credential_expires_at_ms: inner.credential_expires_at_ms, + credential_expires_at_ms, dynamic_credentials: inner.dynamic_credentials, static_credential_bindings: inner.static_credential_bindings, non_secret_environment_keys: inner.non_secret_environment_keys, @@ -886,9 +897,18 @@ pub async fn exchange_provider_subject_token( .await .map_err(provider_subject_token_exchange_status)?; let inner = response.into_inner(); + let expires_in = inner + .expires_after + .as_ref() + .map(duration_to_std) + .transpose() + .into_diagnostic()? + .map_or(0, |value| { + i64::try_from(value.as_secs()).unwrap_or(i64::MAX) + }); Ok(ProviderSubjectTokenExchangeResult { access_token: inner.access_token, - expires_in: inner.expires_in, + expires_in, token_type: inner.token_type, }) } diff --git a/crates/openshell-core/src/middleware.rs b/crates/openshell-core/src/middleware.rs index 2b3fb18982..f7f92eb94a 100644 --- a/crates/openshell-core/src/middleware.rs +++ b/crates/openshell-core/src/middleware.rs @@ -175,7 +175,7 @@ impl<'a> HttpRequestView<'a> { /// operation: SupervisorMiddlewareOperation::HttpRequest as i32, /// phase: SupervisorMiddlewarePhase::PreCredentials as i32, /// max_payload_bytes: 1024, -/// timeout: String::new(), +/// request_timeout: None, /// }], /// expected_audience: String::new(), /// } diff --git a/crates/openshell-core/src/time.rs b/crates/openshell-core/src/time.rs index 15dc0c40d3..f6d9ecda6a 100644 --- a/crates/openshell-core/src/time.rs +++ b/crates/openshell-core/src/time.rs @@ -3,7 +3,34 @@ //! Time utilities shared across `OpenShell` crates. -use std::time::{SystemTime, UNIX_EPOCH}; +use prost_types::{Duration as ProtoDuration, Timestamp}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use thiserror::Error; + +/// Earliest second accepted by `google.protobuf.Timestamp` (0001-01-01 UTC). +pub const MIN_TIMESTAMP_SECONDS: i64 = -62_135_596_800; +/// Latest second accepted by `google.protobuf.Timestamp` (9999-12-31T23:59:59 UTC). +pub const MAX_TIMESTAMP_SECONDS: i64 = 253_402_300_799; +/// Largest absolute seconds component accepted by `google.protobuf.Duration`. +pub const MAX_DURATION_SECONDS: i64 = 315_576_000_000; + +/// Error returned when a protobuf well-known time value is not canonical or +/// cannot be represented by the requested Rust type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum ProtoTimeError { + #[error("timestamp seconds are outside the protobuf range")] + TimestampOutOfRange, + #[error("timestamp nanos must be between 0 and 999999999")] + InvalidTimestampNanos, + #[error("duration seconds are outside the protobuf range")] + DurationOutOfRange, + #[error("duration nanos are outside the protobuf range or have a different sign than seconds")] + InvalidDurationNanos, + #[error("negative protobuf duration cannot be represented by std::time::Duration")] + NegativeDuration, + #[error("time conversion overflowed the destination type")] + Overflow, +} /// Return the current Unix timestamp in milliseconds, saturating to [`i64::MAX`] /// on overflow. Returns `0` if the system clock is before the Unix epoch. @@ -14,3 +41,214 @@ pub fn now_ms() -> i64 { .duration_since(UNIX_EPOCH) .map_or(0, |d| i64::try_from(d.as_millis()).unwrap_or(i64::MAX)) } + +/// Validate a protobuf timestamp's range and canonical nanosecond component. +pub fn validate_timestamp(value: &Timestamp) -> Result<(), ProtoTimeError> { + if !(MIN_TIMESTAMP_SECONDS..=MAX_TIMESTAMP_SECONDS).contains(&value.seconds) { + return Err(ProtoTimeError::TimestampOutOfRange); + } + if !(0..1_000_000_000).contains(&value.nanos) { + return Err(ProtoTimeError::InvalidTimestampNanos); + } + Ok(()) +} + +/// Convert Unix epoch milliseconds to a canonical protobuf timestamp. +pub fn timestamp_from_millis(value: i64) -> Result { + let timestamp = Timestamp { + seconds: value.div_euclid(1_000), + nanos: i32::try_from(value.rem_euclid(1_000) * 1_000_000) + .map_err(|_| ProtoTimeError::Overflow)?, + }; + validate_timestamp(×tamp)?; + Ok(timestamp) +} + +/// Convert a legacy timestamp where zero meant "unset" to WKT presence. +pub fn optional_timestamp_from_legacy_millis( + value: i64, +) -> Result, ProtoTimeError> { + if value == 0 { + Ok(None) + } else { + timestamp_from_millis(value).map(Some) + } +} + +/// Convert a protobuf timestamp to Unix epoch milliseconds. +/// +/// Nanoseconds finer than one millisecond are truncated toward the start of the +/// represented second. New protobuf-facing code should retain the `Timestamp` +/// instead of using this compatibility helper. +pub fn timestamp_to_millis(value: &Timestamp) -> Result { + validate_timestamp(value)?; + value + .seconds + .checked_mul(1_000) + .and_then(|seconds| seconds.checked_add(i64::from(value.nanos / 1_000_000))) + .ok_or(ProtoTimeError::Overflow) +} + +/// Convert a Rust system time to a protobuf timestamp without losing nanos. +pub fn timestamp_from_system_time(value: SystemTime) -> Result { + let timestamp = match value.duration_since(UNIX_EPOCH) { + Ok(after_epoch) => Timestamp { + seconds: i64::try_from(after_epoch.as_secs()).map_err(|_| ProtoTimeError::Overflow)?, + nanos: i32::try_from(after_epoch.subsec_nanos()) + .map_err(|_| ProtoTimeError::Overflow)?, + }, + Err(before_epoch) => { + let duration = before_epoch.duration(); + let seconds = + i64::try_from(duration.as_secs()).map_err(|_| ProtoTimeError::Overflow)?; + if duration.subsec_nanos() == 0 { + Timestamp { + seconds: -seconds, + nanos: 0, + } + } else { + Timestamp { + seconds: seconds + .checked_neg() + .and_then(|v| v.checked_sub(1)) + .ok_or(ProtoTimeError::Overflow)?, + nanos: i32::try_from(1_000_000_000 - duration.subsec_nanos()) + .map_err(|_| ProtoTimeError::Overflow)?, + } + } + } + }; + validate_timestamp(×tamp)?; + Ok(timestamp) +} + +/// Convert a protobuf timestamp to `SystemTime` without losing nanos. +pub fn system_time_from_timestamp(value: &Timestamp) -> Result { + validate_timestamp(value)?; + let nanos = u32::try_from(value.nanos).map_err(|_| ProtoTimeError::Overflow)?; + if value.seconds >= 0 { + let seconds = u64::try_from(value.seconds).map_err(|_| ProtoTimeError::Overflow)?; + UNIX_EPOCH + .checked_add(Duration::new(seconds, nanos)) + .ok_or(ProtoTimeError::Overflow) + } else if value.nanos == 0 { + UNIX_EPOCH + .checked_sub(Duration::from_secs(value.seconds.unsigned_abs())) + .ok_or(ProtoTimeError::Overflow) + } else { + let seconds_before = value + .seconds + .unsigned_abs() + .checked_sub(1) + .ok_or(ProtoTimeError::Overflow)?; + UNIX_EPOCH + .checked_sub(Duration::new(seconds_before, 1_000_000_000 - nanos)) + .ok_or(ProtoTimeError::Overflow) + } +} + +/// Validate a protobuf duration's range and canonical sign relationship. +pub fn validate_duration(value: &ProtoDuration) -> Result<(), ProtoTimeError> { + if !(-MAX_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&value.seconds) { + return Err(ProtoTimeError::DurationOutOfRange); + } + if !(-999_999_999..=999_999_999).contains(&value.nanos) + || (value.seconds > 0 && value.nanos < 0) + || (value.seconds < 0 && value.nanos > 0) + { + return Err(ProtoTimeError::InvalidDurationNanos); + } + Ok(()) +} + +/// Convert a nonnegative Rust duration to a protobuf duration. +pub fn duration_from_std(value: Duration) -> Result { + let duration = ProtoDuration { + seconds: i64::try_from(value.as_secs()).map_err(|_| ProtoTimeError::Overflow)?, + nanos: i32::try_from(value.subsec_nanos()).map_err(|_| ProtoTimeError::Overflow)?, + }; + validate_duration(&duration)?; + Ok(duration) +} + +/// Convert a nonnegative protobuf duration to a Rust duration. +pub fn duration_to_std(value: &ProtoDuration) -> Result { + validate_duration(value)?; + if value.seconds < 0 || value.nanos < 0 { + return Err(ProtoTimeError::NegativeDuration); + } + let seconds = u64::try_from(value.seconds).map_err(|_| ProtoTimeError::Overflow)?; + let nanos = u32::try_from(value.nanos).map_err(|_| ProtoTimeError::Overflow)?; + Ok(Duration::new(seconds, nanos)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn legacy_zero_timestamp_is_absent() { + assert_eq!(optional_timestamp_from_legacy_millis(0), Ok(None)); + } + + #[test] + fn negative_millis_produce_canonical_timestamp() { + assert_eq!( + timestamp_from_millis(-1).unwrap(), + Timestamp { + seconds: -1, + nanos: 999_000_000, + } + ); + } + + #[test] + fn system_time_round_trip_preserves_sub_millisecond_precision() { + let original = UNIX_EPOCH - Duration::new(12, 345_678_901); + let timestamp = timestamp_from_system_time(original).unwrap(); + assert_eq!(system_time_from_timestamp(×tamp), Ok(original)); + } + + #[test] + fn timestamp_validation_rejects_invalid_range_and_nanos() { + assert_eq!( + validate_timestamp(&Timestamp { + seconds: MAX_TIMESTAMP_SECONDS + 1, + nanos: 0, + }), + Err(ProtoTimeError::TimestampOutOfRange) + ); + assert_eq!( + validate_timestamp(&Timestamp { + seconds: 0, + nanos: -1, + }), + Err(ProtoTimeError::InvalidTimestampNanos) + ); + } + + #[test] + fn duration_round_trip_preserves_nanos() { + let original = Duration::new(42, 123_456_789); + let proto = duration_from_std(original).unwrap(); + assert_eq!(duration_to_std(&proto), Ok(original)); + } + + #[test] + fn duration_validation_rejects_mixed_signs_and_negative_std_conversion() { + assert_eq!( + validate_duration(&ProtoDuration { + seconds: 1, + nanos: -1, + }), + Err(ProtoTimeError::InvalidDurationNanos) + ); + assert_eq!( + duration_to_std(&ProtoDuration { + seconds: 0, + nanos: -1, + }), + Err(ProtoTimeError::NegativeDuration) + ); + } +} diff --git a/crates/openshell-driver-db-credstore/src/lib.rs b/crates/openshell-driver-db-credstore/src/lib.rs index 24c21e993f..f0783b1f70 100644 --- a/crates/openshell-driver-db-credstore/src/lib.rs +++ b/crates/openshell-driver-db-credstore/src/lib.rs @@ -304,7 +304,7 @@ impl DbCredstoreCredentialDriver { Ok::<_, Status>(ResolvedCredential { request_id: request.request_id, value, - expires_at_ms: 0, + expiration_time: None, }) }); futures::future::try_join_all(futures).await diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index 3941819c61..889f326b11 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -1438,7 +1438,10 @@ impl DockerComputeDriver { self.publish_platform_event( sandbox_id.to_string(), DriverPlatformEvent { - timestamp_ms: openshell_core::time::now_ms(), + event_time: openshell_core::time::timestamp_from_millis( + openshell_core::time::now_ms(), + ) + .ok(), source: "docker".to_string(), r#type: "Normal".to_string(), reason: reason.to_string(), @@ -2221,7 +2224,7 @@ fn provisioning_condition() -> DriverCondition { status: "False".to_string(), reason: "Starting".to_string(), message: "Docker container is starting".to_string(), - last_transition_time: String::new(), + transition_time: None, } } @@ -2231,7 +2234,7 @@ fn error_condition(reason: &str, message: &str) -> DriverCondition { status: "False".to_string(), reason: reason.to_string(), message: message.to_string(), - last_transition_time: String::new(), + transition_time: None, } } @@ -2242,7 +2245,8 @@ fn platform_event( message: String, ) -> DriverPlatformEvent { DriverPlatformEvent { - timestamp_ms: openshell_core::time::now_ms(), + event_time: openshell_core::time::timestamp_from_millis(openshell_core::time::now_ms()) + .ok(), source: source.to_string(), r#type: event_type.to_string(), reason: reason.to_string(), @@ -2270,7 +2274,8 @@ fn docker_pull_progress_event(image: &str, info: &CreateImageInfo) -> Option DriverSandbox { status: "False".to_string(), reason: reason.to_string(), message: "Container exited".to_string(), - last_transition_time: String::new(), + transition_time: None, }], deleting: false, }), diff --git a/crates/openshell-driver-kubernetes-secrets/src/lib.rs b/crates/openshell-driver-kubernetes-secrets/src/lib.rs index 91224212bd..ae733d62e7 100644 --- a/crates/openshell-driver-kubernetes-secrets/src/lib.rs +++ b/crates/openshell-driver-kubernetes-secrets/src/lib.rs @@ -329,7 +329,7 @@ impl KubernetesSecretsCredentialDriver { Ok::<_, Status>(ResolvedCredential { request_id: request.request_id, value, - expires_at_ms: 0, + expiration_time: None, }) }); futures::future::try_join_all(futures).await diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index f4addd384a..483b3e3eff 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -2629,7 +2629,7 @@ fn map_kube_event_to_platform( Some(( sandbox_id, PlatformEvent { - timestamp_ms: ts, + event_time: openshell_core::time::timestamp_from_millis(ts).ok(), source: "kubernetes".to_string(), r#type: obj.type_.clone().unwrap_or_default(), reason: obj.reason.clone().unwrap_or_default(), @@ -4663,11 +4663,10 @@ fn condition_from_value(value: &serde_json::Value) -> Option { .and_then(|val| val.as_str()) .unwrap_or_default() .to_string(), - last_transition_time: obj + transition_time: obj .get("lastTransitionTime") .and_then(|val| val.as_str()) - .unwrap_or_default() - .to_string(), + .and_then(|value| value.parse().ok()), }) } diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs index d8e981a46d..7799b465a4 100644 --- a/crates/openshell-driver-mxc/src/driver.rs +++ b/crates/openshell-driver-mxc/src/driver.rs @@ -156,7 +156,7 @@ fn platform_event(sandbox_id: String, reason: &str, message: String) -> WatchSan WatchSandboxesPlatformEvent { sandbox_id, event: Some(DriverPlatformEvent { - timestamp_ms: 0, + event_time: None, source: "mxc-driver".into(), r#type: "Warning".into(), reason: reason.to_string(), @@ -397,7 +397,7 @@ impl MxcComputeBackend { status: "False".into(), reason: "Starting".into(), message: "MXC lifecycle starting".into(), - last_transition_time: String::new(), + transition_time: None, }, false, ); @@ -490,7 +490,7 @@ impl MxcComputeBackend { status: "False".into(), reason: "Stopped".into(), message: "MXC sandbox stopped".into(), - last_transition_time: String::new(), + transition_time: None, }, false, ); @@ -729,7 +729,7 @@ async fn run_lifecycle( status: "True".into(), reason: "AgentRunning".into(), message: format!("Agent exec launched: {command_line}"), - last_transition_time: String::new(), + transition_time: None, }, false, ); @@ -790,7 +790,7 @@ async fn monitor_exec( status: "True".into(), reason: "AgentCompleted".into(), message: "Agent exec finished successfully (exit code 0)".into(), - last_transition_time: String::new(), + transition_time: None, }, false, ); @@ -817,7 +817,7 @@ async fn monitor_exec( status: "False".into(), reason: "ExecFailed".into(), message: format!("Agent exec exited {code}"), - last_transition_time: String::new(), + transition_time: None, }, false, ); @@ -849,7 +849,7 @@ async fn set_failed( status: "False".into(), reason: "ProvisionFailed".into(), message: message.to_string(), - last_transition_time: String::new(), + transition_time: None, }, false, ); diff --git a/crates/openshell-driver-podman/src/watcher.rs b/crates/openshell-driver-podman/src/watcher.rs index 0c94bf72b2..af5ff7aeb9 100644 --- a/crates/openshell-driver-podman/src/watcher.rs +++ b/crates/openshell-driver-podman/src/watcher.rs @@ -315,7 +315,7 @@ async fn map_podman_event( status: "Unknown".to_string(), reason: "InspectFailed".to_string(), message: format!("Container inspect failed: {e}"), - last_transition_time: String::new(), + transition_time: None, }, false, ))) @@ -423,7 +423,7 @@ pub fn driver_sandbox_from_list_entry(entry: &ContainerListEntry) -> Option DriverCondition { ), }; - // Use Podman's state timestamps for last_transition_time: + // Use Podman's state timestamps for transition_time: // - Running/healthy states use started_at // - Stopped/exited states use finished_at - let last_transition_time = match state.status.as_str() { + let transition_time = match state.status.as_str() { "running" => state.started_at.clone().unwrap_or_default(), "exited" | "stopped" => state.finished_at.clone().unwrap_or_default(), _ => String::new(), - }; + } + .parse() + .ok(); DriverCondition { r#type: "Ready".to_string(), status: status_val.to_string(), reason: reason.to_string(), message, - last_transition_time, + transition_time, } } @@ -578,7 +580,7 @@ mod tests { assert_eq!(cond.r#type, "Ready"); assert_eq!(cond.status, "True"); assert_eq!(cond.reason, "HealthCheckPassed"); - assert_eq!(cond.last_transition_time, "2026-04-14T10:00:00Z"); + assert_eq!(cond.transition_time, "2026-04-14T10:00:00Z".parse().ok()); } #[test] @@ -595,7 +597,7 @@ mod tests { let cond = condition_from_state(&state); assert_eq!(cond.status, "False"); assert_eq!(cond.reason, "OOMKilled"); - assert_eq!(cond.last_transition_time, "2026-04-14T11:00:00Z"); + assert_eq!(cond.transition_time, "2026-04-14T11:00:00Z".parse().ok()); } #[test] @@ -682,7 +684,7 @@ mod tests { status: "Unknown".to_string(), reason: "InspectFailed".to_string(), message: "Container inspect failed: connection refused".to_string(), - last_transition_time: String::new(), + transition_time: None, }; let sandbox = DriverSandbox { diff --git a/crates/openshell-driver-vault/src/lib.rs b/crates/openshell-driver-vault/src/lib.rs index d932005708..6e007f5c62 100644 --- a/crates/openshell-driver-vault/src/lib.rs +++ b/crates/openshell-driver-vault/src/lib.rs @@ -252,7 +252,7 @@ impl VaultCredentialDriver { Ok::<_, Status>(ResolvedCredential { request_id, value, - expires_at_ms: 0, + expiration_time: None, }) } }); diff --git a/crates/openshell-driver-vm/src/driver.rs b/crates/openshell-driver-vm/src/driver.rs index 2de65c3add..2bb828a5e6 100644 --- a/crates/openshell-driver-vm/src/driver.rs +++ b/crates/openshell-driver-vm/src/driver.rs @@ -5449,7 +5449,7 @@ fn provisioning_condition() -> SandboxCondition { status: "False".to_string(), reason: "Starting".to_string(), message: "VM is starting".to_string(), - last_transition_time: String::new(), + transition_time: None, } } @@ -5459,7 +5459,7 @@ fn deleting_condition() -> SandboxCondition { status: "False".to_string(), reason: "Deleting".to_string(), message: "Sandbox is being deleted".to_string(), - last_transition_time: String::new(), + transition_time: None, } } @@ -5469,7 +5469,7 @@ fn stopped_condition() -> SandboxCondition { status: "True".to_string(), reason: "ComputeStopped".to_string(), message: "VM compute is stopped and persistent state is retained".to_string(), - last_transition_time: String::new(), + transition_time: None, } } @@ -5479,13 +5479,14 @@ fn error_condition(reason: &str, message: &str) -> SandboxCondition { status: "False".to_string(), reason: reason.to_string(), message: message.to_string(), - last_transition_time: String::new(), + transition_time: None, } } fn platform_event(source: &str, event_type: &str, reason: &str, message: String) -> PlatformEvent { let mut event = PlatformEvent { - timestamp_ms: openshell_core::time::now_ms(), + event_time: openshell_core::time::timestamp_from_millis(openshell_core::time::now_ms()) + .ok(), source: source.to_string(), r#type: event_type.to_string(), reason: reason.to_string(), diff --git a/crates/openshell-providers/Cargo.toml b/crates/openshell-providers/Cargo.toml index abf6a6f11a..89a00bd47c 100644 --- a/crates/openshell-providers/Cargo.toml +++ b/crates/openshell-providers/Cargo.toml @@ -11,6 +11,7 @@ license.workspace = true repository.workspace = true [dependencies] +prost-types = { workspace = true } glob = { workspace = true } openshell-core = { path = "../openshell-core", default-features = false } openshell-policy = { path = "../openshell-policy" } diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 6e87826c4d..9a5110ff22 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -1029,8 +1029,14 @@ fn credential_refresh_from_proto(refresh: &ProviderCredentialRefresh) -> Credent .unwrap_or(ProviderCredentialRefreshStrategy::Unspecified), token_url: refresh.token_url.clone(), scopes: refresh.scopes.clone(), - refresh_before_seconds: refresh.refresh_before_seconds, - max_lifetime_seconds: refresh.max_lifetime_seconds, + refresh_before_seconds: refresh + .refresh_before + .as_ref() + .map_or(0, |value| value.seconds), + max_lifetime_seconds: refresh + .max_lifetime + .as_ref() + .map_or(0, |value| value.seconds), material: refresh .material .iter() @@ -1057,8 +1063,14 @@ fn credential_refresh_to_proto(refresh: &CredentialRefreshProfile) -> ProviderCr strategy: refresh.strategy as i32, token_url: refresh.token_url.clone(), scopes: refresh.scopes.clone(), - refresh_before_seconds: refresh.refresh_before_seconds, - max_lifetime_seconds: refresh.max_lifetime_seconds, + refresh_before: Some(prost_types::Duration { + seconds: refresh.refresh_before_seconds, + nanos: 0, + }), + max_lifetime: Some(prost_types::Duration { + seconds: refresh.max_lifetime_seconds, + nanos: 0, + }), material: refresh .material .iter() @@ -1093,7 +1105,10 @@ fn token_grant_from_proto( jwt_svid_audience: token_grant.jwt_svid_audience.clone(), client_assertion_type: token_grant.client_assertion_type.clone(), scopes: token_grant.scopes.clone(), - cache_ttl_seconds: token_grant.cache_ttl_seconds, + cache_ttl_seconds: token_grant + .cache_ttl + .as_ref() + .map_or(0, |value| value.seconds), audience_overrides: token_grant .audience_overrides .iter() @@ -1117,7 +1132,10 @@ fn token_grant_to_proto( jwt_svid_audience: token_grant.jwt_svid_audience.clone(), client_assertion_type: token_grant.client_assertion_type.clone(), scopes: token_grant.scopes.clone(), - cache_ttl_seconds: token_grant.cache_ttl_seconds, + cache_ttl: Some(prost_types::Duration { + seconds: token_grant.cache_ttl_seconds, + nanos: 0, + }), audience_overrides: token_grant .audience_overrides .iter() diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index e70aac7a9f..f52c68f020 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -1499,8 +1499,8 @@ async fn flush_proposals_to_gateway( binary: s.binary, ancestors: s.ancestors, deny_reason: s.deny_reason, - first_seen_ms: s.first_seen_ms, - last_seen_ms: s.last_seen_ms, + first_seen_time: openshell_core::time::timestamp_from_millis(s.first_seen_ms).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(s.last_seen_ms).ok(), count: s.count, suppressed_count: 0, total_count: s.count, diff --git a/crates/openshell-sandbox/src/mechanistic_mapper.rs b/crates/openshell-sandbox/src/mechanistic_mapper.rs index 9be5f8e438..79770fdb2b 100644 --- a/crates/openshell-sandbox/src/mechanistic_mapper.rs +++ b/crates/openshell-sandbox/src/mechanistic_mapper.rs @@ -84,8 +84,14 @@ pub fn generate_proposals(summaries: &[DenialSummary]) -> Vec { for denial in denials { total_count += denial.count; - first_seen_ms = first_seen_ms.min(denial.first_seen_ms); - last_seen_ms = last_seen_ms.max(denial.last_seen_ms); + if let Some(timestamp) = denial.first_seen_time.as_ref() { + first_seen_ms = first_seen_ms + .min(openshell_core::time::timestamp_to_millis(timestamp).unwrap_or(i64::MAX)); + } + if let Some(timestamp) = denial.last_seen_time.as_ref() { + last_seen_ms = last_seen_ms + .max(openshell_core::time::timestamp_to_millis(timestamp).unwrap_or_default()); + } if denial.denial_stage == "ssrf" { is_ssrf = true; } @@ -218,13 +224,13 @@ pub fn generate_proposals(summaries: &[DenialSummary]) -> Vec { security_notes, confidence, denial_summary_ids: vec![], - created_at_ms: 0, // Set by gateway on persist - decided_at_ms: 0, + created_time: None, // Set by gateway on persist + decided_time: None, stage, supersedes_chunk_id: String::new(), hit_count: total_count.cast_signed(), - first_seen_ms, - last_seen_ms, + first_seen_time: openshell_core::time::timestamp_from_millis(first_seen_ms).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(last_seen_ms).ok(), binary: binary.clone(), validation_result: String::new(), rejection_reason: String::new(), @@ -511,8 +517,8 @@ mod tests { binary: "/usr/bin/curl".to_string(), ancestors: vec![], deny_reason: "no matching policy".to_string(), - first_seen_ms: 1000, - last_seen_ms: 2000, + first_seen_time: openshell_core::time::timestamp_from_millis(1000).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(2000).ok(), count: 5, suppressed_count: 0, total_count: 5, @@ -559,8 +565,8 @@ mod tests { binary: "/usr/bin/python3".to_string(), ancestors: vec![], deny_reason: "l7 deny".to_string(), - first_seen_ms: 1000, - last_seen_ms: 2000, + first_seen_time: openshell_core::time::timestamp_from_millis(1000).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(2000).ok(), count: 3, suppressed_count: 0, total_count: 3, @@ -663,8 +669,8 @@ mod tests { port: 80, binary: "/usr/bin/curl".to_string(), count: 5, - first_seen_ms: 1000, - last_seen_ms: 2000, + first_seen_time: openshell_core::time::timestamp_from_millis(1000).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(2000).ok(), denial_stage: "ssrf".to_string(), ..Default::default() }]; @@ -683,8 +689,8 @@ mod tests { port: 80, binary: "/usr/bin/curl".to_string(), count: 5, - first_seen_ms: 1000, - last_seen_ms: 2000, + first_seen_time: openshell_core::time::timestamp_from_millis(1000).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(2000).ok(), denial_stage: "ssrf".to_string(), ..Default::default() }]; @@ -703,8 +709,8 @@ mod tests { port: 80, binary: "/usr/bin/curl".to_string(), count: 5, - first_seen_ms: 1000, - last_seen_ms: 2000, + first_seen_time: openshell_core::time::timestamp_from_millis(1000).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(2000).ok(), denial_stage: "ssrf".to_string(), ..Default::default() }]; @@ -723,8 +729,8 @@ mod tests { port: 8080, binary: "/usr/bin/curl".to_string(), count: 3, - first_seen_ms: 1000, - last_seen_ms: 2000, + first_seen_time: openshell_core::time::timestamp_from_millis(1000).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(2000).ok(), denial_stage: "ssrf".to_string(), ..Default::default() }]; @@ -743,8 +749,8 @@ mod tests { port: 443, binary: "/usr/bin/curl".to_string(), count: 5, - first_seen_ms: 1000, - last_seen_ms: 2000, + first_seen_time: openshell_core::time::timestamp_from_millis(1000).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(2000).ok(), denial_stage: "connect".to_string(), ..Default::default() }]; diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index a02b5735ae..c51fc8a59d 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -404,9 +404,11 @@ impl OpenShellClient { command: cmd.to_vec(), workdir: opts.workdir.unwrap_or_default(), environment: opts.environment, - timeout_seconds: opts + execution_timeout: opts .timeout - .map_or(0, |d| u32::try_from(d.as_secs()).unwrap_or(u32::MAX)), + .map(openshell_core::time::duration_from_std) + .transpose() + .map_err(|error| SdkError::invalid_config(error.to_string()))?, stdin: opts.stdin.unwrap_or_default(), tty: false, cols: 0, @@ -710,9 +712,11 @@ impl WorkspaceScopedClient { command: cmd.to_vec(), workdir: opts.workdir.unwrap_or_default(), environment: opts.environment, - timeout_seconds: opts + execution_timeout: opts .timeout - .map_or(0, |d| u32::try_from(d.as_secs()).unwrap_or(u32::MAX)), + .map(openshell_core::time::duration_from_std) + .transpose() + .map_err(|error| SdkError::invalid_config(error.to_string()))?, stdin: opts.stdin.unwrap_or_default(), tty: false, cols: 0, diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 1cdac7da41..711765f6c8 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -17,6 +17,7 @@ use openshell_sdk::{ use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; +use std::time::Duration; use tokio::net::TcpListener; use tokio::sync::Mutex; use tokio_stream::wrappers::TcpListenerStream; @@ -65,11 +66,11 @@ fn sandbox_with_phase_ws( metadata: Some(proto::datamodel::v1::ObjectMeta { id: format!("id-{name}"), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), annotations: HashMap::new(), resource_version: 1, - deletion_timestamp_ms: 0, + deletion_time: None, workspace: workspace.to_string(), }), spec: None, @@ -85,11 +86,11 @@ fn workspace_proto(name: &str, phase: proto::datamodel::v1::WorkspacePhase) -> p metadata: Some(proto::datamodel::v1::ObjectMeta { id: format!("ws-{name}"), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 1, - deletion_timestamp_ms: 0, + deletion_time: None, workspace: String::new(), }), status: Some(proto::datamodel::v1::WorkspaceStatus { @@ -908,7 +909,7 @@ async fn wait_ready_transitions_through_phases() { let client = connect(&endpoint).await; let sandbox = client - .wait_ready("my-box", std::time::Duration::from_secs(5)) + .wait_ready("my-box", Duration::from_secs(5)) .await .unwrap(); assert_eq!(sandbox.phase, SandboxPhase::Ready); @@ -928,7 +929,7 @@ async fn wait_ready_accepts_successful_completion() { let client = connect(&endpoint).await; let sandbox = client - .wait_ready("short-job", std::time::Duration::from_secs(5)) + .wait_ready("short-job", Duration::from_secs(5)) .await .unwrap(); assert_eq!(sandbox.phase, SandboxPhase::Completed); @@ -944,7 +945,7 @@ async fn wait_ready_surfaces_stopped_phase_without_timing_out() { let client = connect(&endpoint).await; let err = client - .wait_ready("failed-job", std::time::Duration::from_secs(5)) + .wait_ready("failed-job", Duration::from_secs(5)) .await .unwrap_err(); assert_eq!(err.code(), "connect"); @@ -960,7 +961,7 @@ async fn wait_ready_surfaces_error_phase() { let client = connect(&endpoint).await; let err = client - .wait_ready("my-box", std::time::Duration::from_secs(5)) + .wait_ready("my-box", Duration::from_secs(5)) .await .unwrap_err(); assert_eq!(err.code(), "connect"); @@ -977,7 +978,7 @@ async fn wait_deleted_returns_when_get_reports_not_found() { let client = connect(&endpoint).await; client - .wait_deleted("my-box", std::time::Duration::from_secs(5)) + .wait_deleted("my-box", Duration::from_secs(5)) .await .unwrap(); assert!(state.get_calls.load(Ordering::SeqCst) >= 3); @@ -1011,7 +1012,7 @@ async fn exec_buffers_stdout_stderr_and_exit() { &["echo".to_string(), "hello".to_string()], ExecOptions { workdir: Some("/work".to_string()), - timeout: Some(std::time::Duration::from_secs(10)), + timeout: Some(Duration::from_secs(10)), ..Default::default() }, ) @@ -1029,7 +1030,13 @@ async fn exec_buffers_stdout_stderr_and_exit() { vec!["echo".to_string(), "hello".to_string()] ); assert_eq!(observed.workdir, "/work"); - assert_eq!(observed.timeout_seconds, 10); + assert_eq!( + observed + .execution_timeout + .as_ref() + .and_then(|value| openshell_core::time::duration_to_std(value).ok()), + Some(Duration::from_secs(10)) + ); } /// Refresher that hands out a fixed "fresh-token" and counts invocations. diff --git a/crates/openshell-server/src/auth/workspace_authz.rs b/crates/openshell-server/src/auth/workspace_authz.rs index e23d2287a3..5ff1a942f6 100644 --- a/crates/openshell-server/src/auth/workspace_authz.rs +++ b/crates/openshell-server/src/auth/workspace_authz.rs @@ -234,12 +234,12 @@ mod tests { metadata: Some(ObjectMeta { id: uuid::Uuid::new_v4().to_string(), name: subject.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), principal_subject: subject.to_string(), role: role.into(), diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 3181f339a9..0317543122 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -1519,7 +1519,7 @@ impl ComputeRuntime { status: "False".to_string(), reason: reason.clone(), message: message.clone(), - last_transition_time: String::new(), + transition_time: None, }, ); }, @@ -2541,7 +2541,7 @@ impl ComputeRuntime { status: "False".to_string(), reason: reason.clone(), message: message.clone(), - last_transition_time: String::new(), + transition_time: None, }, ); }) @@ -2581,7 +2581,7 @@ impl ComputeRuntime { status: "False".to_string(), reason: "Resumed".to_string(), message: "Sandbox recovered during gateway startup".to_string(), - last_transition_time: String::new(), + transition_time: None, }, ); }) @@ -3515,7 +3515,7 @@ impl ComputeRuntime { reason: "ComputeResourceMissing".to_string(), message: "The compute driver could not find the retained sandbox resource; delete the sandbox to clean up its remaining state" .to_string(), - last_transition_time: String::new(), + transition_time: None, }, ); }, @@ -3614,7 +3614,7 @@ fn apply_main_process_exit(sandbox: &mut Sandbox, instance_id: &str, exit_code: status: "False".to_string(), reason: reason.to_string(), message, - last_transition_time: String::new(), + transition_time: None, }, ); sandbox.set_phase(phase as i32); @@ -3952,7 +3952,7 @@ fn driver_condition_from_public(condition: &SandboxCondition) -> DriverCondition status: condition.status.clone(), reason: condition.reason.clone(), message: condition.message.clone(), - last_transition_time: condition.last_transition_time.clone(), + transition_time: condition.transition_time, } } @@ -4150,7 +4150,7 @@ fn ensure_supervisor_ready_status(status: &mut Option, sandbox_na status: "True".to_string(), reason: "DependenciesReady".to_string(), message: "Supervisor session connected".to_string(), - last_transition_time: String::new(), + transition_time: None, }, ); } @@ -4211,7 +4211,7 @@ fn ensure_supervisor_not_connected_status(status: &mut Option, sa status: "False".to_string(), reason: "SupervisorNotConnected".to_string(), message: "Backend ready; waiting for supervisor session".to_string(), - last_transition_time: String::new(), + transition_time: None, }, ); } @@ -4225,7 +4225,7 @@ fn ensure_supervisor_not_ready_status(status: &mut Option, sandbo status: "False".to_string(), reason: "DependenciesNotReady".to_string(), message: "Supervisor session disconnected".to_string(), - last_transition_time: String::new(), + transition_time: None, }, ); } @@ -4257,13 +4257,13 @@ fn public_condition_from_driver(condition: &DriverCondition) -> SandboxCondition status: condition.status.clone(), reason: condition.reason.clone(), message: condition.message.clone(), - last_transition_time: condition.last_transition_time.clone(), + transition_time: condition.transition_time, } } fn public_platform_event_from_driver(event: &DriverPlatformEvent) -> PlatformEvent { PlatformEvent { - timestamp_ms: event.timestamp_ms, + event_time: event.event_time, source: event.source.clone(), r#type: event.r#type.clone(), reason: event.reason.clone(), @@ -5364,12 +5364,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Default::default() }; @@ -5682,7 +5682,7 @@ mod tests { status: "False".to_string(), reason: reason.to_string(), message: String::new(), - last_transition_time: String::new(), + transition_time: None, }); sandbox } @@ -5692,17 +5692,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: format!("session-{id}"), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), sandbox_id: sandbox_id.to_string(), token: format!("token-{id}"), revoked: false, - expires_at_ms: 0, + expiration_time: None, } } @@ -5711,12 +5711,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: format!("{}--web", sandbox.object_name()), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: sandbox.object_workspace().to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), sandbox_id: sandbox.object_id().to_string(), sandbox_name: sandbox.object_name().to_string(), @@ -5849,7 +5849,7 @@ mod tests { status: "False".to_string(), reason: reason.to_string(), message: message.to_string(), - last_transition_time: String::new(), + transition_time: None, } } @@ -5881,7 +5881,7 @@ mod tests { status: "True".to_string(), reason: "BackendReady".to_string(), message: "Container is running".to_string(), - last_transition_time: String::new(), + transition_time: None, }], deleting: false, }), @@ -6062,7 +6062,7 @@ mod tests { status: "True".to_string(), reason: "DependenciesReady".to_string(), message: "Pod is Ready; Service Exists".to_string(), - last_transition_time: String::new(), + transition_time: None, }], ..make_driver_status(make_driver_condition("", "")) }; @@ -6197,7 +6197,7 @@ mod tests { status: "False".to_string(), reason: "Unschedulable".to_string(), message: "0/1 nodes are available: 1 Insufficient nvidia.com/gpu.".to_string(), - last_transition_time: String::new(), + transition_time: None, }], ..Default::default() }); @@ -6230,7 +6230,7 @@ mod tests { status: "False".to_string(), reason: "Unschedulable".to_string(), message: original.to_string(), - last_transition_time: String::new(), + transition_time: None, }], ..Default::default() }); @@ -6853,7 +6853,7 @@ mod tests { status: "False".to_string(), reason: "PodTerminating".to_string(), message: "Pod is terminating. Sandbox is stopping".to_string(), - last_transition_time: String::new(), + transition_time: None, }, make_driver_condition("SandboxStopped", "Sandbox is stopping"), ], @@ -7069,14 +7069,14 @@ mod tests { status: "True".to_string(), reason: "DependenciesReady".to_string(), message: "Sandbox is ready".to_string(), - last_transition_time: String::new(), + transition_time: None, }, DriverCondition { r#type: "Suspended".to_string(), status: "True".to_string(), reason: "PodTerminated".to_string(), message: "Pod terminated".to_string(), - last_transition_time: String::new(), + transition_time: None, }, ], ..Default::default() @@ -7206,7 +7206,7 @@ mod tests { status: "True".to_string(), reason: "BackendReady".to_string(), message: "Container is running".to_string(), - last_transition_time: String::new(), + transition_time: None, }], deleting: false, }), @@ -8388,7 +8388,7 @@ mod tests { status: "True".to_string(), reason: "DependenciesReady".to_string(), message: "Pod is Ready".to_string(), - last_transition_time: String::new(), + transition_time: None, }], current_policy_version: 7, ..Default::default() @@ -8516,7 +8516,7 @@ mod tests { status: "True".to_string(), reason: "DependenciesReady".to_string(), message: "Supervisor session connected".to_string(), - last_transition_time: String::new(), + transition_time: None, }], ..Default::default() }); @@ -8566,7 +8566,7 @@ mod tests { status: "True".to_string(), reason: "BackendReady".to_string(), message: "Container is running".to_string(), - last_transition_time: String::new(), + transition_time: None, }], deleting: false, } @@ -8583,7 +8583,7 @@ mod tests { status: "False".to_string(), reason: "Deleting".to_string(), message: "Container is being removed".to_string(), - last_transition_time: String::new(), + transition_time: None, }], deleting: true, } @@ -8860,7 +8860,7 @@ mod tests { status: "False".to_string(), reason: "DependenciesNotReady".to_string(), message: "Pod is Pending".to_string(), - last_transition_time: String::new(), + transition_time: None, }], deleting: false, }), @@ -8881,7 +8881,7 @@ mod tests { status: "True".to_string(), reason: "DependenciesReady".to_string(), message: "Pod is Ready".to_string(), - last_transition_time: String::new(), + transition_time: None, }], deleting: false, }), @@ -9054,7 +9054,7 @@ mod tests { status: "True".to_string(), reason: "DependenciesReady".to_string(), message: "Pod is Ready".to_string(), - last_transition_time: String::new(), + transition_time: None, })), workspace: "default".to_string(), }], @@ -9094,7 +9094,7 @@ mod tests { status: "True".to_string(), reason: "DependenciesReady".to_string(), message: "Pod is Ready".to_string(), - last_transition_time: String::new(), + transition_time: None, }], deleting: false, }), @@ -10202,12 +10202,12 @@ mod tests { sandbox.metadata = Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-new".to_string(), name: "test-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }); let created = runtime.create_sandbox(sandbox, None, false).await.unwrap(); diff --git a/crates/openshell-server/src/config_file.rs b/crates/openshell-server/src/config_file.rs index 74b6aad01b..4efde2b7e8 100644 --- a/crates/openshell-server/src/config_file.rs +++ b/crates/openshell-server/src/config_file.rs @@ -271,7 +271,21 @@ impl TryFrom<&MiddlewareServiceFileConfig> for SupervisorMiddlewareService { name: config.name.clone(), grpc_endpoint: config.grpc_endpoint.clone(), max_payload_bytes: config.max_payload_bytes, - timeout: config.timeout.clone().unwrap_or_default(), + request_timeout: config + .timeout + .as_deref() + .map(openshell_core::middleware::parse_middleware_timeout) + .transpose() + .map_err(|_| ConfigFileError::InvalidValue { + field: "openshell.supervisor_middleware.services.timeout", + message: "must be a duration between 10ms and 30s", + })? + .map(openshell_core::time::duration_from_std) + .transpose() + .map_err(|_| ConfigFileError::InvalidValue { + field: "openshell.supervisor_middleware.services.timeout", + message: "duration is outside the protobuf range", + })?, tls_ca_cert_pem, audience: config .audience @@ -742,7 +756,7 @@ timeout = "2s" let registration = SupervisorMiddlewareService::try_from(&file.openshell.supervisor.middleware[0]) .expect("valid CA resolves"); - assert_eq!(registration.timeout, "2s"); + assert_eq!(registration.request_timeout.unwrap().seconds, 2); let registered_pem = String::from_utf8(registration.tls_ca_cert_pem) .expect("registered CA remains PEM text") .replace("\r\n", "\n"); diff --git a/crates/openshell-server/src/credentials.rs b/crates/openshell-server/src/credentials.rs index 12d2835be3..a29e51d098 100644 --- a/crates/openshell-server/src/credentials.rs +++ b/crates/openshell-server/src/credentials.rs @@ -683,14 +683,22 @@ impl CredentialRuntime { // Check provider-level expiration let provider_expires_at_ms = provider - .credential_expires_at_ms + .credential_expiration_times .get(&credential_key) - .copied() + .map(openshell_core::time::timestamp_to_millis) + .transpose() + .map_err(|error| Status::invalid_argument(error.to_string()))? + .unwrap_or(0); + let driver_expires_at_ms = response + .expiration_time + .as_ref() + .map(openshell_core::time::timestamp_to_millis) + .transpose() + .map_err(|error| Status::internal(error.to_string()))? .unwrap_or(0); // Compute effective expiration (earliest non-zero timestamp) - let effective_expires_at_ms = match (provider_expires_at_ms, response.expires_at_ms) - { + let effective_expires_at_ms = match (provider_expires_at_ms, driver_expires_at_ms) { (0, driver) => driver, (provider, 0) => provider, (provider, driver) => provider.min(driver), @@ -701,7 +709,7 @@ impl CredentialRuntime { provider_name = %provider_name, credential_key = %credential_key, provider_expires_at_ms, - driver_expires_at_ms = response.expires_at_ms, + driver_expires_at_ms, effective_expires_at_ms, "skipping expired handle-backed credential" ); @@ -1817,7 +1825,7 @@ impl CredentialDriver for TestStaticCredentialDriver { responses.push(ResolvedCredential { request_id: request.request_id, value, - expires_at_ms: 0, + expiration_time: None, }); } diff --git a/crates/openshell-server/src/grpc/auth_rpc.rs b/crates/openshell-server/src/grpc/auth_rpc.rs index 104d639584..06e2a2aa5f 100644 --- a/crates/openshell-server/src/grpc/auth_rpc.rs +++ b/crates/openshell-server/src/grpc/auth_rpc.rs @@ -100,7 +100,10 @@ pub async fn handle_issue_sandbox_token( ); Ok(Response::new(IssueSandboxTokenResponse { token: minted.token, - expires_at_ms: minted.expires_at_ms, + expiration_time: openshell_core::time::optional_timestamp_from_legacy_millis( + minted.expires_at_ms, + ) + .map_err(|error| Status::internal(error.to_string()))?, })) } @@ -187,7 +190,10 @@ pub async fn handle_refresh_sandbox_token( Ok(Response::new(RefreshSandboxTokenResponse { token: minted.token, - expires_at_ms: minted.expires_at_ms, + expiration_time: openshell_core::time::optional_timestamp_from_legacy_millis( + minted.expires_at_ms, + ) + .map_err(|error| Status::internal(error.to_string()))?, extension_credentials, })) } @@ -257,7 +263,10 @@ fn mint_extension_credentials( Ok(ExtensionServiceCredential { service_name: name.clone(), token: minted.token, - expires_at_ms: minted.expires_at_ms, + expiration_time: openshell_core::time::optional_timestamp_from_legacy_millis( + minted.expires_at_ms, + ) + .map_err(|error| Status::internal(error.to_string()))?, }) }) .collect() @@ -337,12 +346,12 @@ mod tests { metadata: Some(ObjectMeta { id: sandbox_id.to_string(), name: sandbox_id.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::default(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -401,7 +410,7 @@ mod tests { .expect("refresh OK") .into_inner(); assert!(!resp.token.is_empty()); - assert!(resp.expires_at_ms > 0); + assert!(resp.expiration_time.is_some()); } #[tokio::test] @@ -424,7 +433,7 @@ mod tests { assert_eq!(credentials.len(), 1); assert_eq!(credentials[0].service_name, "content-guard"); assert!(!credentials[0].token.is_empty()); - assert!(credentials[0].expires_at_ms > 0); + assert!(credentials[0].expiration_time.is_some()); let error = mint_extension_credentials( issuer, @@ -524,7 +533,7 @@ mod tests { .expect("issue OK") .into_inner(); assert!(!resp.token.is_empty()); - assert!(resp.expires_at_ms > 0); + assert!(resp.expiration_time.is_some()); } #[tokio::test] diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 0bda93a15f..239a911e58 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -2606,11 +2606,13 @@ async fn compute_provider_env_revision_with_catalog_and_policy_bindings( for key in credential_keys { hasher.update(key.as_bytes()); } - let mut expiry_keys: Vec<_> = provider.credential_expires_at_ms.keys().collect(); + let mut expiry_keys: Vec<_> = provider.credential_expiration_times.keys().collect(); expiry_keys.sort(); for key in expiry_keys { hasher.update(key.as_bytes()); - hasher.update(provider.credential_expires_at_ms[key].to_le_bytes()); + let value = &provider.credential_expiration_times[key]; + hasher.update(value.seconds.to_le_bytes()); + hasher.update(value.nanos.to_le_bytes()); } } None => { @@ -2667,11 +2669,13 @@ fn compute_provider_env_revision_from_records_and_policy_bindings( for key in credential_keys { hasher.update(key.as_bytes()); } - let mut expiry_keys: Vec<_> = provider.credential_expires_at_ms.keys().collect(); + let mut expiry_keys: Vec<_> = provider.credential_expiration_times.keys().collect(); expiry_keys.sort(); for key in expiry_keys { hasher.update(key.as_bytes()); - hasher.update(provider.credential_expires_at_ms[key].to_le_bytes()); + let value = &provider.credential_expiration_times[key]; + hasher.update(value.seconds.to_le_bytes()); + hasher.update(value.nanos.to_le_bytes()); } } @@ -3110,13 +3114,15 @@ pub(super) async fn handle_get_sandbox_provider_environment( "withholding unbound static provider credential from binding-capable supervisor" ); provider_environment.environment.remove(&key); - provider_environment.credential_expires_at_ms.remove(&key); + provider_environment + .credential_expiration_times + .remove(&key); provider_environment.static_credential_keys.remove(&key); } } else { for key in &provider_environment.static_credential_keys { provider_environment.environment.remove(key); - provider_environment.credential_expires_at_ms.remove(key); + provider_environment.credential_expiration_times.remove(key); } provider_environment.static_credential_bindings.clear(); } @@ -3136,10 +3142,20 @@ pub(super) async fn handle_get_sandbox_provider_environment( .cloned() .collect(); + let credential_expiration_times = provider_environment + .credential_expiration_times + .into_iter() + .filter_map(|(key, value)| { + openshell_core::time::optional_timestamp_from_legacy_millis(value) + .ok() + .flatten() + .map(|timestamp| (key, timestamp)) + }) + .collect(); Ok(Response::new(GetSandboxProviderEnvironmentResponse { environment: provider_environment.environment, provider_env_revision, - credential_expires_at_ms: provider_environment.credential_expires_at_ms, + credential_expiration_times, dynamic_credentials: provider_environment.dynamic_credentials, static_credential_bindings: provider_environment.static_credential_bindings, non_secret_environment_keys, @@ -4090,7 +4106,17 @@ pub(super) async fn handle_get_sandbox_logs( if let Some(openshell_core::proto::sandbox_stream_event::Payload::Log(log)) = evt.payload { - if req.since_ms > 0 && log.timestamp_ms < req.since_ms { + let since_ms = req + .since_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or_default(); + let event_ms = log + .event_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or_default(); + if since_ms > 0 && event_ms < since_ms { return None; } if !req.sources.is_empty() && !source_matches(&log.source, &req.sources) { @@ -4457,16 +4483,16 @@ pub(super) async fn handle_submit_policy_analysis( port: ep_port, binary: ep_binary, hit_count: chunk.hit_count.clamp(1, 100), - first_seen_ms: if chunk.first_seen_ms > 0 { - chunk.first_seen_ms - } else { - now_ms - }, - last_seen_ms: if chunk.last_seen_ms > 0 { - chunk.last_seen_ms - } else { - now_ms - }, + first_seen_ms: chunk + .first_seen_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or(now_ms), + last_seen_ms: chunk + .last_seen_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or(now_ms), validation_result: evaluation.validation_result.clone(), rejection_reason: String::new(), application_error: evaluation.application_error.clone(), @@ -4643,7 +4669,10 @@ pub(super) async fn handle_get_draft_policy( .map(|r| draft_chunk_record_to_proto(&r)) .collect::, _>>()?; - let last_analyzed_at_ms = chunks.iter().map(|c| c.created_at_ms).max().unwrap_or(0); + let last_analyzed_time = chunks + .iter() + .filter_map(|chunk| chunk.created_time) + .max_by_key(|value| (value.seconds, value.nanos)); debug!( sandbox_id = %sandbox_id, @@ -4656,7 +4685,7 @@ pub(super) async fn handle_get_draft_policy( chunks, rolling_summary: String::new(), draft_version: u64::try_from(draft_version).unwrap_or(0), - last_analyzed_at_ms, + last_analyzed_time, })) } @@ -5509,7 +5538,11 @@ pub(super) async fn handle_get_draft_history( for chunk in &all_chunks { entries.push(DraftHistoryEntry { - timestamp_ms: chunk.created_at_ms, + event_time: openshell_core::time::optional_timestamp_from_legacy_millis( + chunk.created_at_ms, + ) + .ok() + .flatten(), event_type: "proposed".to_string(), description: format!( "Rule '{}' proposed (confidence: {:.0}%)", @@ -5521,7 +5554,9 @@ pub(super) async fn handle_get_draft_history( if let Some(decided_at) = chunk.decided_at_ms { entries.push(DraftHistoryEntry { - timestamp_ms: decided_at, + event_time: openshell_core::time::optional_timestamp_from_legacy_millis(decided_at) + .ok() + .flatten(), event_type: chunk.status.clone(), description: format!("Rule '{}' {}", chunk.rule_name, chunk.status), chunk_id: chunk.id.clone(), @@ -5529,7 +5564,12 @@ pub(super) async fn handle_get_draft_history( } } - entries.sort_by_key(|e| e.timestamp_ms); + entries.sort_by_key(|entry| { + entry + .event_time + .as_ref() + .map_or((0, 0), |value| (value.seconds, value.nanos)) + }); debug!( sandbox_id = %sandbox_id, @@ -5842,11 +5882,25 @@ fn draft_chunk_record_to_proto(record: &DraftChunkRecord) -> Result San policy_hash: record.policy_hash.clone(), status: status.into(), load_error: record.load_error.clone().unwrap_or_default(), - created_at_ms: record.created_at_ms, - loaded_at_ms: record.loaded_at_ms.unwrap_or(0), + created_time: openshell_core::time::optional_timestamp_from_legacy_millis( + record.created_at_ms, + ) + .ok() + .flatten(), + loaded_time: record + .loaded_at_ms + .and_then(|value| openshell_core::time::timestamp_from_millis(value).ok()), policy, provenance: record.provenance.clone(), } @@ -7394,12 +7454,12 @@ mod tests { metadata: Some(ObjectMeta { id: "sandbox-b-id".to_string(), name: "sandbox-b".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "workspace-b".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Sandbox::default() }; @@ -7409,12 +7469,12 @@ mod tests { metadata: Some(ObjectMeta { id: "member-a-id".to_string(), name: "test-user".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), principal_subject: "test-user".to_string(), role: WorkspaceRole::User.into(), @@ -7450,12 +7510,12 @@ mod tests { metadata: Some(ObjectMeta { id: "default-admin-member-id".to_string(), name: "test-user".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), principal_subject: "test-user".to_string(), role: WorkspaceRole::Admin.into(), @@ -7654,12 +7714,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -7690,12 +7750,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-self".to_string(), name: "self".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -7725,12 +7785,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -7763,12 +7823,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -7855,12 +7915,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-x".to_string(), name: "x".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -7938,12 +7998,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-no-policy".to_string(), name: "no-policy-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -7967,18 +8027,18 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("provider-{name}"), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: std::iter::once(("GITHUB_TOKEN".to_string(), "ghp-test".to_string())) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), } @@ -8069,12 +8129,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(policy), @@ -8343,12 +8403,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-generic".to_string(), name: "generic".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(openshell_core::proto::ProviderProfile { id: "generic".to_string(), @@ -8395,12 +8455,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-custom-api".to_string(), name: "custom-api".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(openshell_core::proto::ProviderProfile { id: "custom-api".to_string(), @@ -8467,12 +8527,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-custom-api".to_string(), name: "custom-api".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(openshell_core::proto::ProviderProfile { id: "custom-api".to_string(), @@ -8607,12 +8667,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("profile-{id}-{workspace}"), name: id.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(openshell_core::proto::ProviderProfile { id: id.to_string(), @@ -9294,12 +9354,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-ambiguous".to_string(), name: "ambiguous".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: "ambiguous".to_string(), @@ -9366,12 +9426,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-tls-skip".to_string(), name: "tls-skip".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: "tls-skip".to_string(), @@ -9528,12 +9588,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-custom-policy".to_string(), name: "custom-policy".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: "custom-policy".to_string(), @@ -10083,12 +10143,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-custom-dynamic".to_string(), name: "custom-dynamic".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: "custom-dynamic".to_string(), @@ -10172,12 +10232,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-token-exchange-subject".to_string(), name: "token-exchange-subject".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: "token-exchange-subject".to_string(), @@ -10224,8 +10284,10 @@ mod tests { "subject_token".to_string(), "raw-gateway-oidc-token".to_string(), )]); - provider.credential_expires_at_ms = - HashMap::from([("subject_token".to_string(), current_time_ms() + 60_000)]); + provider.credential_expiration_times = HashMap::from([( + "subject_token".to_string(), + openshell_core::time::timestamp_from_millis(current_time_ms() + 60_000).unwrap(), + )]); state.store.put_message(&provider).await.unwrap(); state .store @@ -10261,7 +10323,7 @@ mod tests { ); assert!( !response - .credential_expires_at_ms + .credential_expiration_times .contains_key("subject_token"), "withheld subject credentials must not emit sandbox expiry metadata" ); @@ -10381,12 +10443,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("profile-{id}"), name: id.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: id.to_string(), @@ -10566,12 +10628,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-custom-token".to_string(), name: "custom-token".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: "custom-token".to_string(), @@ -10689,12 +10751,12 @@ mod tests { } ), name: "scoped-revision".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: "scoped-revision".to_string(), @@ -11084,12 +11146,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-global-profile".to_string(), name: "global-profile-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(sandbox_policy), @@ -11176,12 +11238,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-backfill".to_string(), name: "backfill-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -12123,12 +12185,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-draft-flow".to_string(), name: "draft-flow".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -12165,8 +12227,8 @@ mod tests { rationale: "observed denied request".to_string(), confidence: 0.85, hit_count: 3, - first_seen_ms: 100, - last_seen_ms: 200, + first_seen_time: openshell_core::time::timestamp_from_millis(100).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(200).ok(), binary: "/usr/bin/curl".to_string(), ..Default::default() }], @@ -12357,12 +12419,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-feedback".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -12458,12 +12520,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-agent-l7-verdict".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -12577,12 +12639,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-supersede-flow".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -12786,12 +12848,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-mechanistic-clean".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -12897,12 +12959,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-mechanistic-existing-rest".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(base_policy), @@ -13010,7 +13072,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-invalid-graphql-preflight".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), workspace: "default".to_string(), ..Default::default() }), @@ -13092,7 +13154,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: sandbox_id.to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), workspace: "default".to_string(), ..Default::default() }), @@ -13318,12 +13380,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-l7-full-with-cred".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -13427,12 +13489,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-default-manual-mode".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -13524,12 +13586,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-unknown-mode".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -13613,12 +13675,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-explicit-manual-mode".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -13704,12 +13766,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-gateway-auto-mode".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -13795,12 +13857,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-gateway-pinned-manual".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -13891,12 +13953,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-reject-provider-prefix".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -14072,12 +14134,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-agent-l4-with-cred".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -14173,12 +14235,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-agent-l4-no-cred".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -14262,12 +14324,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-agent-link-local".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -14360,12 +14422,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "profile-custom-api".to_string(), name: "custom-api".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: "custom-api".to_string(), @@ -14405,12 +14467,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-agent-provider-effective-policy".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -14534,12 +14596,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-full-loop-v2".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -14724,12 +14786,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-redraft".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -14843,12 +14905,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-mech-dedup".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -14951,12 +15013,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: sandbox_id.to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: std::collections::HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: Some(SandboxPolicy { @@ -15225,12 +15287,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-undo-clears".to_string(), name: sandbox_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -15354,12 +15416,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-draft-owner".to_string(), name: "draft-owner".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -15373,12 +15435,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-draft-other".to_string(), name: "draft-other".to_string(), - created_at_ms: 1_000_001, + created_time: openshell_core::time::timestamp_from_millis(1_000_001).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -15414,8 +15476,8 @@ mod tests { rationale: "observed denied request".to_string(), confidence: 0.85, hit_count: 3, - first_seen_ms: 100, - last_seen_ms: 200, + first_seen_time: openshell_core::time::timestamp_from_millis(100).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(200).ok(), binary: "/usr/bin/curl".to_string(), ..Default::default() }], @@ -16452,12 +16514,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("profile-{suffix}"), name: profile_name.clone(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(ProviderProfile { id: profile_name.clone(), @@ -17419,12 +17481,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: sb_id.to_string(), name: sb_name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: ws.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -17561,12 +17623,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-1".to_string(), name: "test-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, // No policy yet - will be backfilled @@ -17654,7 +17716,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-annotated-backfill".to_string(), name: "annotated-backfill".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::from([( @@ -17662,7 +17724,7 @@ mod tests { "keep".to_string(), )]), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -18087,7 +18149,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-preserve-backfill".to_string(), name: "preserve-backfill".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::from([( @@ -18095,7 +18157,7 @@ mod tests { "keep".to_string(), )]), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -18254,12 +18316,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-sync-strip".to_string(), name: "sync-strip".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -18360,12 +18422,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-1".to_string(), name: "test-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, @@ -18455,12 +18517,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sb-1".to_string(), name: "test-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { policy: None, diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index c8c8149a82..d8e957c113 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -63,7 +63,7 @@ fn redact_provider_credentials(mut provider: Provider) -> Provider { #[derive(Debug, Clone, Default, PartialEq)] pub(super) struct ProviderEnvironment { pub environment: HashMap, - pub credential_expires_at_ms: HashMap, + pub credential_expiration_times: HashMap, pub dynamic_credentials: HashMap, pub static_credential_bindings: HashMap, pub static_credential_keys: HashSet, @@ -137,12 +137,12 @@ async fn create_provider_record_validating( provider.metadata = Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: uuid::Uuid::new_v4().to_string(), name: generate_name(), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }); } @@ -419,9 +419,9 @@ async fn update_provider_record_validating( .collect::>(); candidate.credentials = merge_map(candidate.credentials, provider.credentials); candidate.config = merge_map(candidate.config, provider.config); - candidate.credential_expires_at_ms = merge_i64_map( - candidate.credential_expires_at_ms, - provider.credential_expires_at_ms, + candidate.credential_expiration_times = merge_timestamp_map( + candidate.credential_expiration_times, + provider.credential_expiration_times, ); // Validate BEFORE writing to prevent persisting invalid state. @@ -704,19 +704,15 @@ fn merge_map( existing } -fn merge_i64_map( - mut existing: HashMap, - incoming: HashMap, -) -> HashMap { +fn merge_timestamp_map( + mut existing: HashMap, + incoming: HashMap, +) -> HashMap { if incoming.is_empty() { return existing; } for (key, value) in incoming { - if value <= 0 { - existing.remove(&key); - } else { - existing.insert(key, value); - } + existing.insert(key, value); } existing } @@ -1167,9 +1163,9 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin continue; } let expires_at_ms = provider - .credential_expires_at_ms + .credential_expiration_times .get(key) - .copied() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) .unwrap_or_default(); if expires_at_ms > 0 && expires_at_ms <= now_ms { warn!( @@ -1282,7 +1278,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin Ok(ProviderEnvironment { environment: env, - credential_expires_at_ms: expires, + credential_expiration_times: expires, dynamic_credentials: resolve_dynamic_credentials_from_records(catalog, records), static_credential_bindings, static_credential_keys, @@ -1297,7 +1293,7 @@ fn refresh_authorization_epochs_by_key( if state .metadata .as_ref() - .is_some_and(|metadata| metadata.deletion_timestamp_ms != 0) + .is_some_and(|metadata| metadata.deletion_time.is_some()) { continue; } @@ -1727,7 +1723,7 @@ pub async fn validate_provider_credential_key_available_for_attached_sandboxes_w .credentials .entry(credential_key.to_string()) .or_insert_with(|| "pending".to_string()); - candidate.credential_expires_at_ms.remove(credential_key); + candidate.credential_expiration_times.remove(credential_key); validate_provider_update_against_attached_sandboxes_with_catalog( store, catalog, workspace, &candidate, ) @@ -2170,9 +2166,10 @@ fn broker_only_provider_credential_keys(profile: &ProviderProfile) -> HashSet bool { provider - .credential_expires_at_ms + .credential_expiration_times .get(key) - .is_none_or(|expires_at_ms| *expires_at_ms <= 0 || *expires_at_ms > now_ms) + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .is_none_or(|expiration_ms| expiration_ms > now_ms) } fn is_non_injectable_provider_credential(provider: &Provider, key: &str) -> bool { @@ -3405,12 +3402,12 @@ fn stored_provider_profile_for_workspace( metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: uuid::Uuid::new_v4().to_string(), name: profile.id.clone(), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(profile), } @@ -3530,8 +3527,8 @@ pub(super) async fn handle_update_provider( }; let provider_type = provider.r#type.clone(); provider - .credential_expires_at_ms - .extend(req.credential_expires_at_ms); + .credential_expiration_times + .extend(req.credential_expiration_times); if state.credentials.stores_provider_credentials() && !provider.credentials.is_empty() { state.compute.ensure_workspace(&workspace).await?; } @@ -3691,7 +3688,10 @@ pub(super) async fn handle_exchange_provider_subject_token( if let Some(cached) = INTERMEDIATE_TOKEN_CACHE.get(&intermediate_cache_key) { return Ok(Response::new(ExchangeProviderSubjectTokenResponse { access_token: cached.access_token, - expires_in: cached.expires_in, + expires_after: openshell_core::time::duration_from_std(std::time::Duration::from_secs( + u64::try_from(cached.expires_in).unwrap_or_default(), + )) + .ok(), token_type: cached.token_type, })); } @@ -3726,8 +3726,11 @@ pub(super) async fn handle_exchange_provider_subject_token( })?; let cache_expires_at_ms = intermediate_token_cache_expires_at_ms( &token_response, - token_grant.cache_ttl_seconds, - provider_credential_expires_at_ms(&provider, &subject_token.credential), + token_grant + .cache_ttl + .as_ref() + .map_or(0, |value| value.seconds), + provider_credential_expiration_times(&provider, &subject_token.credential), supervisor_claims.exp, ); if cache_expires_at_ms > crate::persistence::current_time_ms() { @@ -3736,7 +3739,10 @@ pub(super) async fn handle_exchange_provider_subject_token( Ok(Response::new(ExchangeProviderSubjectTokenResponse { access_token: token_response.access_token, - expires_in: token_response.expires_in, + expires_after: openshell_core::time::duration_from_std(std::time::Duration::from_secs( + u64::try_from(token_response.expires_in).unwrap_or_default(), + )) + .ok(), token_type: token_response.token_type, })) } @@ -3777,7 +3783,7 @@ fn ensure_subject_token_credential_not_expired( provider: &Provider, credential_key: &str, ) -> Result<(), Status> { - let expires_at_ms = provider_credential_expires_at_ms(provider, credential_key); + let expires_at_ms = provider_credential_expiration_times(provider, credential_key); if expires_at_ms > 0 && expires_at_ms <= crate::persistence::current_time_ms() { return Err(Status::failed_precondition( "subject token credential has expired", @@ -3786,11 +3792,11 @@ fn ensure_subject_token_credential_not_expired( Ok(()) } -fn provider_credential_expires_at_ms(provider: &Provider, credential_key: &str) -> i64 { +fn provider_credential_expiration_times(provider: &Provider, credential_key: &str) -> i64 { provider - .credential_expires_at_ms + .credential_expiration_times .get(credential_key) - .copied() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) .unwrap_or_default() } @@ -4221,14 +4227,12 @@ pub(super) async fn handle_configure_provider_refresh( "aws_session_token requires aws_access_key_id and aws_secret_access_key", )); } - if request - .expires_at_ms - .is_some_and(|expires_at_ms| expires_at_ms < 0) - { - return Err(Status::invalid_argument( - "expires_at_ms must be greater than or equal to 0", - )); - } + let requested_expiration_ms = request + .expiration_time + .as_ref() + .map(openshell_core::time::timestamp_to_millis) + .transpose() + .map_err(|error| Status::invalid_argument(error.to_string()))?; // Serialize the reserve-then-persist sequence against other configurations // and sandbox mutations. The collision validation below and the refresh-state @@ -4356,7 +4360,7 @@ pub(super) async fn handle_configure_provider_refresh( state .metadata .as_ref() - .is_some_and(|metadata| metadata.deletion_timestamp_ms != 0) + .is_some_and(|metadata| metadata.deletion_time.is_some()) }) { return Err(Status::failed_precondition( "provider refresh is being deleted; retry deletion before configuring it again", @@ -4368,7 +4372,7 @@ pub(super) async fn handle_configure_provider_refresh( .as_ref() .map(|metadata| metadata.resource_version) }); - let expires_at_ms = request.expires_at_ms.unwrap_or_else(|| { + let expires_at_ms = requested_expiration_ms.unwrap_or_else(|| { existing_refresh_state .as_ref() .map(|state| state.expires_at_ms) @@ -4464,22 +4468,26 @@ pub(super) async fn handle_configure_provider_refresh( return Err(err); } - if let Some(expires_at_ms) = request.expires_at_ms { + if let Some(expires_at_ms) = requested_expiration_ms { let updated = Provider { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: provider_name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::from([(credential_key.to_string(), expires_at_ms)]), + credential_expiration_times: HashMap::from([( + credential_key.to_string(), + openshell_core::time::timestamp_from_millis(expires_at_ms) + .map_err(|error| Status::invalid_argument(error.to_string()))?, + )]), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -4555,11 +4563,12 @@ fn clear_refresh_owned_expiries( } for key in owned_keys { if provider - .credential_expires_at_ms + .credential_expiration_times .get(key) - .is_some_and(|expires_at_ms| *expires_at_ms == refresh_expires_at_ms) + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .is_some_and(|expires_at_ms| expires_at_ms == refresh_expires_at_ms) { - provider.credential_expires_at_ms.remove(key); + provider.credential_expiration_times.remove(key); } } } @@ -4764,6 +4773,10 @@ mod tests { use openshell_core::{ObjectId, ObjectName}; use tonic::{Code, Request}; + fn ts(milliseconds: i64) -> prost_types::Timestamp { + openshell_core::time::timestamp_from_millis(milliseconds).unwrap() + } + #[test] fn env_key_validation_accepts_valid_keys() { assert!(is_valid_env_key("PATH")); @@ -4850,7 +4863,7 @@ mod tests { subject_token: None, scopes: vec!["openid".to_string()], requested_token_type: String::new(), - cache_ttl_seconds: 300, + cache_ttl: Some(prost_types::Duration { seconds: 300, nanos: 0 }), audience_overrides: service_audiences .iter() .map( @@ -4944,17 +4957,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -5029,12 +5042,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-import-ambiguity-id".to_string(), name: "sandbox-import-ambiguity".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec![ @@ -5171,7 +5184,7 @@ mod tests { let after_meta = after.metadata.unwrap(); assert_eq!(after_meta.id, before_meta.id); assert_eq!(after_meta.name, before_meta.name); - assert_eq!(after_meta.created_at_ms, before_meta.created_at_ms); + assert_eq!(after_meta.created_time, before_meta.created_time); assert_eq!(after_meta.labels, before_meta.labels); assert!(after_meta.resource_version > before_meta.resource_version); assert_eq!( @@ -5356,12 +5369,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-update-ambiguity-id".to_string(), name: "sandbox-update-ambiguity".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec![ @@ -5423,12 +5436,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: [ @@ -5443,7 +5456,7 @@ mod tests { ] .into_iter() .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), } @@ -5458,7 +5471,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, ..Default::default() @@ -5466,7 +5479,7 @@ mod tests { r#type: provider_type.to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: std::iter::once(( credential_key.to_string(), @@ -5490,7 +5503,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, ..Default::default() @@ -5498,7 +5511,7 @@ mod tests { r#type: provider_type.to_string(), credentials: std::iter::once((credential_key.to_string(), value.to_string())).collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), } @@ -5546,8 +5559,14 @@ mod tests { strategy: ProviderCredentialRefreshStrategy::Oauth2ClientCredentials as i32, token_url: "https://auth.example.com/token".to_string(), scopes: Vec::new(), - refresh_before_seconds: 300, - max_lifetime_seconds: 3600, + refresh_before: Some(prost_types::Duration { + seconds: 300, + nanos: 0, + }), + max_lifetime: Some(prost_types::Duration { + seconds: 3600, + nanos: 0, + }), additional_outputs: Vec::new(), material: vec![ ProviderCredentialRefreshMaterial { @@ -5628,7 +5647,10 @@ mod tests { subject_token: None, scopes: vec!["read".to_string()], requested_token_type: String::new(), - cache_ttl_seconds: 300, + cache_ttl: Some(prost_types::Duration { + seconds: 300, + nanos: 0, + }), audience_overrides: Vec::new(), }), } @@ -5823,12 +5845,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "fanout-sandbox-id".to_string(), name: "fanout-sandbox".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["fanout-provider".to_string()], @@ -6334,12 +6356,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-id".to_string(), name: "sandbox-using-custom".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["custom-provider".to_string()], @@ -6374,12 +6396,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "msgraph".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: TEST_GRAPH_PROVIDER_TYPE.to_string(), credentials: std::iter::once(( @@ -6388,7 +6410,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -6412,7 +6434,7 @@ mod tests { // profile; direct callers cannot opt a client secret out of // credential storage by omitting this advisory list. secret_material_keys: Vec::new(), - expires_at_ms: Some(expires_at_ms), + expiration_time: Some(ts(expires_at_ms)), workspace: "default".to_string(), }), ) @@ -6435,7 +6457,13 @@ mod tests { .unwrap() .into_inner(); assert_eq!(status.credentials.len(), 1); - assert_eq!(status.credentials[0].expires_at_ms, expires_at_ms); + assert_eq!( + status.credentials[0] + .expiration_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()), + Some(expires_at_ms) + ); let provider = state .store @@ -6445,9 +6473,9 @@ mod tests { .expect("provider"); assert_eq!( provider - .credential_expires_at_ms + .credential_expiration_times .get("MS_GRAPH_ACCESS_TOKEN"), - Some(&expires_at_ms) + Some(&ts(expires_at_ms)) ); let first_refresh = crate::provider_refresh::get_refresh_state( @@ -6494,7 +6522,7 @@ mod tests { ("client_secret".to_string(), "client-secret".to_string()), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: Some(expires_at_ms), + expiration_time: Some(ts(expires_at_ms)), workspace: "default".to_string(), }), ) @@ -6550,7 +6578,7 @@ mod tests { .expect("provider"); assert!( !provider_after_delete - .credential_expires_at_ms + .credential_expiration_times .contains_key("MS_GRAPH_ACCESS_TOKEN") ); } @@ -6585,7 +6613,7 @@ mod tests { ("client_secret".to_string(), client_secret.to_string()), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }; handle_configure_provider_refresh(&state, authed_request(request("original-secret"))) @@ -6705,7 +6733,7 @@ mod tests { ("client_secret".to_string(), client_secret.to_string()), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }; let (first_store_hit, release_first_store) = first_state.credentials.gate_next_store(); @@ -6793,12 +6821,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-authoritative-profiles-id".to_string(), name: "sandbox-authoritative-profiles".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["provider-a".to_string(), "provider-b".to_string()], @@ -6830,7 +6858,7 @@ mod tests { strategy: ProviderCredentialRefreshStrategy::Oauth2ClientCredentials as i32, material: HashMap::new(), secret_material_keys: Vec::new(), - expires_at_ms: Some(expires_at_ms), + expiration_time: Some(ts(expires_at_ms)), workspace: "default".to_string(), }), ) @@ -6844,8 +6872,8 @@ mod tests { .unwrap() .expect("provider-a"); assert_eq!( - provider.credential_expires_at_ms.get("REFRESH_TOKEN"), - Some(&expires_at_ms) + provider.credential_expiration_times.get("REFRESH_TOKEN"), + Some(&ts(expires_at_ms)) ); } @@ -6857,17 +6885,20 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "provider-a".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::from([("REFRESH_TOKEN".to_string(), expires_at_ms)]), + credential_expiration_times: HashMap::from([( + "REFRESH_TOKEN".to_string(), + ts(expires_at_ms), + )]), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -6920,7 +6951,7 @@ mod tests { .expect("provider-a"); assert!( !provider - .credential_expires_at_ms + .credential_expiration_times .contains_key("REFRESH_TOKEN") ); } @@ -6935,12 +6966,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "vertex-sa".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-vertex-ai".to_string(), credentials: std::iter::once(( @@ -6949,7 +6980,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -6974,7 +7005,7 @@ mod tests { ), ]), secret_material_keys: vec!["private_key".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -7005,12 +7036,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "msgraph".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: TEST_GRAPH_PROVIDER_TYPE.to_string(), credentials: std::iter::once(( @@ -7019,7 +7050,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -7040,7 +7071,7 @@ mod tests { ("client_secret".to_string(), "client-secret".to_string()), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: Some(refresh_expires_at_ms), + expiration_time: Some(ts(refresh_expires_at_ms)), workspace: "default".to_string(), }), ) @@ -7055,19 +7086,19 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "msgraph".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::from([( + credential_expiration_times: HashMap::from([( "MS_GRAPH_ACCESS_TOKEN".to_string(), - manual_expires_at_ms, + ts(manual_expires_at_ms), )]), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), @@ -7097,9 +7128,9 @@ mod tests { .expect("provider"); assert_eq!( provider_after_delete - .credential_expires_at_ms + .credential_expiration_times .get("MS_GRAPH_ACCESS_TOKEN"), - Some(&manual_expires_at_ms) + Some(&ts(manual_expires_at_ms)) ); } @@ -7123,17 +7154,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-delete".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -7153,7 +7184,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: Some(refresh_expires_at_ms), + expiration_time: Some(ts(refresh_expires_at_ms)), workspace: "default".to_string(), }), ) @@ -7171,19 +7202,25 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-delete".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::from([ - ("AWS_SECRET_ACCESS_KEY".to_string(), refresh_expires_at_ms), - ("AWS_SESSION_TOKEN".to_string(), independent_expires_at_ms), + credential_expiration_times: HashMap::from([ + ( + "AWS_SECRET_ACCESS_KEY".to_string(), + ts(refresh_expires_at_ms), + ), + ( + "AWS_SESSION_TOKEN".to_string(), + ts(independent_expires_at_ms), + ), ]), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), @@ -7212,18 +7249,20 @@ mod tests { // Refresh-owned expiries for the primary and secret are cleared. assert!( !provider - .credential_expires_at_ms + .credential_expiration_times .contains_key("AWS_ACCESS_KEY_ID") ); assert!( !provider - .credential_expires_at_ms + .credential_expiration_times .contains_key("AWS_SECRET_ACCESS_KEY") ); // The independently updated session-token expiry is preserved. assert_eq!( - provider.credential_expires_at_ms.get("AWS_SESSION_TOKEN"), - Some(&independent_expires_at_ms) + provider + .credential_expiration_times + .get("AWS_SESSION_TOKEN"), + Some(&ts(independent_expires_at_ms)) ); } @@ -7241,20 +7280,23 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "p".to_string(), name: "p".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::from([ - ("AWS_ACCESS_KEY_ID".to_string(), refresh_expires_at_ms), - ("AWS_SECRET_ACCESS_KEY".to_string(), refresh_expires_at_ms), - ("AWS_SESSION_TOKEN".to_string(), concurrently_changed), + credential_expiration_times: HashMap::from([ + ("AWS_ACCESS_KEY_ID".to_string(), ts(refresh_expires_at_ms)), + ( + "AWS_SECRET_ACCESS_KEY".to_string(), + ts(refresh_expires_at_ms), + ), + ("AWS_SESSION_TOKEN".to_string(), ts(concurrently_changed)), ]), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), @@ -7269,17 +7311,19 @@ mod tests { assert!( !provider - .credential_expires_at_ms + .credential_expiration_times .contains_key("AWS_ACCESS_KEY_ID") ); assert!( !provider - .credential_expires_at_ms + .credential_expiration_times .contains_key("AWS_SECRET_ACCESS_KEY") ); assert_eq!( - provider.credential_expires_at_ms.get("AWS_SESSION_TOKEN"), - Some(&concurrently_changed) + provider + .credential_expiration_times + .get("AWS_SESSION_TOKEN"), + Some(&ts(concurrently_changed)) ); } @@ -7294,12 +7338,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "existing-graph".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: TEST_GRAPH_PROVIDER_TYPE.to_string(), credentials: std::iter::once(( @@ -7308,7 +7352,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -7322,18 +7366,18 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "refreshing-graph".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: TEST_GRAPH_PROVIDER_TYPE.to_string(), credentials: std::iter::once(("OTHER_TOKEN".to_string(), "other".to_string())) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -7346,12 +7390,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-collision".to_string(), name: "collision".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["existing-graph".to_string(), "refreshing-graph".to_string()], @@ -7374,7 +7418,7 @@ mod tests { ("client_secret".to_string(), "client-secret".to_string()), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -7402,17 +7446,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: TEST_GRAPH_PROVIDER_TYPE.to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -7426,12 +7470,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-refresh-collision".to_string(), name: "refresh-collision".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["first-graph".to_string(), "second-graph".to_string()], @@ -7454,7 +7498,7 @@ mod tests { ("client_secret".to_string(), "client-secret".to_string()), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -7473,7 +7517,7 @@ mod tests { ("client_secret".to_string(), "client-secret".to_string()), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -7498,12 +7542,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "msgraph".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: TEST_GRAPH_PROVIDER_TYPE.to_string(), credentials: std::iter::once(( @@ -7512,7 +7556,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -7536,7 +7580,7 @@ mod tests { ), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -7553,7 +7597,7 @@ mod tests { strategy: ProviderCredentialRefreshStrategy::Oauth2ClientCredentials as i32, material: HashMap::from([("tenant_id".to_string(), "tenant".to_string())]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -7573,12 +7617,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "msgraph".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "outlook".to_string(), credentials: std::iter::once(( @@ -7587,7 +7631,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -7607,7 +7651,7 @@ mod tests { strategy: strategy as i32, material: HashMap::new(), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -7741,12 +7785,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "gitlab-local".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "gitlab".to_string(), credentials: std::iter::once(( @@ -7756,7 +7800,7 @@ mod tests { .collect(), config: std::iter::once(("endpoint".to_string(), "https://gitlab.com".to_string())) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -7982,7 +8026,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "legacy-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, ..Default::default() @@ -7994,7 +8038,7 @@ mod tests { "https://updated.example.com".to_string(), )) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }, @@ -8047,7 +8091,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "legacy-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, ..Default::default() @@ -8059,7 +8103,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }, @@ -8207,7 +8251,7 @@ mod tests { "openai", "OPENAI_API_KEY", )), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), workspace: "default".to_string(), }), ) @@ -8226,7 +8270,10 @@ mod tests { &store, "default", Provider { - credential_expires_at_ms: HashMap::from([("API_TOKEN".to_string(), 123_456)]), + credential_expiration_times: HashMap::from([( + "API_TOKEN".to_string(), + ts(123_456), + )]), ..provider_with_values("gitlab-local", "gitlab") }, ) @@ -8284,12 +8331,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-id".to_string(), name: "attached-sandbox".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["gitlab-local".to_string()], @@ -8334,12 +8381,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "test-provider".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "openai".to_string(), credentials: std::iter::once(( @@ -8348,7 +8395,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8369,12 +8416,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "test-provider".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "openai".to_string(), credentials: std::iter::once(( @@ -8383,7 +8430,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8409,17 +8456,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "bad-provider".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8435,17 +8482,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "gitlab-no-creds".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "gitlab".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8479,8 +8526,14 @@ mod tests { as i32, token_url: "https://login.example/token".to_string(), scopes: vec!["https://example.test/.default".to_string()], - refresh_before_seconds: 300, - max_lifetime_seconds: 3600, + refresh_before: Some(prost_types::Duration { + seconds: 300, + nanos: 0, + }), + max_lifetime: Some(prost_types::Duration { + seconds: 3600, + nanos: 0, + }), additional_outputs: Vec::new(), material: vec![ ProviderCredentialRefreshMaterial { @@ -8520,17 +8573,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "delegated-refresh-no-token-yet".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "delegated-refresh-api".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8563,17 +8616,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "mixed-required-no-token-yet".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "mixed-required-api".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8606,17 +8659,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "optional-static-no-token-yet".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "optional-static-api".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8632,17 +8685,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "vertex-no-token-yet".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-vertex-ai".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8666,17 +8719,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "missing".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8702,17 +8755,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "noop-test".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8757,17 +8810,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "delete-key-test".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: std::iter::once(("SECONDARY".to_string(), String::new())).collect(), config: std::iter::once(("region".to_string(), String::new())).collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8816,17 +8869,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "type-preserve-test".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8853,17 +8906,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "type-change-test".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "openai".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8892,17 +8945,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "validate-merge-test".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: std::iter::once((oversized_key, "value".to_string())).collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8925,17 +8978,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: uuid::Uuid::new_v4().to_string(), name: "legacy-oversized-type".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: oversized_type.clone(), credentials: std::iter::once(("API_TOKEN".to_string(), "old".to_string())).collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -8948,18 +9001,18 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "legacy-oversized-type".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: std::iter::once(("API_TOKEN".to_string(), "new".to_string())) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -8986,12 +9039,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "claude-local".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "claude".to_string(), credentials: [ @@ -9005,7 +9058,7 @@ mod tests { "https://api.anthropic.com".to_string(), )) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -9169,7 +9222,7 @@ mod tests { ("client_secret".to_string(), "client-secret".to_string()), ]), secret_material_keys: vec!["client_secret".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }; handle_configure_provider_refresh(&state, authed_request(configure())) @@ -9211,7 +9264,7 @@ mod tests { openshell_core::provider_credentials::ProviderCredentialState::from_bound_environment( revision_1, first.environment.clone(), - first.credential_expires_at_ms.clone(), + first.credential_expiration_times.clone(), first.dynamic_credentials.clone(), first.static_credential_bindings.clone(), Vec::new(), @@ -9234,7 +9287,7 @@ mod tests { )]), ..Default::default() }), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), workspace: "default".to_string(), }), ) @@ -9271,7 +9324,7 @@ mod tests { .install_bound_environment( revision_1, unchanged_environment.environment, - unchanged_environment.credential_expires_at_ms, + unchanged_environment.credential_expiration_times, unchanged_environment.dynamic_credentials, unchanged_environment.static_credential_bindings, Vec::new(), @@ -9327,7 +9380,7 @@ mod tests { .install_bound_environment( revision_2, second.environment.clone(), - second.credential_expires_at_ms.clone(), + second.credential_expiration_times.clone(), second.dynamic_credentials.clone(), second.static_credential_bindings.clone(), Vec::new(), @@ -9344,7 +9397,7 @@ mod tests { openshell_core::provider_credentials::ProviderCredentialState::from_bound_environment( revision_2, second.environment.clone(), - second.credential_expires_at_ms.clone(), + second.credential_expiration_times.clone(), second.dynamic_credentials.clone(), second.static_credential_bindings.clone(), Vec::new(), @@ -9391,7 +9444,7 @@ mod tests { .install_bound_environment( revision_3, third.environment, - third.credential_expires_at_ms, + third.credential_expiration_times, third.dynamic_credentials, third.static_credential_bindings, Vec::new(), @@ -9419,8 +9472,8 @@ mod tests { "GCP_ADC_ACCESS_TOKEN".to_string(), "google-token".to_string(), )]); - google_cloud.credential_expires_at_ms = - HashMap::from([("GCP_ADC_ACCESS_TOKEN".to_string(), expires_at_ms)]); + google_cloud.credential_expiration_times = + HashMap::from([("GCP_ADC_ACCESS_TOKEN".to_string(), ts(expires_at_ms))]); create_provider_record(&store, "default", google_cloud) .await .unwrap(); @@ -9447,7 +9500,7 @@ mod tests { ); assert!( !result - .credential_expires_at_ms + .credential_expiration_times .contains_key("GCP_ADC_ACCESS_TOKEN"), "withheld static credentials must not retain expiry metadata" ); @@ -9744,12 +9797,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "expiring-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "test".to_string(), credentials: [ @@ -9759,9 +9812,9 @@ mod tests { .into_iter() .collect(), config: HashMap::new(), - credential_expires_at_ms: [ - ("FRESH_TOKEN".to_string(), now_ms + 60_000), - ("STALE_TOKEN".to_string(), now_ms - 60_000), + credential_expiration_times: [ + ("FRESH_TOKEN".to_string(), ts(now_ms + 60_000)), + ("STALE_TOKEN".to_string(), ts(now_ms - 60_000)), ] .into_iter() .collect(), @@ -9779,7 +9832,7 @@ mod tests { assert_eq!(result.get("FRESH_TOKEN"), Some(&"fresh".to_string())); assert!(!result.contains_key("STALE_TOKEN")); assert_eq!( - result.credential_expires_at_ms.get("FRESH_TOKEN"), + result.credential_expiration_times.get("FRESH_TOKEN"), Some(&(now_ms + 60_000)) ); } @@ -9801,12 +9854,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "test-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "test".to_string(), credentials: [ @@ -9817,7 +9870,7 @@ mod tests { .into_iter() .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -9844,12 +9897,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "claude-local".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "claude".to_string(), credentials: std::iter::once(( @@ -9858,7 +9911,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -9872,18 +9925,18 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "gitlab-local".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "gitlab".to_string(), credentials: std::iter::once(("GITLAB_TOKEN".to_string(), "glpat-xyz".to_string())) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -9912,18 +9965,18 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "provider-a".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "claude".to_string(), credentials: std::iter::once(("SHARED_KEY".to_string(), "first-value".to_string())) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -9937,12 +9990,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "provider-b".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "gitlab".to_string(), credentials: std::iter::once(( @@ -9951,7 +10004,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -9988,7 +10041,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "provider-a".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, ..Default::default() @@ -9997,7 +10050,7 @@ mod tests { credentials: std::iter::once(("SHARED_KEY".to_string(), "first-value".to_string())) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }, @@ -10038,12 +10091,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "google-config".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-cloud".to_string(), credentials: std::iter::once(( @@ -10053,7 +10106,7 @@ mod tests { .collect(), config: std::iter::once(("project_id".to_string(), "config-project".to_string())) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10067,12 +10120,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "static-credential".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "gitlab".to_string(), credentials: std::iter::once(( @@ -10081,7 +10134,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10127,12 +10180,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "vertex-local".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-vertex-ai".to_string(), credentials: std::iter::once(( @@ -10149,7 +10202,7 @@ mod tests { ] .into_iter() .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10207,12 +10260,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "vertex-bootstrap".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-vertex-ai".to_string(), credentials: [ @@ -10228,7 +10281,7 @@ mod tests { .into_iter() .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10258,12 +10311,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "vertex-no-config".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-vertex-ai".to_string(), credentials: std::iter::once(( @@ -10272,7 +10325,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10313,12 +10366,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "vertex-collision".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-vertex-ai".to_string(), credentials: [ @@ -10337,7 +10390,7 @@ mod tests { ] .into_iter() .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10367,18 +10420,18 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "openai-local".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "openai".to_string(), credentials: std::iter::once(("OPENAI_API_KEY".to_string(), "sk-test".to_string())) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10411,12 +10464,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "provider-a".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "outlook".to_string(), credentials: std::iter::once(( @@ -10425,7 +10478,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10439,12 +10492,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "provider-b".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-drive".to_string(), credentials: std::iter::once(( @@ -10453,7 +10506,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10464,12 +10517,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-collision".to_string(), name: "collision".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["provider-a".to_string(), "provider-b".to_string()], @@ -10486,12 +10539,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "provider-b".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: std::iter::once(( @@ -10500,7 +10553,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10523,12 +10576,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "google-config".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-cloud".to_string(), credentials: std::iter::once(( @@ -10538,7 +10591,7 @@ mod tests { .collect(), config: std::iter::once(("project_id".to_string(), "config-project".to_string())) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10552,12 +10605,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "credential-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "gitlab".to_string(), credentials: std::iter::once(( @@ -10566,7 +10619,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10578,12 +10631,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-plugin-config-collision".to_string(), name: "plugin-config-collision".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec![ @@ -10604,12 +10657,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "credential-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: std::iter::once(( @@ -10618,7 +10671,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10645,12 +10698,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "my-claude".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "claude".to_string(), credentials: std::iter::once(( @@ -10659,7 +10712,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -10671,12 +10724,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-001".to_string(), name: "test-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["my-claude".to_string()], @@ -10710,12 +10763,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-002".to_string(), name: "empty-sandbox".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec::default()), status: None, @@ -10760,17 +10813,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "test-validate-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), // Empty type is ignored in update credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -10928,7 +10981,7 @@ mod tests { &state, authed_request(UpdateProviderRequest { provider: Some(updated_provider.clone()), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), workspace: "default".to_string(), }), ) @@ -10999,7 +11052,7 @@ mod tests { &state, authed_request(UpdateProviderRequest { provider: Some(stale_provider), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), workspace: "default".to_string(), }), ) @@ -11073,7 +11126,7 @@ mod tests { &state, authed_request(UpdateProviderRequest { provider: Some(stale_provider), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), workspace: "default".to_string(), }), ) @@ -11144,7 +11197,7 @@ mod tests { &state_clone, authed_request(UpdateProviderRequest { provider: Some(updated), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), workspace: "default".to_string(), }), ) @@ -11208,12 +11261,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "my-aws".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: std::iter::once(( @@ -11222,7 +11275,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11241,7 +11294,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11280,12 +11333,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "my-aws-v2".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: std::iter::once(( @@ -11294,7 +11347,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11313,7 +11366,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11357,17 +11410,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-endpoint-override".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11392,7 +11445,7 @@ mod tests { ), ]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11449,17 +11502,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-partial-source".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11483,7 +11536,7 @@ mod tests { ("aws_access_key_id".to_string(), "AKIATESTKEY".to_string()), ]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11514,17 +11567,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-lone-session".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11549,7 +11602,7 @@ mod tests { ), ]), secret_material_keys: vec!["aws_session_token".to_string()], - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11587,17 +11640,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-outputs".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11616,7 +11669,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11704,7 +11757,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11728,7 +11781,7 @@ mod tests { credentials: HashMap::from([(key.to_string(), value.to_string())]), ..Default::default() }), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), workspace: "default".to_string(), }), ) @@ -11778,12 +11831,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "generic-no-profile".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "generic".to_string(), credentials: std::iter::once(( @@ -11792,7 +11845,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11811,7 +11864,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11849,17 +11902,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-wrong-key".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11880,7 +11933,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -11912,17 +11965,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-gate".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -11941,7 +11994,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -12011,17 +12064,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "aws-env".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -12101,17 +12154,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "existing-aws-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -12127,12 +12180,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "new-aws-provider".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: std::iter::once(( @@ -12141,7 +12194,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -12155,12 +12208,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-aws-configure-collision".to_string(), name: "aws-configure-collision".to_string(), - created_at_ms: 1, + created_time: openshell_core::time::timestamp_from_millis(1).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec![ @@ -12185,7 +12238,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }), ) @@ -12217,17 +12270,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws".to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, @@ -12244,12 +12297,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "sandbox-concurrent-configure".to_string(), name: "concurrent-configure".to_string(), - created_at_ms: 1, + created_time: openshell_core::time::timestamp_from_millis(1).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["aws-a".to_string(), "aws-b".to_string()], @@ -12270,7 +12323,7 @@ mod tests { "arn:aws:iam::123456789012:role/Test".to_string(), )]), secret_material_keys: Vec::new(), - expires_at_ms: None, + expiration_time: None, workspace: "default".to_string(), }) }; @@ -12302,17 +12355,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "my-google-cloud".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-cloud".to_string(), credentials: HashMap::new(), config, - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), } @@ -12409,17 +12462,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "github".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "github".to_string(), credentials: HashMap::new(), config: HashMap::from([("project_id".to_string(), "should-be-ignored".to_string())]), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -12457,7 +12510,7 @@ mod tests { r#type: "custom".to_string(), credentials: HashMap::from([("TOKEN".to_string(), "secret".to_string())]), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -12470,12 +12523,12 @@ mod tests { p.metadata = Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "shared-name".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }); p }), @@ -12500,12 +12553,12 @@ mod tests { p.metadata = Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "shared-name".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }); p }), @@ -12629,12 +12682,12 @@ mod tests { p.metadata = Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "provider-d".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }); p }), @@ -12779,17 +12832,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "cross-ws".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "claude".to_string(), credentials: HashMap::from([("ANTHROPIC_API_KEY".to_string(), "sk-123".to_string())]), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "other-workspace".to_string(), credential_handles: HashMap::new(), }; @@ -12807,17 +12860,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "global-profile".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "claude".to_string(), credentials: HashMap::from([("ANTHROPIC_API_KEY".to_string(), "sk-123".to_string())]), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -12834,17 +12887,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "same-ws-profile".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "claude".to_string(), credentials: HashMap::from([("ANTHROPIC_API_KEY".to_string(), "sk-123".to_string())]), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -12861,17 +12914,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "immutable-pw".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "claude".to_string(), credentials: HashMap::from([("ANTHROPIC_API_KEY".to_string(), "sk-123".to_string())]), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }; @@ -12883,17 +12936,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "immutable-pw".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: String::new(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "other".to_string(), credential_handles: HashMap::new(), }; @@ -12966,17 +13019,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: "uses-ws".to_string(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "ws-custom".to_string(), credentials: HashMap::from([("TOKEN".to_string(), "val".to_string())]), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), }, diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index a8d198bc80..9a737d284a 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -312,12 +312,12 @@ async fn handle_create_sandbox_inner( metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.clone(), name: name.clone(), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: request.labels.clone(), resource_version: 0, annotations: request.annotations.clone(), workspace, - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(spec), status: None, @@ -970,7 +970,13 @@ pub(super) async fn handle_watch_sandbox( req.log_tail_lines }; let stop_on_terminal = req.stop_on_terminal; - let log_since_ms = req.log_since_ms; + let log_since_ms = req + .since_time + .as_ref() + .map(openshell_core::time::timestamp_to_millis) + .transpose() + .map_err(|error| Status::invalid_argument(error.to_string()))? + .unwrap_or_default(); let log_sources = req.log_sources; let log_min_level = req.log_min_level; let event_tail = req.event_tail; @@ -1061,7 +1067,12 @@ pub(super) async fn handle_watch_sandbox( ref log, )) = evt.payload { - if log_since_ms > 0 && log.timestamp_ms < log_since_ms { + let event_ms = log + .event_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or_default(); + if log_since_ms > 0 && event_ms < log_since_ms { continue; } if !log_sources.is_empty() && !source_matches(&log.source, &log_sources) { @@ -1234,7 +1245,12 @@ pub(super) async fn handle_exec_sandbox( let command_str = build_remote_exec_command(&req) .map_err(|e| Status::invalid_argument(format!("command construction failed: {e}")))?; let stdin_payload = req.stdin; - let timeout_seconds = req.timeout_seconds; + let execution_timeout = req + .execution_timeout + .as_ref() + .map(openshell_core::time::duration_to_std) + .transpose() + .map_err(|error| Status::invalid_argument(error.to_string()))?; let request_tty = req.tty; let sandbox_id = sandbox.object_id().to_string(); @@ -1257,7 +1273,7 @@ pub(super) async fn handle_exec_sandbox( relay_stream, &command_str, stdin_payload, - timeout_seconds, + execution_timeout, request_tty, no_login_shell, ) @@ -1431,9 +1447,11 @@ async fn validate_ssh_forward_token( return Err(Status::unauthenticated("SSH session token is not valid")); } - if session.expires_at_ms > 0 { + if let Some(expiration_time) = session.expiration_time.as_ref() { let now_ms = current_time_ms(); - if now_ms > session.expires_at_ms { + let expires_at_ms = openshell_core::time::timestamp_to_millis(expiration_time) + .map_err(|error| Status::internal(error.to_string()))?; + if now_ms > expires_at_ms { return Err(Status::unauthenticated("SSH session token expired")); } } @@ -1670,7 +1688,12 @@ pub(super) async fn handle_exec_sandbox_interactive( .map_err(|e| Status::invalid_argument(format!("command construction failed: {e}")))?; let request_tty = req.tty; let no_login_shell = req.no_login_shell; - let timeout_seconds = req.timeout_seconds; + let execution_timeout = req + .execution_timeout + .as_ref() + .map(openshell_core::time::duration_to_std) + .transpose() + .map_err(|error| Status::invalid_argument(error.to_string()))?; let cols = if req.cols == 0 { 80 } else { req.cols }; let rows = if req.rows == 0 { 24 } else { req.rows }; @@ -1699,7 +1722,7 @@ pub(super) async fn handle_exec_sandbox_interactive( input_stream, request_tty, no_login_shell, - timeout_seconds, + execution_timeout, cols, rows, ) @@ -1754,17 +1777,18 @@ pub(super) async fn handle_create_ssh_session( metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: token.clone(), name: generate_name(), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: sandbox.object_workspace().to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), sandbox_id: req.sandbox_id.clone(), token: token.clone(), revoked: false, - expires_at_ms, + expiration_time: openshell_core::time::optional_timestamp_from_legacy_millis(expires_at_ms) + .map_err(|error| Status::internal(error.to_string()))?, }; // Ensure metadata is valid (defense in depth - should always be true for server-constructed metadata) @@ -1808,7 +1832,8 @@ pub(super) async fn handle_create_ssh_session( gateway_port: gateway_port.into(), gateway_scheme: scheme.to_string(), host_key_fingerprint: String::new(), - expires_at_ms, + expiration_time: openshell_core::time::optional_timestamp_from_legacy_millis(expires_at_ms) + .map_err(|error| Status::internal(error.to_string()))?, })) } @@ -1994,7 +2019,7 @@ async fn stream_exec_over_relay( relay_stream: tokio::io::DuplexStream, command: &str, stdin_payload: Vec, - timeout_seconds: u32, + execution_timeout: Option, request_tty: bool, no_login_shell: bool, ) -> Result<(), Status> { @@ -2025,25 +2050,22 @@ async fn stream_exec_over_relay( tx.clone(), ); - let exec_result = if timeout_seconds == 0 { - exec.await - } else if let Ok(r) = tokio::time::timeout( - std::time::Duration::from_secs(u64::from(timeout_seconds)), - exec, - ) - .await - { - r + let exec_result = if let Some(execution_timeout) = execution_timeout { + if let Ok(result) = tokio::time::timeout(execution_timeout, exec).await { + result + } else { + let _ = tx + .send(Ok(ExecSandboxEvent { + payload: Some(openshell_core::proto::exec_sandbox_event::Payload::Exit( + ExecSandboxExit { exit_code: 124 }, + )), + })) + .await; + let _ = proxy_task.await; + return Ok(()); + } } else { - let _ = tx - .send(Ok(ExecSandboxEvent { - payload: Some(openshell_core::proto::exec_sandbox_event::Payload::Exit( - ExecSandboxExit { exit_code: 124 }, - )), - })) - .await; - let _ = proxy_task.await; - return Ok(()); + exec.await }; let exit_code = match exec_result { @@ -2077,7 +2099,7 @@ async fn stream_interactive_exec_over_relay( input_stream: tonic::Streaming, request_tty: bool, no_login_shell: bool, - timeout_seconds: u32, + execution_timeout: Option, cols: u32, rows: u32, ) -> Result<(), Status> { @@ -2109,25 +2131,22 @@ async fn stream_interactive_exec_over_relay( tx.clone(), ); - let exec_result = if timeout_seconds == 0 { - exec.await - } else if let Ok(r) = tokio::time::timeout( - std::time::Duration::from_secs(u64::from(timeout_seconds)), - exec, - ) - .await - { - r + let exec_result = if let Some(execution_timeout) = execution_timeout { + if let Ok(result) = tokio::time::timeout(execution_timeout, exec).await { + result + } else { + let _ = tx + .send(Ok(ExecSandboxEvent { + payload: Some(openshell_core::proto::exec_sandbox_event::Payload::Exit( + ExecSandboxExit { exit_code: 124 }, + )), + })) + .await; + let _ = proxy_task.await; + return Ok(()); + } } else { - let _ = tx - .send(Ok(ExecSandboxEvent { - payload: Some(openshell_core::proto::exec_sandbox_event::Payload::Exit( - ExecSandboxExit { exit_code: 124 }, - )), - })) - .await; - let _ = proxy_task.await; - return Ok(()); + exec.await }; let exit_code = match exec_result { @@ -2879,18 +2898,18 @@ mod tests { metadata: Some(ObjectMeta { id: format!("provider-{name}"), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: std::iter::once((credential_key.to_string(), "secret".to_string())) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), } @@ -2901,12 +2920,12 @@ mod tests { metadata: Some(ObjectMeta { id: format!("sandbox-{name}"), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: std::iter::once(("team".to_string(), "agents".to_string())).collect(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(openshell_core::proto::SandboxSpec { log_level: "debug".to_string(), diff --git a/crates/openshell-server/src/grpc/service.rs b/crates/openshell-server/src/grpc/service.rs index 790e26d618..d0bbce2b79 100644 --- a/crates/openshell-server/src/grpc/service.rs +++ b/crates/openshell-server/src/grpc/service.rs @@ -73,7 +73,9 @@ pub(super) async fn handle_expose_service( existing .metadata .as_ref() - .map_or(now, |metadata| metadata.created_at_ms), + .and_then(|metadata| metadata.created_time.as_ref()) + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or(now), WriteCondition::MatchResourceVersion(resource_version), false, ) @@ -97,12 +99,12 @@ pub(super) async fn handle_expose_service( metadata: Some(ObjectMeta { id: id.clone(), name: key.clone(), - created_at_ms, + created_time: openshell_core::time::timestamp_from_millis(created_at_ms).ok(), labels: HashMap::from([("sandbox".to_string(), req.sandbox.clone())]), resource_version: 0, annotations: HashMap::new(), workspace: workspace.clone(), - deletion_timestamp_ms: 0, + deletion_time: None, }), sandbox_id: sandbox.object_id().to_string(), sandbox_name: req.sandbox.clone(), @@ -362,12 +364,12 @@ mod tests { metadata: Some(ObjectMeta { id: format!("sandbox-{name}"), name: name.to_string(), - created_at_ms: 1_000, + created_time: openshell_core::time::timestamp_from_millis(1_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(openshell_core::proto::SandboxSpec::default()), ..Default::default() @@ -666,12 +668,12 @@ mod tests { metadata: Some(ObjectMeta { id: "sandbox-my-sandbox-beta".to_string(), name: "my-sandbox".to_string(), - created_at_ms: 1_000, + created_time: openshell_core::time::timestamp_from_millis(1_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "beta".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(openshell_core::proto::SandboxSpec::default()), ..Default::default() diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index eb53c80f29..3f95fc14b7 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -461,24 +461,21 @@ pub(super) fn validate_provider_mutable_fields(provider: &Provider) -> Result<() MAX_MAP_VALUE_LEN, "provider.config", )?; - if provider.credential_expires_at_ms.len() > MAX_PROVIDER_CREDENTIALS_ENTRIES { + if provider.credential_expiration_times.len() > MAX_PROVIDER_CREDENTIALS_ENTRIES { return Err(Status::invalid_argument(format!( - "provider.credential_expires_at_ms exceeds maximum entries ({} > {MAX_PROVIDER_CREDENTIALS_ENTRIES})", - provider.credential_expires_at_ms.len() + "provider.credential_expiration_times exceeds maximum entries ({} > {MAX_PROVIDER_CREDENTIALS_ENTRIES})", + provider.credential_expiration_times.len() ))); } - for (key, value) in &provider.credential_expires_at_ms { + for (key, value) in &provider.credential_expiration_times { if key.len() > MAX_MAP_KEY_LEN { return Err(Status::invalid_argument(format!( - "provider.credential_expires_at_ms key exceeds maximum length ({} > {MAX_MAP_KEY_LEN})", + "provider.credential_expiration_times key exceeds maximum length ({} > {MAX_MAP_KEY_LEN})", key.len() ))); } - if *value < 0 { - return Err(Status::invalid_argument( - "provider.credential_expires_at_ms value must be greater than or equal to 0", - )); - } + openshell_core::time::validate_timestamp(value) + .map_err(|error| Status::invalid_argument(error.to_string()))?; } Ok(()) } @@ -1404,17 +1401,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials, config, - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), } diff --git a/crates/openshell-server/src/grpc/workspace.rs b/crates/openshell-server/src/grpc/workspace.rs index d83ffab0e8..01e22e5840 100644 --- a/crates/openshell-server/src/grpc/workspace.rs +++ b/crates/openshell-server/src/grpc/workspace.rs @@ -132,7 +132,7 @@ pub async fn resolve_workspace( let terminating = ws .metadata .as_ref() - .is_some_and(|m| m.deletion_timestamp_ms != 0); + .is_some_and(|m| m.deletion_time.is_some()); Ok(ResolvedWorkspace { name, terminating }) } None => Err(Status::not_found(format!("workspace '{name}' not found"))), @@ -154,12 +154,12 @@ pub(super) async fn handle_create_workspace( metadata: Some(ObjectMeta { id: workspace_id.clone(), name: req.name, - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: req.labels, annotations: HashMap::new(), resource_version: 0, workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), status: Some(WorkspaceStatus { phase: WorkspacePhase::Active.into(), @@ -314,7 +314,7 @@ pub(super) async fn handle_delete_workspace( let already_terminating = ws .metadata .as_ref() - .is_some_and(|m| m.deletion_timestamp_ms != 0); + .is_some_and(|m| m.deletion_time.is_some()); // Track the resource_version so the final delete targets exactly this // workspace instance (prevents ABA if a same-name workspace is recreated @@ -327,7 +327,7 @@ pub(super) async fn handle_delete_workspace( .update_message_cas::(&ws_id, 0, |w| { let now_ms = current_time_ms(); if let Some(meta) = w.metadata.as_mut() { - meta.deletion_timestamp_ms = now_ms; + meta.deletion_time = openshell_core::time::timestamp_from_millis(now_ms).ok(); } w.status = Some(WorkspaceStatus { phase: WorkspacePhase::Terminating.into(), @@ -351,7 +351,7 @@ pub(super) async fn handle_delete_workspace( let now_terminating = refreshed .metadata .as_ref() - .is_some_and(|m| m.deletion_timestamp_ms != 0); + .is_some_and(|m| m.deletion_time.is_some()); if !now_terminating { return Err(Status::aborted( "workspace was concurrently modified, please retry", @@ -500,12 +500,12 @@ pub(super) async fn handle_add_workspace_member( metadata: Some(ObjectMeta { id: member_id.clone(), name: req.principal_subject.clone(), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: workspace.clone(), - deletion_timestamp_ms: 0, + deletion_time: None, }), principal_subject: req.principal_subject, role: req.role, @@ -648,10 +648,10 @@ mod tests { let meta = ws.metadata.as_ref().unwrap(); assert_eq!(meta.name, "new-ws"); assert!(!meta.id.is_empty(), "id should be a generated UUID"); - assert!(meta.created_at_ms > 0, "created_at_ms should be set"); + assert!(meta.created_time.is_some(), "created_time should be set"); assert_eq!(meta.labels.get("env").map(String::as_str), Some("test")); assert!(meta.resource_version > 0, "resource_version should be set"); - assert_eq!(meta.deletion_timestamp_ms, 0); + assert!(meta.deletion_time.is_none()); let status = ws.status.as_ref().unwrap(); assert_eq!(status.phase, i32::from(WorkspacePhase::Active)); @@ -775,12 +775,12 @@ mod tests { metadata: Some(ObjectMeta { id: "sbx-eph-1".to_string(), name: "blocker".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "ephemeral".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Default::default() }; @@ -837,17 +837,17 @@ mod tests { metadata: Some(ObjectMeta { id: "ssh-1".to_string(), name: "session-ssh-1".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "sessioned".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), sandbox_id: "sbx-1".to_string(), token: "ssh-1".to_string(), revoked: false, - expires_at_ms: 0, + expiration_time: None, }; state.store.put_message(&session).await.unwrap(); @@ -885,12 +885,12 @@ mod tests { metadata: Some(ObjectMeta { id: "prof-1".to_string(), name: "my-profile".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "profiles-ws".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Default::default() }; @@ -1192,12 +1192,12 @@ mod tests { metadata: Some(ObjectMeta { id: "sbx-term-1".to_string(), name: "blocker".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "term-test".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Default::default() }; @@ -1219,11 +1219,7 @@ mod tests { .await .unwrap() .unwrap(); - assert_ne!( - ws.metadata.as_ref().unwrap().deletion_timestamp_ms, - 0, - "workspace should have deletion_timestamp set" - ); + assert!(ws.metadata.as_ref().unwrap().deletion_time.is_some()); assert_eq!( ws.status.as_ref().unwrap().phase, i32::from(WorkspacePhase::Terminating), @@ -1248,12 +1244,12 @@ mod tests { metadata: Some(ObjectMeta { id: "sbx-dying-1".to_string(), name: "hold".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "dying-ws".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Default::default() }; @@ -1293,12 +1289,12 @@ mod tests { metadata: Some(ObjectMeta { id: "sbx-idem-1".to_string(), name: "temp".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "idempotent-ws".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Default::default() }; @@ -1363,11 +1359,7 @@ mod tests { .await .unwrap() .expect("workspace must remain durable after cleanup failure"); - assert_ne!( - retained.metadata.unwrap().deletion_timestamp_ms, - 0, - "retained workspace must remain terminating" - ); + assert!(retained.metadata.unwrap().deletion_time.is_some()); let retry = handle_delete_workspace( &state, @@ -1476,17 +1468,17 @@ mod tests { metadata: Some(ObjectMeta { id: "route-1".to_string(), name: "inference.local".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "route-test".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), config: Some(openshell_core::proto::InferenceRouteConfig { provider_name: "test-provider".to_string(), model_id: "gpt-4o".to_string(), - timeout_secs: 0, + request_timeout: None, }), version: 1, }; diff --git a/crates/openshell-server/src/inference.rs b/crates/openshell-server/src/inference.rs index b83fd6be4f..cb03f5851d 100644 --- a/crates/openshell-server/src/inference.rs +++ b/crates/openshell-server/src/inference.rs @@ -117,7 +117,7 @@ impl Inference for InferenceService { route_name, &req.provider_name, &req.model_id, - req.timeout_secs, + req.request_timeout, verify, ) .await?; @@ -135,7 +135,7 @@ impl Inference for InferenceService { route_name: route_name.to_string(), validation_performed: !route.validation.is_empty(), validated_endpoints: route.validation, - timeout_secs: config.timeout_secs, + request_timeout: config.request_timeout, workspace, })) } @@ -187,7 +187,7 @@ impl Inference for InferenceService { model_id: config.model_id.clone(), version: route.version, route_name: route_name.to_string(), - timeout_secs: config.timeout_secs, + request_timeout: config.request_timeout, workspace, })) } @@ -238,7 +238,7 @@ async fn upsert_cluster_inference_route( route_name, provider_name, model_id, - timeout_secs, + openshell_core::time::duration_from_std(Duration::from_secs(timeout_secs)).ok(), verify, ) .await @@ -274,7 +274,7 @@ async fn upsert_cluster_inference_route_with_credentials( route_name: &str, provider_name: &str, model_id: &str, - timeout_secs: u64, + request_timeout: Option, verify: bool, ) -> Result { if provider_name.trim().is_empty() { @@ -302,7 +302,11 @@ async fn upsert_cluster_inference_route_with_credentials( Vec::new() }; - let config = build_inference_route_config(&provider, model_id, timeout_secs); + if let Some(timeout) = request_timeout.as_ref() { + openshell_core::time::duration_to_std(timeout) + .map_err(|error| Status::invalid_argument(error.to_string()))?; + } + let config = build_inference_route_config(&provider, model_id, request_timeout); let existing = store .get_message_by_name::(workspace, route_name) @@ -324,12 +328,12 @@ async fn upsert_cluster_inference_route_with_credentials( let new_metadata = Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: new_id.clone(), name: route_name.to_string(), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }); (new_id, new_metadata, 1, WriteCondition::MustCreate) }; @@ -370,12 +374,12 @@ async fn upsert_cluster_inference_route_with_credentials( fn build_inference_route_config( provider: &Provider, model_id: &str, - timeout_secs: u64, + request_timeout: Option, ) -> InferenceRouteConfig { InferenceRouteConfig { provider_name: provider.object_name().to_string(), model_id: model_id.to_string(), - timeout_secs, + request_timeout, } } @@ -1085,7 +1089,10 @@ async fn resolve_inference_bundle_with_credentials( r.api_key.hash(&mut hasher); r.protocols.hash(&mut hasher); r.provider_type.hash(&mut hasher); - r.timeout_secs.hash(&mut hasher); + r.request_timeout + .as_ref() + .map(|value| (value.seconds, value.nanos)) + .hash(&mut hasher); r.model_in_path.hash(&mut hasher); r.request_path_override.hash(&mut hasher); } @@ -1095,7 +1102,7 @@ async fn resolve_inference_bundle_with_credentials( Ok(GetInferenceBundleResponse { routes, revision, - generated_at_ms: now_ms, + generated_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), }) } @@ -1160,7 +1167,7 @@ async fn resolve_route_by_name_with_credentials( api_key: resolved.route.api_key, protocols: resolved.route.protocols, provider_type: resolved.provider_type, - timeout_secs: config.timeout_secs, + request_timeout: config.request_timeout, model_in_path: resolved.route.model_in_path, request_path_override: resolved.route.request_path_override, })) @@ -1188,9 +1195,9 @@ async fn resolve_provider_credentials( // Merge expiration times, keeping the earliest non-zero value for (key, driver_expires_at_ms) in resolved.expires_at_ms { let provider_expires_at_ms = provider - .credential_expires_at_ms + .credential_expiration_times .get(&key) - .copied() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) .unwrap_or(0); let effective_expires_at_ms = match (provider_expires_at_ms, driver_expires_at_ms) { @@ -1199,10 +1206,13 @@ async fn resolve_provider_credentials( (provider, driver) => provider.min(driver), }; - if effective_expires_at_ms > 0 { + if effective_expires_at_ms > 0 + && let Ok(expiration_time) = + openshell_core::time::timestamp_from_millis(effective_expires_at_ms) + { provider - .credential_expires_at_ms - .insert(key, effective_expires_at_ms); + .credential_expiration_times + .insert(key, expiration_time); } } Ok(provider) @@ -1250,17 +1260,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("id-{name}"), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), config: Some(InferenceRouteConfig { provider_name: provider_name.to_string(), model_id: model_id.to_string(), - timeout_secs: 0, + request_timeout: None, }), version: 0, } @@ -1271,17 +1281,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("provider-{name}"), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: std::iter::once((key_name.to_string(), key_value.to_string())).collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), } @@ -1434,12 +1444,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "provider-bedrock-bridge".to_string(), name: "bedrock-bridge".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws-bedrock".to_string(), // Placeholder credential — the router ignores it because @@ -1455,7 +1465,7 @@ mod tests { "http://bedrock-bridge.demo.svc.cluster.local:8080".to_string(), )) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -1516,12 +1526,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "provider-bedrock-misconfigured".to_string(), name: "bedrock-misconfigured".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws-bedrock".to_string(), credentials: std::iter::once(( @@ -1531,7 +1541,7 @@ mod tests { .collect(), // Intentionally no BEDROCK_BASE_URL. config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -1570,12 +1580,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "provider-bedrock-bridge".to_string(), name: "bedrock-bridge".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "aws-bedrock".to_string(), credentials: HashMap::new(), @@ -1584,7 +1594,7 @@ mod tests { "http://bedrock-bridge.demo.svc.cluster.local:8080".to_string(), )) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -1663,7 +1673,7 @@ mod tests { assert_eq!(resp.routes[0].api_key, "sk-test"); assert_eq!(resp.routes[0].base_url, "https://api.openai.com/v1"); assert!(!resp.revision.is_empty()); - assert!(resp.generated_at_ms > 0); + assert!(resp.generated_time.is_some()); } #[tokio::test] @@ -1839,12 +1849,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "provider-1".to_string(), name: "openai-dev".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "openai".to_string(), credentials: std::iter::once(("OPENAI_API_KEY".to_string(), "sk-test".to_string())) @@ -1854,7 +1864,7 @@ mod tests { "https://station.example.com/v1".to_string(), )) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -1867,17 +1877,17 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "r-1".to_string(), name: CLUSTER_INFERENCE_ROUTE_NAME.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), config: Some(InferenceRouteConfig { provider_name: "openai-dev".to_string(), model_id: "test/model".to_string(), - timeout_secs: 0, + request_timeout: None, }), version: 1, }; @@ -1929,7 +1939,7 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "provider-1".to_string(), name: "openai-dev".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, workspace: "default".to_string(), @@ -1942,7 +1952,7 @@ mod tests { "https://station.example.com/v1".to_string(), )) .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), credential_handles: handles, profile_workspace: String::new(), }; @@ -1958,7 +1968,7 @@ mod tests { CLUSTER_INFERENCE_ROUTE_NAME, "openai-dev", "test/model", - 0, + None, false, ) .await @@ -2006,7 +2016,7 @@ mod tests { credentials: std::iter::once(("OPENAI_API_KEY".to_string(), "sk-rotated".to_string())) .collect(), config: provider.config.clone(), - credential_expires_at_ms: provider.credential_expires_at_ms.clone(), + credential_expiration_times: provider.credential_expiration_times.clone(), profile_workspace: provider.profile_workspace.clone(), credential_handles: HashMap::new(), }; @@ -2056,12 +2066,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "provider-vertex-test".to_string(), name: "vertex-test".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-vertex-ai".to_string(), credentials: std::iter::once(( @@ -2078,7 +2088,7 @@ mod tests { ] .into_iter() .collect(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -2400,12 +2410,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: format!("provider-{name}"), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 1, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "google-vertex-ai".to_string(), credentials: std::iter::once(( @@ -2414,7 +2424,7 @@ mod tests { )) .collect(), config, - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), } @@ -3551,12 +3561,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "provider-alpha".to_string(), name: "openai-alpha".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "alpha".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "openai".to_string(), credentials: std::iter::once(( @@ -3565,7 +3575,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; @@ -3578,12 +3588,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "provider-beta".to_string(), name: "anthropic-beta".to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: "beta".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: "anthropic".to_string(), credentials: std::iter::once(( @@ -3592,7 +3602,7 @@ mod tests { )) .collect(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: String::new(), credential_handles: HashMap::new(), }; diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index aafc0cd369..8059d87ebe 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -1691,12 +1691,15 @@ pub(crate) async fn ensure_default_workspace(store: &Store) -> Result<()> { metadata: Some(ObjectMeta { id: id.clone(), name: DEFAULT_WORKSPACE_NAME.to_string(), - created_at_ms: persistence::current_time_ms(), + created_time: openshell_core::time::timestamp_from_millis( + persistence::current_time_ms(), + ) + .ok(), labels: HashMap::new(), annotations: HashMap::new(), resource_version: 0, workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), status: Some(openshell_core::proto::datamodel::v1::WorkspaceStatus { phase: openshell_core::proto::datamodel::v1::WorkspacePhase::Active.into(), diff --git a/crates/openshell-server/src/persistence/legacy_time_wire.rs b/crates/openshell-server/src/persistence/legacy_time_wire.rs new file mode 100644 index 0000000000..65bdc29447 --- /dev/null +++ b/crates/openshell-server/src/persistence/legacy_time_wire.rs @@ -0,0 +1,495 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Compatibility rewriting for protobuf records written before time fields used WKTs. + +use prost::Message; +use prost_reflect::{DescriptorPool, Kind, MessageDescriptor}; +use std::sync::LazyLock; + +use super::{PersistenceError, PersistenceResult}; + +static DESCRIPTORS: LazyLock = LazyLock::new(|| { + DescriptorPool::decode(openshell_core::FILE_DESCRIPTOR_SET) + .expect("the embedded protobuf descriptor set must be valid") +}); + +#[derive(Clone, Copy)] +enum Conversion { + Timestamp { new_tag: u32 }, + TimestampString { new_tag: u32 }, + DurationSeconds { new_tag: u32 }, + DurationString { new_tag: u32 }, + TimestampMap { new_tag: u32 }, +} + +pub(super) fn migrate(object_type: &str, payload: &[u8]) -> PersistenceResult> { + let Some(message_name) = root_message_name(object_type) else { + return Ok(payload.to_vec()); + }; + let descriptor = DESCRIPTORS + .get_message_by_name(message_name) + .ok_or_else(|| { + PersistenceError::Decode(format!("missing descriptor for {message_name}")) + })?; + rewrite_message(&descriptor, payload) +} + +fn root_message_name(object_type: &str) -> Option<&'static str> { + match object_type { + "sandbox" => Some("openshell.v1.Sandbox"), + "provider" => Some("openshell.datamodel.v1.Provider"), + "workspace" => Some("openshell.datamodel.v1.Workspace"), + "workspace_member" => Some("openshell.v1.WorkspaceMember"), + "inference_route" => Some("openshell.inference.v1.InferenceRoute"), + "provider_profile" => Some("openshell.v1.StoredProviderProfile"), + "provider_credential_refresh_state" => { + Some("openshell.v1.StoredProviderCredentialRefreshState") + } + "service_endpoint" => Some("openshell.v1.ServiceEndpoint"), + "ssh_session" => Some("openshell.v1.SshSession"), + "sandbox_policy" => Some("openshell.v1.PolicyRevisionPayload"), + "draft_policy_chunk" => Some("openshell.v1.DraftChunkPayload"), + _ => None, + } +} + +fn rewrite_message(descriptor: &MessageDescriptor, input: &[u8]) -> PersistenceResult> { + let mut output = Vec::with_capacity(input.len()); + let mut offset = 0; + while offset < input.len() { + let field_start = offset; + let (key, key_len) = read_varint(&input[offset..])?; + offset += key_len; + let field_number = u32::try_from(key >> 3) + .map_err(|_| PersistenceError::Decode("protobuf field number overflow".into()))?; + let wire_type = (key & 7) as u8; + let (payload_start, payload_end, field_end) = field_bounds(input, offset, wire_type)?; + + if let Some(conversion) = conversion(descriptor.full_name(), field_number) { + rewrite_legacy_field( + &mut output, + conversion, + wire_type, + &input[payload_start..payload_end], + )?; + } else if wire_type == 2 + && let Some(field) = descriptor.get_field(field_number) + && !field.is_map() + && let Kind::Message(child) = field.kind() + { + let rewritten = rewrite_message(&child, &input[payload_start..payload_end])?; + write_key(&mut output, field_number, 2); + write_varint(&mut output, rewritten.len() as u64); + output.extend_from_slice(&rewritten); + } else { + output.extend_from_slice(&input[field_start..field_end]); + } + offset = field_end; + } + Ok(output) +} + +fn rewrite_legacy_field( + output: &mut Vec, + conversion: Conversion, + wire_type: u8, + payload: &[u8], +) -> PersistenceResult<()> { + match conversion { + Conversion::Timestamp { new_tag } => { + require_wire_type(wire_type, 0)?; + let (raw, consumed) = read_varint(payload)?; + if consumed != payload.len() { + return Err(PersistenceError::Decode("invalid legacy timestamp".into())); + } + let millis = raw.cast_signed(); + if millis != 0 { + let timestamp = openshell_core::time::timestamp_from_millis(millis) + .map_err(|error| PersistenceError::Decode(error.to_string()))?; + write_embedded(output, new_tag, ×tamp.encode_to_vec()); + } + } + Conversion::TimestampString { new_tag } => { + require_wire_type(wire_type, 2)?; + if !payload.is_empty() { + let value = std::str::from_utf8(payload).map_err(|error| { + PersistenceError::Decode(format!("legacy timestamp is not UTF-8: {error}")) + })?; + let timestamp: prost_types::Timestamp = value.parse().map_err(|error| { + PersistenceError::Decode(format!("invalid legacy timestamp: {error}")) + })?; + openshell_core::time::validate_timestamp(×tamp) + .map_err(|error| PersistenceError::Decode(error.to_string()))?; + write_embedded(output, new_tag, ×tamp.encode_to_vec()); + } + } + Conversion::DurationSeconds { new_tag } => { + require_wire_type(wire_type, 0)?; + let (seconds, consumed) = read_varint(payload)?; + if consumed != payload.len() { + return Err(PersistenceError::Decode("invalid legacy duration".into())); + } + if seconds != 0 { + let seconds = i64::try_from(seconds).map_err(|_| { + PersistenceError::Decode("legacy duration exceeds protobuf range".into()) + })?; + let duration = prost_types::Duration { seconds, nanos: 0 }; + openshell_core::time::validate_duration(&duration) + .map_err(|error| PersistenceError::Decode(error.to_string()))?; + write_embedded(output, new_tag, &duration.encode_to_vec()); + } + } + Conversion::DurationString { new_tag } => { + require_wire_type(wire_type, 2)?; + if !payload.is_empty() { + let value = std::str::from_utf8(payload).map_err(|error| { + PersistenceError::Decode(format!("legacy duration is not UTF-8: {error}")) + })?; + let duration = parse_legacy_duration(value)?; + let duration = openshell_core::time::duration_from_std(duration) + .map_err(|error| PersistenceError::Decode(error.to_string()))?; + write_embedded(output, new_tag, &duration.encode_to_vec()); + } + } + Conversion::TimestampMap { new_tag } => { + require_wire_type(wire_type, 2)?; + if let Some(rewritten) = rewrite_timestamp_map_entry(payload)? { + write_embedded(output, new_tag, &rewritten); + } + } + } + Ok(()) +} + +fn parse_legacy_duration(value: &str) -> PersistenceResult { + let (number, millis_multiplier) = value + .strip_suffix("ms") + .map(|number| (number, 1u64)) + .or_else(|| value.strip_suffix('s').map(|number| (number, 1_000u64))) + .ok_or_else(|| PersistenceError::Decode("legacy duration must end in ms or s".into()))?; + let amount = number + .parse::() + .map_err(|error| PersistenceError::Decode(format!("invalid legacy duration: {error}")))?; + let millis = amount + .checked_mul(millis_multiplier) + .ok_or_else(|| PersistenceError::Decode("legacy duration overflow".into()))?; + Ok(std::time::Duration::from_millis(millis)) +} + +fn rewrite_timestamp_map_entry(input: &[u8]) -> PersistenceResult>> { + let mut output = Vec::with_capacity(input.len() + 8); + let mut has_expiration = false; + let mut offset = 0; + while offset < input.len() { + let start = offset; + let (key, key_len) = read_varint(&input[offset..])?; + offset += key_len; + let number = u32::try_from(key >> 3) + .map_err(|_| PersistenceError::Decode("protobuf field number overflow".into()))?; + let wire_type = (key & 7) as u8; + let (payload_start, payload_end, field_end) = field_bounds(input, offset, wire_type)?; + if number == 2 { + require_wire_type(wire_type, 0)?; + let (raw, consumed) = read_varint(&input[payload_start..payload_end])?; + if consumed != payload_end - payload_start { + return Err(PersistenceError::Decode( + "invalid legacy expiration map".into(), + )); + } + let millis = raw.cast_signed(); + if millis != 0 { + let timestamp = openshell_core::time::timestamp_from_millis(millis) + .map_err(|error| PersistenceError::Decode(error.to_string()))?; + write_embedded(&mut output, 2, ×tamp.encode_to_vec()); + has_expiration = true; + } + } else { + output.extend_from_slice(&input[start..field_end]); + } + offset = field_end; + } + Ok(has_expiration.then_some(output)) +} + +fn conversion(message: &str, field: u32) -> Option { + use Conversion::{ + DurationSeconds as D, DurationString as DS, Timestamp as T, TimestampMap as M, + TimestampString as TS, + }; + match (message, field) { + ("openshell.datamodel.v1.ObjectMeta", 3) => Some(T { new_tag: 103 }), + ("openshell.datamodel.v1.ObjectMeta" | "openshell.v1.SshSession", 8) => { + Some(T { new_tag: 108 }) + } + ("openshell.datamodel.v1.Provider", 5) => Some(M { new_tag: 105 }), + ("openshell.inference.v1.InferenceRouteConfig", 3) => Some(D { new_tag: 103 }), + ("openshell.v1.SandboxCondition", 5) => Some(TS { new_tag: 105 }), + ("openshell.v1.PlatformEvent", 1) => Some(T { new_tag: 101 }), + ( + "openshell.v1.ProviderCredentialTokenGrant" | "openshell.v1.ProviderCredentialRefresh", + 4, + ) => Some(D { new_tag: 104 }), + ("openshell.v1.ProviderCredentialRefresh", 5) => Some(D { new_tag: 105 }), + ("openshell.sandbox.v1.MiddlewareBinding", 4) => Some(DS { new_tag: 104 }), + _ => None, + } +} + +fn field_bounds( + input: &[u8], + value_offset: usize, + wire_type: u8, +) -> PersistenceResult<(usize, usize, usize)> { + match wire_type { + 0 => { + let (_, len) = read_varint(&input[value_offset..])?; + Ok((value_offset, value_offset + len, value_offset + len)) + } + 1 => checked_fixed_bounds(input, value_offset, 8), + 2 => { + let (len, prefix_len) = read_varint(&input[value_offset..])?; + let start = value_offset + prefix_len; + let len = usize::try_from(len) + .map_err(|_| PersistenceError::Decode("protobuf length overflow".into()))?; + let end = start + .checked_add(len) + .filter(|end| *end <= input.len()) + .ok_or_else(|| PersistenceError::Decode("truncated protobuf field".into()))?; + Ok((start, end, end)) + } + 5 => checked_fixed_bounds(input, value_offset, 4), + _ => Err(PersistenceError::Decode(format!( + "unsupported protobuf wire type {wire_type}" + ))), + } +} + +fn checked_fixed_bounds( + input: &[u8], + value_offset: usize, + len: usize, +) -> PersistenceResult<(usize, usize, usize)> { + let end = value_offset + .checked_add(len) + .filter(|end| *end <= input.len()) + .ok_or_else(|| PersistenceError::Decode("truncated protobuf field".into()))?; + Ok((value_offset, end, end)) +} + +fn read_varint(input: &[u8]) -> PersistenceResult<(u64, usize)> { + let mut value = 0u64; + for (index, byte) in input.iter().copied().take(10).enumerate() { + value |= u64::from(byte & 0x7f) << (index * 7); + if byte & 0x80 == 0 { + return Ok((value, index + 1)); + } + } + Err(PersistenceError::Decode("invalid protobuf varint".into())) +} + +fn write_key(output: &mut Vec, field_number: u32, wire_type: u8) { + write_varint( + output, + (u64::from(field_number) << 3) | u64::from(wire_type), + ); +} + +fn write_varint(output: &mut Vec, mut value: u64) { + while value >= 0x80 { + output.push(value.to_le_bytes()[0] | 0x80); + value >>= 7; + } + output.push(value.to_le_bytes()[0]); +} + +fn write_embedded(output: &mut Vec, field_number: u32, payload: &[u8]) { + write_key(output, field_number, 2); + write_varint(output, payload.len() as u64); + output.extend_from_slice(payload); +} + +fn require_wire_type(actual: u8, expected: u8) -> PersistenceResult<()> { + if actual == expected { + Ok(()) + } else { + Err(PersistenceError::Decode(format!( + "legacy time field has wire type {actual}, expected {expected}" + ))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::proto::{InferenceRoute, Provider}; + use std::collections::HashMap; + + #[derive(Clone, PartialEq, Message)] + struct LegacyObjectMeta { + #[prost(string, tag = "1")] + id: String, + #[prost(string, tag = "2")] + name: String, + #[prost(int64, tag = "3")] + created_at_ms: i64, + #[prost(int64, tag = "8")] + deletion_timestamp_ms: i64, + } + + #[derive(Clone, PartialEq, Message)] + struct LegacyProvider { + #[prost(message, optional, tag = "1")] + metadata: Option, + #[prost(string, tag = "2")] + r#type: String, + #[prost(map = "string, int64", tag = "5")] + credential_expires_at_ms: HashMap, + } + + #[derive(Clone, PartialEq, Message)] + struct LegacyInferenceRouteConfig { + #[prost(string, tag = "1")] + provider_name: String, + #[prost(string, tag = "2")] + model_id: String, + #[prost(uint64, tag = "3")] + timeout_secs: u64, + } + + #[derive(Clone, PartialEq, Message)] + struct LegacyInferenceRoute { + #[prost(message, optional, tag = "1")] + metadata: Option, + #[prost(message, optional, tag = "2")] + config: Option, + } + + #[test] + fn migrates_nested_metadata_and_timestamp_maps() { + let legacy = LegacyProvider { + metadata: Some(LegacyObjectMeta { + id: "provider-id".into(), + name: "provider-name".into(), + created_at_ms: 1_700_000_000_123, + deletion_timestamp_ms: 1_700_000_001_456, + }), + r#type: "test".into(), + credential_expires_at_ms: HashMap::from([ + ("TOKEN".into(), 1_700_000_002_789), + ("NO_EXPIRY".into(), 0), + ]), + }; + + let migrated = migrate("provider", &legacy.encode_to_vec()).unwrap(); + let provider = Provider::decode(migrated.as_slice()).unwrap(); + let metadata = provider.metadata.unwrap(); + assert_eq!( + openshell_core::time::timestamp_to_millis(&metadata.created_time.unwrap()).unwrap(), + 1_700_000_000_123 + ); + assert_eq!( + openshell_core::time::timestamp_to_millis(&metadata.deletion_time.unwrap()).unwrap(), + 1_700_000_001_456 + ); + assert_eq!( + openshell_core::time::timestamp_to_millis( + provider.credential_expiration_times.get("TOKEN").unwrap() + ) + .unwrap(), + 1_700_000_002_789 + ); + assert!( + !provider + .credential_expiration_times + .contains_key("NO_EXPIRY") + ); + } + + #[test] + fn migrates_nested_duration() { + let legacy = LegacyInferenceRoute { + metadata: Some(LegacyObjectMeta { + id: "route-id".into(), + name: "route-name".into(), + created_at_ms: 1_700_000_000_000, + deletion_timestamp_ms: 0, + }), + config: Some(LegacyInferenceRouteConfig { + provider_name: "provider".into(), + model_id: "model".into(), + timeout_secs: 90, + }), + }; + + let migrated = migrate("inference_route", &legacy.encode_to_vec()).unwrap(); + let route = InferenceRoute::decode(migrated.as_slice()).unwrap(); + assert_eq!(route.config.unwrap().request_timeout.unwrap().seconds, 90); + } + + #[test] + fn rejects_malformed_legacy_time_wire_type() { + let descriptor = DESCRIPTORS + .get_message_by_name("openshell.datamodel.v1.ObjectMeta") + .unwrap(); + // Legacy field 3 encoded as length-delimited instead of int64 varint. + let error = rewrite_message(&descriptor, &[0x1a, 0x01, 0x00]).unwrap_err(); + assert!(error.to_string().contains("wire type")); + } + + #[test] + fn rejects_out_of_range_legacy_timestamps() { + let legacy = LegacyObjectMeta { + id: "invalid".into(), + name: "invalid".into(), + created_at_ms: i64::MAX, + deletion_timestamp_ms: 0, + }; + let descriptor = DESCRIPTORS + .get_message_by_name("openshell.datamodel.v1.ObjectMeta") + .unwrap(); + let error = rewrite_message(&descriptor, &legacy.encode_to_vec()).unwrap_err(); + assert!(error.to_string().contains("timestamp")); + } + + #[test] + fn public_time_fields_use_well_known_types() { + let private_storage_messages = [ + "openshell.v1.StoredProviderCredentialRefreshState", + "openshell.v1.PolicyRevisionPayload", + "openshell.v1.DraftChunkPayload", + "openshell.v1.StoredPolicyRevision", + "openshell.v1.StoredDraftChunk", + ]; + let mut violations = Vec::new(); + for message in DESCRIPTORS.all_messages() { + if private_storage_messages.contains(&message.full_name()) { + continue; + } + for field in message.fields() { + let name = field.name(); + let looks_temporal = name.ends_with("_ms") + || name.ends_with("_secs") + || name.ends_with("_seconds") + || name == "timeout" + || name == "expires_in" + || name == "last_transition_time"; + if looks_temporal + && !matches!( + field.kind(), + Kind::Message(ref descriptor) + if descriptor.full_name() == "google.protobuf.Timestamp" + || descriptor.full_name() == "google.protobuf.Duration" + ) + { + violations.push(format!("{}.{}", message.full_name(), name)); + } + } + } + assert!( + violations.is_empty(), + "public scalar time fields remain: {}", + violations.join(", ") + ); + } +} diff --git a/crates/openshell-server/src/persistence/mod.rs b/crates/openshell-server/src/persistence/mod.rs index 516faf4fe4..1977a53985 100644 --- a/crates/openshell-server/src/persistence/mod.rs +++ b/crates/openshell-server/src/persistence/mod.rs @@ -3,6 +3,7 @@ //! Persistence layer for `OpenShell` Server. +mod legacy_time_wire; mod postgres; mod sqlite; @@ -132,6 +133,10 @@ pub trait ObjectType { fn object_type() -> &'static str; } +pub fn migrate_legacy_time_fields(object_type: &str, payload: &[u8]) -> PersistenceResult> { + legacy_time_wire::migrate(object_type, payload) +} + // Import object metadata accessor traits from openshell-core // (implementations for all proto types are in openshell-core::metadata) pub use openshell_core::{ @@ -157,10 +162,11 @@ pub fn generate_name() -> String { /// Extracted to avoid repeating the identical decode-and-hydrate block across /// `get_message`, `get_message_by_name`, `list_messages`, and /// `list_messages_with_selector`. -fn decode_record( +fn decode_record( record: ObjectRecord, ) -> PersistenceResult { - let mut message = T::decode(record.payload.as_slice()) + let payload = legacy_time_wire::migrate(T::object_type(), &record.payload)?; + let mut message = T::decode(payload.as_slice()) .map_err(|e| PersistenceError::Decode(format!("protobuf decode error: {e}")))?; message.set_resource_version(record.resource_version); Ok(message) diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index 8bab0ada96..5d1634c391 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -40,7 +40,44 @@ impl PostgresStore { POSTGRES_MIGRATOR .run(&self.pool) .await - .map_err(|e| map_migrate_error(&e)) + .map_err(|e| map_migrate_error(&e))?; + self.migrate_legacy_time_payloads().await + } + + async fn migrate_legacy_time_payloads(&self) -> PersistenceResult<()> { + let mut transaction = self.pool.begin().await.map_err(|e| map_db_error(&e))?; + // Serialize this application-level data migration across gateway replicas. + sqlx::query("SELECT pg_advisory_xact_lock(3052)") + .execute(&mut *transaction) + .await + .map_err(|e| map_db_error(&e))?; + let rows = + sqlx::query("SELECT id, object_type, payload FROM objects ORDER BY id FOR UPDATE") + .fetch_all(&mut *transaction) + .await + .map_err(|e| map_db_error(&e))?; + + for row in rows { + let id: String = row.try_get("id").map_err(|e| map_db_error(&e))?; + let object_type: String = row.try_get("object_type").map_err(|e| map_db_error(&e))?; + let payload: Vec = row.try_get("payload").map_err(|e| map_db_error(&e))?; + let migrated = + super::legacy_time_wire::migrate(&object_type, &payload).map_err(|error| { + PersistenceError::Migration(format!( + "failed to migrate {object_type} record {id}: {error}" + )) + })?; + if migrated != payload { + sqlx::query("UPDATE objects SET payload = $1 WHERE id = $2") + .bind(migrated) + .bind(id) + .execute(&mut *transaction) + .await + .map_err(|e| map_db_error(&e))?; + } + } + + transaction.commit().await.map_err(|e| map_db_error(&e)) } /// Verify the database is reachable by acquiring a pooled connection diff --git a/crates/openshell-server/src/persistence/sqlite.rs b/crates/openshell-server/src/persistence/sqlite.rs index 658530f753..31da2c0172 100644 --- a/crates/openshell-server/src/persistence/sqlite.rs +++ b/crates/openshell-server/src/persistence/sqlite.rs @@ -72,7 +72,38 @@ impl SqliteStore { SQLITE_MIGRATOR .run(&self.pool) .await - .map_err(|e| map_migrate_error(&e)) + .map_err(|e| map_migrate_error(&e))?; + self.migrate_legacy_time_payloads().await + } + + async fn migrate_legacy_time_payloads(&self) -> PersistenceResult<()> { + let mut transaction = self.pool.begin().await.map_err(|e| map_db_error(&e))?; + let rows = sqlx::query("SELECT id, object_type, payload FROM objects ORDER BY id") + .fetch_all(&mut *transaction) + .await + .map_err(|e| map_db_error(&e))?; + + for row in rows { + let id: String = row.try_get("id").map_err(|e| map_db_error(&e))?; + let object_type: String = row.try_get("object_type").map_err(|e| map_db_error(&e))?; + let payload: Vec = row.try_get("payload").map_err(|e| map_db_error(&e))?; + let migrated = + super::legacy_time_wire::migrate(&object_type, &payload).map_err(|error| { + PersistenceError::Migration(format!( + "failed to migrate {object_type} record {id}: {error}" + )) + })?; + if migrated != payload { + sqlx::query("UPDATE objects SET payload = ?1 WHERE id = ?2") + .bind(migrated) + .bind(id) + .execute(&mut *transaction) + .await + .map_err(|e| map_db_error(&e))?; + } + } + + transaction.commit().await.map_err(|e| map_db_error(&e)) } /// Verify the database is reachable by acquiring a pooled connection diff --git a/crates/openshell-server/src/persistence/tests.rs b/crates/openshell-server/src/persistence/tests.rs index 8802ac8d10..e8f9ef929d 100644 --- a/crates/openshell-server/src/persistence/tests.rs +++ b/crates/openshell-server/src/persistence/tests.rs @@ -7,6 +7,7 @@ use openshell_core::proto::datamodel::v1::ObjectMeta as ProtoObjectMeta; use openshell_core::proto::{ObjectForTest, Sandbox, SandboxPolicy, SandboxSpec}; use prost::Message; use std::collections::HashMap as StdHashMap; +use std::str::FromStr; /// A failed store call must be visible as a failure in the trace, not as a /// span that merely happened to return nothing. @@ -132,6 +133,122 @@ async fn sqlite_connect_runs_embedded_migrations() { assert!(records.is_empty()); } +#[derive(Clone, PartialEq, Message)] +struct LegacyTimeObjectMeta { + #[prost(string, tag = "1")] + id: String, + #[prost(string, tag = "2")] + name: String, + #[prost(int64, tag = "3")] + created_at_ms: i64, +} + +#[derive(Clone, PartialEq, Message)] +struct LegacyTimeSandbox { + #[prost(message, optional, tag = "1")] + metadata: Option, +} + +#[tokio::test] +async fn sqlite_startup_migrates_legacy_time_payloads_idempotently() { + let tmp = tempfile::tempdir().expect("tempdir"); + let db_path = tmp.path().join("legacy-time.db"); + let url = format!("sqlite:{}?mode=rwc", db_path.display()); + let legacy = LegacyTimeSandbox { + metadata: Some(LegacyTimeObjectMeta { + id: "legacy-sandbox".into(), + name: "legacy".into(), + created_at_ms: 1_700_000_000_123, + }), + } + .encode_to_vec(); + + let store = Store::connect(&url).await.expect("create database"); + store + .put( + "sandbox", + "legacy-sandbox", + "legacy", + "default", + &legacy, + None, + ) + .await + .expect("seed legacy payload"); + store.close().await; + + let store = Store::connect(&url).await.expect("migrate legacy payload"); + let first = store + .get("sandbox", "legacy-sandbox") + .await + .expect("read migrated record") + .expect("record exists") + .payload; + let decoded = Sandbox::decode(first.as_slice()).expect("decode current sandbox"); + assert_eq!( + decoded.metadata.unwrap().created_time.unwrap().seconds, + 1_700_000_000 + ); + store.close().await; + + let store = Store::connect(&url).await.expect("repeat migration"); + let second = store + .get("sandbox", "legacy-sandbox") + .await + .expect("read migrated record") + .expect("record exists") + .payload; + assert_eq!(first, second, "the startup migration is idempotent"); +} + +#[tokio::test] +async fn sqlite_startup_rolls_back_all_payloads_on_malformed_legacy_data() { + let tmp = tempfile::tempdir().expect("tempdir"); + let db_path = tmp.path().join("malformed-legacy-time.db"); + let url = format!("sqlite:{}?mode=rwc", db_path.display()); + let legacy = LegacyTimeSandbox { + metadata: Some(LegacyTimeObjectMeta { + id: "a-valid".into(), + name: "valid".into(), + created_at_ms: 1_700_000_000_123, + }), + } + .encode_to_vec(); + + let store = Store::connect(&url).await.expect("create database"); + store + .put("sandbox", "a-valid", "valid", "default", &legacy, None) + .await + .expect("seed valid legacy payload"); + store + .put( + "sandbox", + "z-malformed", + "malformed", + "default", + &[0x0a, 0x02, 0x1a, 0x00], + None, + ) + .await + .expect("seed malformed legacy payload"); + store.close().await; + + let error = Store::connect(&url) + .await + .expect_err("startup must reject malformed legacy payload"); + assert!(error.to_string().contains("z-malformed")); + + let options = sqlx::sqlite::SqliteConnectOptions::from_str(&url).expect("sqlite options"); + let pool = sqlx::SqlitePool::connect_with(options) + .await + .expect("inspect rolled-back database"); + let stored: Vec = sqlx::query_scalar("SELECT payload FROM objects WHERE id = 'a-valid'") + .fetch_one(&pool) + .await + .expect("read valid payload after rollback"); + assert_eq!(stored, legacy, "the earlier update was rolled back"); +} + #[cfg(unix)] #[tokio::test] async fn sqlite_connect_restricts_db_file_permissions() { @@ -828,7 +945,7 @@ fn policy_test_sandbox(id: &str, name: &str) -> Sandbox { metadata: Some(ProtoObjectMeta { id: id.to_string(), name: name.to_string(), - created_at_ms: 1, + created_time: openshell_core::time::timestamp_from_millis(1).ok(), workspace: "default".to_string(), ..Default::default() }), @@ -1700,12 +1817,12 @@ async fn cas_update_message_cas_succeeds() { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "test-id".to_string(), name: "test-sandbox".to_string(), - created_at_ms: 1000, + created_time: openshell_core::time::timestamp_from_millis(1000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: std::collections::HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: None, status: None, @@ -1742,12 +1859,12 @@ async fn cas_update_message_cas_conflicts_on_concurrent_updates() { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "test-id".to_string(), name: "test-sandbox".to_string(), - created_at_ms: 1000, + created_time: openshell_core::time::timestamp_from_millis(1000).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: std::collections::HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: None, status: None, @@ -1812,12 +1929,12 @@ async fn cas_update_message_cas_rejects_workspace_change() { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "ws-immutable".to_string(), name: "test-sandbox".to_string(), - created_at_ms: 1000, + created_time: openshell_core::time::timestamp_from_millis(1000).ok(), labels: std::collections::HashMap::new(), annotations: std::collections::HashMap::new(), resource_version: 0, workspace: "alpha".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: None, status: None, @@ -1854,12 +1971,12 @@ async fn cas_update_message_cas_rejects_name_change() { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "name-immutable".to_string(), name: "original".to_string(), - created_at_ms: 1000, + created_time: openshell_core::time::timestamp_from_millis(1000).ok(), labels: std::collections::HashMap::new(), annotations: std::collections::HashMap::new(), resource_version: 0, workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: None, status: None, diff --git a/crates/openshell-server/src/policy_store.rs b/crates/openshell-server/src/policy_store.rs index bd044c8712..bc9a039268 100644 --- a/crates/openshell-server/src/policy_store.rs +++ b/crates/openshell-server/src/policy_store.rs @@ -56,7 +56,8 @@ pub fn project_policy_revision_onto_sandbox( }); } - let mut sandbox = Sandbox::decode(payload) + let payload = crate::persistence::migrate_legacy_time_fields("sandbox", payload)?; + let mut sandbox = Sandbox::decode(payload.as_slice()) .map_err(|e| PersistenceError::Decode(format!("decode sandbox payload failed: {e}")))?; sandbox.set_resource_version(current_resource_version); diff --git a/crates/openshell-server/src/provider_profile_sources.rs b/crates/openshell-server/src/provider_profile_sources.rs index cbe5ff892c..970d0cd233 100644 --- a/crates/openshell-server/src/provider_profile_sources.rs +++ b/crates/openshell-server/src/provider_profile_sources.rs @@ -717,12 +717,12 @@ pub fn stored_provider_profile(profile: ProviderProfile) -> StoredProviderProfil metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: uuid::Uuid::new_v4().to_string(), name: profile.id.clone(), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: std::collections::HashMap::new(), workspace: String::new(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(profile), } @@ -1347,12 +1347,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: uuid::Uuid::new_v4().to_string(), name: proto.id.clone(), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: std::collections::HashMap::new(), resource_version: 0, annotations: std::collections::HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), profile: Some(proto), } diff --git a/crates/openshell-server/src/provider_refresh.rs b/crates/openshell-server/src/provider_refresh.rs index 176f86ae22..18df0c2a6b 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -248,10 +248,11 @@ pub async fn delete_refresh_state_with_credentials( if state .metadata .as_ref() - .is_some_and(|metadata| metadata.deletion_timestamp_ms == 0) + .is_some_and(|metadata| metadata.deletion_time.is_none()) { if let Some(metadata) = state.metadata.as_mut() { - metadata.deletion_timestamp_ms = current_time_ms(); + metadata.deletion_time = + openshell_core::time::timestamp_from_millis(current_time_ms()).ok(); } state.authorization_epoch = uuid::Uuid::new_v4().to_string(); state.status = "deleting".to_string(); @@ -320,14 +321,32 @@ pub fn refresh_status_from_state( credential_key: state.credential_key.clone(), strategy: state.strategy, status: state.status.clone(), - expires_at_ms: state.expires_at_ms, - next_refresh_at_ms: state.next_refresh_at_ms, - last_refresh_at_ms: state.last_refresh_at_ms, + expiration_time: openshell_core::time::optional_timestamp_from_legacy_millis( + state.expires_at_ms, + ) + .ok() + .flatten(), + next_refresh_time: if state.next_refresh_at_ms == i64::MAX { + None + } else { + openshell_core::time::optional_timestamp_from_legacy_millis(state.next_refresh_at_ms) + .ok() + .flatten() + }, + last_refresh_time: openshell_core::time::optional_timestamp_from_legacy_millis( + state.last_refresh_at_ms, + ) + .ok() + .flatten(), last_error: state.last_error.clone(), recovery_action: state.recovery_action, failure_code: state.failure_code.clone(), provider_error_subtype: state.provider_error_subtype.clone(), - last_error_at_ms: state.last_error_at_ms, + last_error_time: openshell_core::time::optional_timestamp_from_legacy_millis( + state.last_error_at_ms, + ) + .ok() + .flatten(), } } @@ -366,12 +385,12 @@ pub fn new_refresh_state( metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: uuid::Uuid::new_v4().to_string(), name: refresh_state_name(&provider_id, credential_key), - created_at_ms: now_ms, + created_time: openshell_core::time::timestamp_from_millis(now_ms).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), provider_id, provider_name, @@ -790,7 +809,7 @@ pub async fn refresh_provider_credential( if state .metadata .as_ref() - .is_some_and(|metadata| metadata.deletion_timestamp_ms != 0) + .is_some_and(|metadata| metadata.deletion_time.is_some()) { return Err(Status::failed_precondition( "provider refresh is being deleted", @@ -1149,19 +1168,22 @@ async fn apply_minted_credential( } None }; - if minted.expires_at_ms > 0 { + let credential_expiration_time = + openshell_core::time::optional_timestamp_from_legacy_millis(minted.expires_at_ms) + .map_err(|error| Status::internal(error.to_string()))?; + if let Some(expiration_time) = credential_expiration_time.as_ref() { updated - .credential_expires_at_ms - .insert(credential_key.to_string(), minted.expires_at_ms); + .credential_expiration_times + .insert(credential_key.to_string(), *expiration_time); for key in minted.additional_credentials.keys() { updated - .credential_expires_at_ms - .insert(key.clone(), minted.expires_at_ms); + .credential_expiration_times + .insert(key.clone(), *expiration_time); } } else { - updated.credential_expires_at_ms.remove(credential_key); + updated.credential_expiration_times.remove(credential_key); for key in minted.additional_credentials.keys() { - updated.credential_expires_at_ms.remove(key); + updated.credential_expiration_times.remove(key); } } if let Err(err) = crate::grpc::provider::validate_provider_update_against_attached_sandboxes( @@ -1202,19 +1224,19 @@ async fn apply_minted_credential( current.credentials.insert(key.clone(), value.clone()); } } - if minted.expires_at_ms > 0 { + if let Some(expiration_time) = credential_expiration_time.as_ref() { current - .credential_expires_at_ms - .insert(credential_key.to_string(), minted.expires_at_ms); + .credential_expiration_times + .insert(credential_key.to_string(), *expiration_time); for key in minted.additional_credentials.keys() { current - .credential_expires_at_ms - .insert(key.clone(), minted.expires_at_ms); + .credential_expiration_times + .insert(key.clone(), *expiration_time); } } else { - current.credential_expires_at_ms.remove(credential_key); + current.credential_expiration_times.remove(credential_key); for key in minted.additional_credentials.keys() { - current.credential_expires_at_ms.remove(key); + current.credential_expiration_times.remove(key); } } }) @@ -1880,11 +1902,11 @@ pub fn spawn_refresh_worker(state: std::sync::Arc, interval: ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { ticker.tick().await; - if let Err(err) = run_refresh_worker_tick( + if let Err(err) = Box::pin(run_refresh_worker_tick( state.store.as_ref(), Some(&state.credentials), Some(&state.compute), - ) + )) .await { warn!(error = %err, "provider credential refresh worker tick failed"); @@ -1931,7 +1953,7 @@ async fn run_refresh_worker_tick( if state .metadata .as_ref() - .is_some_and(|metadata| metadata.deletion_timestamp_ms != 0) + .is_some_and(|metadata| metadata.deletion_time.is_some()) { let Some(credentials) = credentials else { warn!( @@ -2070,6 +2092,10 @@ mod tests { use wiremock::matchers::{body_string_contains, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; + fn ts(milliseconds: i64) -> prost_types::Timestamp { + openshell_core::time::timestamp_from_millis(milliseconds).unwrap() + } + fn test_credentials() -> CredentialRuntime { CredentialRuntime::from_config(&Config::new(None).with_credential_drivers(["test-static"])) .expect("test credential runtime") @@ -2738,8 +2764,10 @@ mod tests { Some(&"minted-graph-token".to_string()) ); assert_eq!( - stored.credential_expires_at_ms.get("MS_GRAPH_ACCESS_TOKEN"), - Some(&refreshed.expires_at_ms) + stored + .credential_expiration_times + .get("MS_GRAPH_ACCESS_TOKEN"), + Some(&ts(refreshed.expires_at_ms)) ); } @@ -2833,8 +2861,10 @@ mod tests { .unwrap(); assert_eq!(handle.driver, "test-static"); assert_eq!( - stored.credential_expires_at_ms.get("MS_GRAPH_ACCESS_TOKEN"), - Some(&refreshed.expires_at_ms) + stored + .credential_expiration_times + .get("MS_GRAPH_ACCESS_TOKEN"), + Some(&ts(refreshed.expires_at_ms)) ); let resolved = credentials @@ -2874,12 +2904,12 @@ mod tests { metadata: Some(ObjectMeta { id: "sandbox-collision".to_string(), name: "collision".to_string(), - created_at_ms: 1, + created_time: openshell_core::time::timestamp_from_millis(1).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["existing-graph".to_string(), "refreshing-graph".to_string()], @@ -3036,9 +3066,9 @@ mod tests { ); assert_eq!( stored_provider - .credential_expires_at_ms + .credential_expiration_times .get("MS_GRAPH_ACCESS_TOKEN"), - Some(&refreshed.expires_at_ms) + Some(&ts(refreshed.expires_at_ms)) ); let stored_state = get_refresh_state( @@ -3375,7 +3405,9 @@ mod tests { .unwrap(); put_refresh_state(&store, &state).await.unwrap(); - run_refresh_worker_tick(&store, None, None).await.unwrap(); + Box::pin(run_refresh_worker_tick(&store, None, None)) + .await + .unwrap(); let stored_state = get_refresh_state( &store, @@ -3431,9 +3463,13 @@ mod tests { state.next_refresh_at_ms = i64::MAX; put_refresh_state(&store, &state).await.unwrap(); - run_refresh_worker_tick(&store, Some(&test_credentials()), None) - .await - .unwrap(); + Box::pin(run_refresh_worker_tick( + &store, + Some(&test_credentials()), + None, + )) + .await + .unwrap(); let stored = get_refresh_state( &store, @@ -3486,12 +3522,13 @@ mod tests { .await .unwrap(); state.material.clear(); - state.metadata.as_mut().unwrap().deletion_timestamp_ms = current_time_ms(); + state.metadata.as_mut().unwrap().deletion_time = + openshell_core::time::timestamp_from_millis(current_time_ms()).ok(); state.status = "deleting".to_string(); put_refresh_state(&store, &state).await.unwrap(); assert_eq!(credentials.stored_credential_count(), Some(1)); - run_refresh_worker_tick(&store, Some(&credentials), None) + Box::pin(run_refresh_worker_tick(&store, Some(&credentials), None)) .await .unwrap(); @@ -3519,7 +3556,9 @@ mod tests { let store = test_store().await; let traced = test_exporter::install_traced(); - run_refresh_worker_tick(&store, None, None).await.unwrap(); + Box::pin(run_refresh_worker_tick(&store, None, None)) + .await + .unwrap(); let spans = traced.finished_spans(); let root = spans @@ -3900,16 +3939,18 @@ mod tests { Some(&"FwoGZXIvYXdzEBYaDH...EXAMPLETOKEN".to_string()) ); assert_eq!( - stored.credential_expires_at_ms.get("AWS_ACCESS_KEY_ID"), - Some(&4_000_000_000_000) + stored.credential_expiration_times.get("AWS_ACCESS_KEY_ID"), + Some(&ts(4_000_000_000_000)) ); assert_eq!( - stored.credential_expires_at_ms.get("AWS_SECRET_ACCESS_KEY"), - Some(&4_000_000_000_000) + stored + .credential_expiration_times + .get("AWS_SECRET_ACCESS_KEY"), + Some(&ts(4_000_000_000_000)) ); assert_eq!( - stored.credential_expires_at_ms.get("AWS_SESSION_TOKEN"), - Some(&4_000_000_000_000) + stored.credential_expiration_times.get("AWS_SESSION_TOKEN"), + Some(&ts(4_000_000_000_000)) ); } @@ -4020,12 +4061,12 @@ mod tests { metadata: Some(ObjectMeta { id: "sandbox-aws-collision".to_string(), name: "aws-collision".to_string(), - created_at_ms: 1, + created_time: openshell_core::time::timestamp_from_millis(1).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), spec: Some(SandboxSpec { providers: vec!["existing-aws".to_string(), "refreshing-aws".to_string()], @@ -4512,17 +4553,17 @@ mod tests { metadata: Some(ObjectMeta { id: format!("{name}-id"), name: name.to_string(), - created_at_ms: 1, + created_time: openshell_core::time::timestamp_from_millis(1).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: provider_type.to_string(), credentials: HashMap::new(), config: HashMap::new(), - credential_expires_at_ms: HashMap::new(), + credential_expiration_times: HashMap::new(), profile_workspace: "default".to_string(), credential_handles: HashMap::new(), } diff --git a/crates/openshell-server/src/service_routing.rs b/crates/openshell-server/src/service_routing.rs index 3e80bc26f5..25fcdeb289 100644 --- a/crates/openshell-server/src/service_routing.rs +++ b/crates/openshell-server/src/service_routing.rs @@ -825,12 +825,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "endpoint-id".to_string(), name: "my-sandbox--web".to_string(), - created_at_ms: 1_700_000_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_700_000_000_000).ok(), labels: std::collections::HashMap::default(), resource_version: 0, annotations: std::collections::HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), sandbox_id: "sandbox-id".to_string(), sandbox_name: "my-sandbox".to_string(), @@ -1210,12 +1210,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: "ep-1".to_string(), name: "my-sandbox--web".to_string(), - created_at_ms: 1_700_000_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_700_000_000_000).ok(), labels: std::collections::HashMap::default(), resource_version: 0, annotations: std::collections::HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), sandbox_id: "sandbox-1".to_string(), sandbox_name: "my-sandbox".to_string(), diff --git a/crates/openshell-server/src/ssh_sessions.rs b/crates/openshell-server/src/ssh_sessions.rs index 490f0dbb25..1a0a68c47e 100644 --- a/crates/openshell-server/src/ssh_sessions.rs +++ b/crates/openshell-server/src/ssh_sessions.rs @@ -48,8 +48,12 @@ async fn reap_expired_sessions(store: &Store) -> Result<(), String> { Err(_) => continue, }; - let should_delete = - (session.expires_at_ms > 0 && now_ms > session.expires_at_ms) || session.revoked; + let should_delete = session + .expiration_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .is_some_and(|expiration_ms| now_ms > expiration_ms) + || session.revoked; if should_delete { if let Err(e) = store @@ -83,16 +87,19 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: format!("session-{id}"), - created_at_ms: 1000, + created_time: openshell_core::time::timestamp_from_millis(1000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), sandbox_id: sandbox_id.to_string(), token: id.to_string(), - expires_at_ms, + expiration_time: (expires_at_ms != 0) + .then(|| openshell_core::time::timestamp_from_millis(expires_at_ms)) + .transpose() + .unwrap(), revoked, } } diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index c8491dc1eb..7870a754de 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -652,7 +652,7 @@ fn sandbox_proto_is_terminating(sandbox: &Sandbox) -> bool { || sandbox .metadata .as_ref() - .is_some_and(|metadata| metadata.deletion_timestamp_ms != 0) + .is_some_and(|metadata| metadata.deletion_time.is_some()) } async fn sandbox_is_terminating_or_gone(state: &Arc, sandbox_id: &str) -> bool { @@ -773,7 +773,10 @@ pub async fn handle_connect_supervisor( let accepted = GatewayMessage { payload: Some(gateway_message::Payload::SessionAccepted(SessionAccepted { session_id: session_id.clone(), - heartbeat_interval_secs: HEARTBEAT_INTERVAL_SECS, + heartbeat_interval: openshell_core::time::duration_from_std(Duration::from_secs( + u64::from(HEARTBEAT_INTERVAL_SECS), + )) + .ok(), })), }; if tx.send(accepted).await.is_err() { @@ -1054,12 +1057,12 @@ mod tests { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: id.to_string(), name: name.to_string(), - created_at_ms: 1_000_000, + created_time: openshell_core::time::timestamp_from_millis(1_000_000).ok(), labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + deletion_time: None, }), ..Default::default() } @@ -1550,7 +1553,8 @@ mod tests { #[test] fn sandbox_proto_terminating_detects_deletion_timestamp() { let mut sandbox = sandbox_record("sbx-1", "sandbox-one"); - sandbox.metadata.as_mut().unwrap().deletion_timestamp_ms = 1; + sandbox.metadata.as_mut().unwrap().deletion_time = + openshell_core::time::timestamp_from_millis(1).ok(); assert!(sandbox_proto_is_terminating(&sandbox)); } diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index a91a5fd877..91db86c275 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -144,7 +144,7 @@ where let ts = openshell_core::time::now_ms(); let log = SandboxLogLine { sandbox_id: sandbox_id.clone(), - timestamp_ms: ts, + event_time: openshell_core::time::timestamp_from_millis(ts).ok(), level, target: meta.target().to_string(), message: msg, @@ -199,7 +199,7 @@ mod tests { fn make_log_event(sandbox_id: &str, message: &str) -> SandboxLogLine { SandboxLogLine { sandbox_id: sandbox_id.to_string(), - timestamp_ms: 1000, + event_time: openshell_core::time::timestamp_from_millis(1000).ok(), level: "INFO".to_string(), target: "test".to_string(), message: message.to_string(), @@ -331,7 +331,7 @@ mod tests { for i in 0..5 { let evt = SandboxStreamEvent { payload: Some(sandbox_stream_event::Payload::Event(PlatformEvent { - timestamp_ms: i, + event_time: openshell_core::time::timestamp_from_millis(i).ok(), source: "test".to_string(), r#type: "Normal".to_string(), reason: format!("Event{i}"), diff --git a/crates/openshell-supervisor-middleware-builtins/src/regex.rs b/crates/openshell-supervisor-middleware-builtins/src/regex.rs index a5c2df882a..3b16524567 100644 --- a/crates/openshell-supervisor-middleware-builtins/src/regex.rs +++ b/crates/openshell-supervisor-middleware-builtins/src/regex.rs @@ -55,13 +55,13 @@ pub fn describe() -> Vec { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: MAX_PAYLOAD_BYTES, - timeout: String::new(), + request_timeout: None, }, MiddlewareBinding { operation: SupervisorMiddlewareOperation::WebsocketMessage as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: MAX_PAYLOAD_BYTES, - timeout: String::new(), + request_timeout: None, }, ] } diff --git a/crates/openshell-supervisor-middleware/src/lib.rs b/crates/openshell-supervisor-middleware/src/lib.rs index 902b5165c2..cc96ceb26a 100644 --- a/crates/openshell-supervisor-middleware/src/lib.rs +++ b/crates/openshell-supervisor-middleware/src/lib.rs @@ -634,10 +634,10 @@ struct MiddlewareServiceState { impl MiddlewareServiceState { fn timeout_for_binding(&self, binding: &MiddlewareBinding) -> Result { - if binding.timeout.trim().is_empty() { + if binding.request_timeout.is_none() { Ok(self.operator_timeout) } else { - parse_middleware_timeout(&binding.timeout) + middleware_proto_timeout_or_default(binding.request_timeout.as_ref()) .map(|binding_timeout| binding_timeout.min(self.operator_timeout)) .map_err(|reason| miette!("middleware binding has invalid timeout: {reason}")) } @@ -765,7 +765,7 @@ fn validate_registration(registration: &SupervisorMiddlewareService) -> Result advertised) { @@ -888,6 +888,24 @@ fn validate_manifest_bindings( Ok(()) } +fn middleware_proto_timeout_or_default( + value: Option<&prost_types::Duration>, +) -> std::result::Result { + let Some(value) = value else { + return Ok(DEFAULT_MIDDLEWARE_TIMEOUT); + }; + let timeout = + openshell_core::time::duration_to_std(value).map_err(|error| error.to_string())?; + if !(MIN_MIDDLEWARE_TIMEOUT..=MAX_MIDDLEWARE_TIMEOUT).contains(&timeout) { + return Err(format!( + "must be between {}ms and {}s", + MIN_MIDDLEWARE_TIMEOUT.as_millis(), + MAX_MIDDLEWARE_TIMEOUT.as_secs() + )); + } + Ok(timeout) +} + fn validate_external_manifest( registration: &SupervisorMiddlewareService, manifest: &MiddlewareManifest, @@ -1460,7 +1478,7 @@ impl ChainRunner { }); continue; }; - let Some(binding) = Self::binding(manifest, operation, phase).cloned() else { + let Some(binding) = Self::binding(manifest, operation, phase).copied() else { // The config remains globally ordered, but it does not // participate in this exact operation/phase chain. unbound.push(entry); @@ -1997,6 +2015,18 @@ mod tests { use tokio_stream::wrappers::TcpListenerStream; + fn proto_duration(value: &str) -> prost_types::Duration { + let duration = match (value.strip_suffix("ms"), value.strip_suffix('s')) { + (Some(milliseconds), _) => { + Duration::from_millis(milliseconds.parse().expect("integer milliseconds")) + } + (_, Some(seconds)) => Duration::from_secs(seconds.parse().expect("integer seconds")), + (None, None) => panic!("test duration must use ms or s"), + }; + openshell_core::time::duration_from_std(duration) + .expect("test duration is in protobuf range") + } + #[test] fn advertised_audience_mismatch_fails_registration() { let configured = "urn:openshell:extension:middleware:content-guard"; @@ -2112,7 +2142,7 @@ mod tests { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), } @@ -2248,7 +2278,7 @@ mod tests { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), } @@ -2339,7 +2369,7 @@ mod tests { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, - timeout: "10ms".into(), + request_timeout: Some(proto_duration("10ms")), }], expected_audience: String::new(), } @@ -2599,7 +2629,7 @@ mod tests { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: self.max_body_bytes, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), })) @@ -2628,7 +2658,7 @@ mod tests { struct SlowService { delay: Duration, - binding_timeout: String, + binding_timeout: Option, } #[tonic::async_trait] @@ -2654,7 +2684,7 @@ mod tests { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, - timeout: self.binding_timeout.clone(), + request_timeout: self.binding_timeout, }], expected_audience: String::new(), })) @@ -2713,7 +2743,7 @@ mod tests { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 256 * 1024, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), })) @@ -2983,7 +3013,7 @@ mod tests { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), })) @@ -3048,7 +3078,7 @@ mod tests { operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), })) @@ -3502,7 +3532,7 @@ mod tests { operation: HTTP_REQUEST_OPERATION as i32, phase: PRE_CREDENTIALS_PHASE as i32, max_payload_bytes: 4096, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), }; @@ -3529,7 +3559,7 @@ mod tests { operation: HTTP_REQUEST_OPERATION as i32, phase: PRE_CREDENTIALS_PHASE as i32, max_payload_bytes: u64::MAX, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), }; @@ -3545,7 +3575,7 @@ mod tests { operation: HTTP_REQUEST_OPERATION as i32, phase: PRE_CREDENTIALS_PHASE as i32, max_payload_bytes: 4096, - timeout: String::new(), + request_timeout: None, }; let manifest = MiddlewareManifest { name: "example/service".into(), @@ -3569,7 +3599,7 @@ mod tests { operation: SupervisorMiddlewareOperation::WebsocketMessage as i32, phase: phase as i32, max_payload_bytes: MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "500ms".into(), + request_timeout: Some(proto_duration("500ms")), }; let mut manifest = MiddlewareManifest { name: "example/websocket".into(), @@ -3596,7 +3626,7 @@ mod tests { operation: SupervisorMiddlewareOperation::WebsocketMessage as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), }; @@ -3620,7 +3650,7 @@ mod tests { operation: SupervisorMiddlewareOperation::WebsocketMessage as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), }; @@ -3669,7 +3699,7 @@ mod tests { assert_eq!(timeout, DEFAULT_MIDDLEWARE_TIMEOUT); let mut registration = external_registration(4096); - registration.timeout = "2s".into(); + registration.request_timeout = Some(proto_duration("2s")); let timeout = validate_registration(®istration).expect("operator timeout"); assert_eq!(timeout, Duration::from_secs(2)); } @@ -3678,7 +3708,7 @@ mod tests { fn registration_timeout_enforces_bounds() { for timeout in ["9ms", "31s"] { let mut registration = external_registration(4096); - registration.timeout = timeout.into(); + registration.request_timeout = Some(proto_duration(timeout)); assert!(validate_registration(®istration).is_err()); } } @@ -3694,7 +3724,7 @@ mod tests { operation: HTTP_REQUEST_OPERATION as i32, phase: PRE_CREDENTIALS_PHASE as i32, max_payload_bytes: 4096, - timeout: timeout.into(), + request_timeout: Some(proto_duration(timeout)), }], expected_audience: String::new(), }; @@ -3707,11 +3737,11 @@ mod tests { #[tokio::test] async fn binding_timeout_override_controls_evaluation_and_on_error() { let mut registration = external_registration(4096); - registration.timeout = "2s".into(); + registration.request_timeout = Some(proto_duration("2s")); let registry = registry_with_external( Arc::new(SlowService { delay: Duration::from_millis(50), - binding_timeout: "10ms".into(), + binding_timeout: Some(proto_duration("10ms")), }), registration, ) @@ -3749,11 +3779,11 @@ mod tests { #[tokio::test] async fn operator_timeout_controls_binding_without_manifest_override() { let mut registration = external_registration(4096); - registration.timeout = "10ms".into(); + registration.request_timeout = Some(proto_duration("10ms")); let registry = registry_with_external( Arc::new(SlowService { delay: Duration::from_millis(50), - binding_timeout: String::new(), + binding_timeout: None, }), registration, ) @@ -3784,11 +3814,14 @@ mod tests { #[tokio::test] async fn operator_timeout_caps_longer_binding_timeout_for_validation_and_evaluation() { let mut registration = external_registration(4096); - registration.timeout = "10ms".into(); + registration.request_timeout = Some(proto_duration("10ms")); let registry = registry_with_external( Arc::new(SlowService { delay: Duration::from_millis(50), - binding_timeout: "2s".into(), + binding_timeout: Some(prost_types::Duration { + seconds: 2, + nanos: 0, + }), }), registration, ) @@ -4719,7 +4752,10 @@ mod tests { operation: SupervisorMiddlewareOperation::WebsocketMessage as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "1s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 1, + nanos: 0, + }), }], expected_audience: String::new(), })) diff --git a/crates/openshell-supervisor-network/src/inference_routes.rs b/crates/openshell-supervisor-network/src/inference_routes.rs index 22b406b8dd..e42d0e6595 100644 --- a/crates/openshell-supervisor-network/src/inference_routes.rs +++ b/crates/openshell-supervisor-network/src/inference_routes.rs @@ -310,11 +310,11 @@ pub fn bundle_to_resolved_routes( .map(|r| { let (auth, default_headers, passthrough_headers) = openshell_core::inference::route_headers_for_provider_type(&r.provider_type); - let timeout = if r.timeout_secs == 0 { - openshell_router::config::DEFAULT_ROUTE_TIMEOUT - } else { - Duration::from_secs(r.timeout_secs) - }; + let timeout = r + .request_timeout + .as_ref() + .and_then(|value| openshell_core::time::duration_to_std(value).ok()) + .unwrap_or(openshell_router::config::DEFAULT_ROUTE_TIMEOUT); openshell_router::config::ResolvedRoute { name: r.name.clone(), endpoint: r.base_url.clone(), @@ -426,7 +426,7 @@ mod tests { "openai_responses".to_string(), ], provider_type: "openai".to_string(), - timeout_secs: 0, + request_timeout: None, model_in_path: false, request_path_override: None, }, @@ -437,13 +437,16 @@ mod tests { model_id: "llama-3".to_string(), protocols: vec!["openai_chat_completions".to_string()], provider_type: String::new(), - timeout_secs: 120, + request_timeout: Some(prost_types::Duration { + seconds: 120, + nanos: 0, + }), model_in_path: false, request_path_override: None, }, ], revision: "abc123".to_string(), - generated_at_ms: 1000, + generated_time: openshell_core::time::timestamp_from_millis(1000).ok(), }; let routes = bundle_to_resolved_routes(&bundle); @@ -482,7 +485,7 @@ mod tests { let bundle = openshell_core::proto::GetInferenceBundleResponse { routes: vec![], revision: "empty".to_string(), - generated_at_ms: 0, + generated_time: None, }; let routes = bundle_to_resolved_routes(&bundle); @@ -499,12 +502,12 @@ mod tests { model_id: "model".to_string(), protocols: vec!["openai_chat_completions".to_string()], provider_type: "openai".to_string(), - timeout_secs: 0, + request_timeout: None, model_in_path: false, request_path_override: None, }], revision: "rev".to_string(), - generated_at_ms: 0, + generated_time: None, }; let routes = bundle_to_resolved_routes(&bundle); diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index 2697fedb3c..f3f1e37cb5 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -3534,7 +3534,10 @@ network_policies: as i32, max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "2s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 2, + nanos: 0, + }), }], expected_audience: String::new(), }, @@ -3646,7 +3649,10 @@ network_policies: grpc_endpoint: format!("http://{address}"), max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "2s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 2, + nanos: 0, + }), tls_ca_cert_pem: Vec::new(), audience: String::new(), allow_insecure_transport: false, @@ -5344,7 +5350,7 @@ network_policies: as i32, phase: openshell_core::proto::SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 8192, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), } @@ -5491,7 +5497,7 @@ network_policies: as i32, phase: openshell_core::proto::SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 8192, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), } @@ -5962,7 +5968,7 @@ network_policies: operation: SupervisorMiddlewareOperation::HttpRequest as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: self.max_body_bytes, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), } diff --git a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs index fc440c6547..64138a2b9b 100644 --- a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs +++ b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs @@ -180,7 +180,10 @@ fn token_grant_request<'a>( client_assertion_type: &token_grant.client_assertion_type, audience: &token_grant.audience, scopes: &token_grant.scopes, - cache_ttl_seconds: token_grant.cache_ttl_seconds, + cache_ttl_seconds: token_grant + .cache_ttl + .as_ref() + .map_or(0, |value| value.seconds), grant_type: token_grant.grant_type, requested_token_type: &token_grant.requested_token_type, } @@ -518,7 +521,10 @@ pub mod test_support { client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" .to_string(), scopes: vec!["read".to_string()], - cache_ttl_seconds: 300, + cache_ttl: Some(prost_types::Duration { + seconds: 300, + nanos: 0, + }), audience_overrides: Vec::new(), grant_type: ProviderCredentialTokenGrantType::ClientCredentials as i32, subject_token: None, diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 6cd8aa4818..7ad8ab88b2 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -3523,7 +3523,10 @@ network_policies: phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "1s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 1, + nanos: 0, + }), }], expected_audience: String::new(), })) @@ -3717,7 +3720,10 @@ network_policies: grpc_endpoint: format!("http://{address}"), max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "2s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 2, + nanos: 0, + }), tls_ca_cert_pem: Vec::new(), audience: String::new(), allow_insecure_transport: false, @@ -3789,7 +3795,10 @@ network_policies: grpc_endpoint: format!("http://{address}"), max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "2s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 2, + nanos: 0, + }), tls_ca_cert_pem: Vec::new(), audience: String::new(), allow_insecure_transport: false, @@ -4680,7 +4689,10 @@ network_policies: grpc_endpoint: format!("http://{address}"), max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "2s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 2, + nanos: 0, + }), tls_ca_cert_pem: Vec::new(), audience: String::new(), allow_insecure_transport: false, @@ -4830,7 +4842,10 @@ network_policies: grpc_endpoint: format!("http://{address}"), max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, - timeout: "2s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 2, + nanos: 0, + }), tls_ca_cert_pem: Vec::new(), audience: String::new(), allow_insecure_transport: false, diff --git a/crates/openshell-supervisor-network/src/policy_local.rs b/crates/openshell-supervisor-network/src/policy_local.rs index 908587ed31..8556f05887 100644 --- a/crates/openshell-supervisor-network/src/policy_local.rs +++ b/crates/openshell-supervisor-network/src/policy_local.rs @@ -1051,13 +1051,13 @@ fn policy_chunk_from_add_rule( security_notes: String::new(), confidence: 0.75, denial_summary_ids: vec![], - created_at_ms: 0, - decided_at_ms: 0, + created_time: None, + decided_time: None, stage: "agent".to_string(), supersedes_chunk_id: String::new(), hit_count: 1, - first_seen_ms: 0, - last_seen_ms: 0, + first_seen_time: None, + last_seen_time: None, binary, validation_result: String::new(), rejection_reason: String::new(), diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index dc2736a4ea..2bdb6f27f4 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -6377,7 +6377,10 @@ mod tests { phase: openshell_core::proto::SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 1024, - timeout: "1s".into(), + request_timeout: Some(prost_types::Duration { + seconds: 1, + nanos: 0, + }), }], expected_audience: String::new(), }, @@ -6461,7 +6464,7 @@ mod tests { as i32, phase: openshell_core::proto::SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 8192, - timeout: String::new(), + request_timeout: None, }], expected_audience: String::new(), } diff --git a/crates/openshell-supervisor-process/src/debug_rpc.rs b/crates/openshell-supervisor-process/src/debug_rpc.rs index 6f885a69db..83e85731fc 100644 --- a/crates/openshell-supervisor-process/src/debug_rpc.rs +++ b/crates/openshell-supervisor-process/src/debug_rpc.rs @@ -112,7 +112,11 @@ async fn run_refresh() -> Result { match resp { Ok(r) => { let inner = r.into_inner(); - print_token_summary(&inner.token, Some(inner.expires_at_ms)); + let expires_at_ms = inner + .expiration_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()); + print_token_summary(&inner.token, expires_at_ms); Ok(0) } Err(status) => { diff --git a/crates/openshell-supervisor-process/src/log_push.rs b/crates/openshell-supervisor-process/src/log_push.rs index 01e373ba8e..4c5b5a3528 100644 --- a/crates/openshell-supervisor-process/src/log_push.rs +++ b/crates/openshell-supervisor-process/src/log_push.rs @@ -72,7 +72,7 @@ impl Layer for LogPushLayer { let log = SandboxLogLine { sandbox_id: self.sandbox_id.clone(), - timestamp_ms: ts, + event_time: openshell_core::time::timestamp_from_millis(ts).ok(), level: if is_ocsf { "OCSF".to_string() } else { diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index 98a3c0497b..16fe13249b 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -384,17 +384,20 @@ async fn run_single_session( _ => return Err("expected SessionAccepted or SessionRejected".into()), }; - let heartbeat_secs = accepted.heartbeat_interval_secs.max(5); + let heartbeat_secs = accepted + .heartbeat_interval + .as_ref() + .and_then(|value| openshell_core::time::duration_to_std(value).ok()) + .map_or(5, |value| value.as_secs().max(5)); let event = session_established_event( openshell_ocsf::ctx::ctx(), &config.endpoint, &accepted.session_id, - heartbeat_secs, + u32::try_from(heartbeat_secs).unwrap_or(u32::MAX), ); ocsf_emit!(event); // Main loop: receive gateway messages + send heartbeats. - let mut heartbeat_interval = - tokio::time::interval(Duration::from_secs(u64::from(heartbeat_secs))); + let mut heartbeat_interval = tokio::time::interval(Duration::from_secs(heartbeat_secs)); heartbeat_interval.tick().await; // skip immediate tick loop { diff --git a/crates/openshell-tui/src/app.rs b/crates/openshell-tui/src/app.rs index a5e14bcbab..41a44310b6 100644 --- a/crates/openshell-tui/src/app.rs +++ b/crates/openshell-tui/src/app.rs @@ -881,9 +881,12 @@ fn provider_to_redacted_yaml(provider: &openshell_core::proto::Provider) -> Stri } } - if !provider.credential_expires_at_ms.is_empty() { - out.push_str("credential_expires_at_ms:\n"); - let mut entries = provider.credential_expires_at_ms.iter().collect::>(); + if !provider.credential_expiration_times.is_empty() { + out.push_str("credential_expiration_times:\n"); + let mut entries = provider + .credential_expiration_times + .iter() + .collect::>(); entries.sort_by_key(|(key, _)| *key); for (key, value) in entries { out.push_str(" "); @@ -3189,10 +3192,9 @@ impl App { .get(key) .map_or_else(|| "-".to_string(), |value| mask_secret(value)); let expiry = provider - .credential_expires_at_ms + .credential_expiration_times .get(key) - .copied() - .filter(|value| *value > 0) + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) .map_or_else(String::new, |value| format!(" expires={value}")); format!("{key}: {masked}{expiry}") }) @@ -3219,9 +3221,8 @@ impl App { credential.env_vars.join(", ") }; let expiry = present_key - .and_then(|key| provider.credential_expires_at_ms.get(key)) - .copied() - .filter(|value| *value > 0) + .and_then(|key| provider.credential_expiration_times.get(key)) + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) .map_or_else(String::new, |value| format!(" expires={value}")); format!( "{} ({required}) env=[{env_vars}] {status}{expiry}", diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 625bc8a41a..b786d17935 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -648,7 +648,7 @@ fn spawn_log_stream(app: &mut App, tx: mpsc::UnboundedSender) { let req = openshell_core::proto::GetSandboxLogsRequest { sandbox_id: sandbox_id.clone(), lines: 500, - since_ms: 0, + since_time: None, sources: vec![], min_level: String::new(), workspace, @@ -725,7 +725,11 @@ fn proto_to_log_line(log: openshell_core::proto::SandboxLogLine) -> LogLine { log.source }; LogLine { - timestamp_ms: log.timestamp_ms, + timestamp_ms: log + .event_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or_default(), level: log.level, source, target: log.target, @@ -1676,17 +1680,17 @@ fn spawn_create_provider(app: &App, tx: mpsc::UnboundedSender) { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: provider_name.clone(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.clone(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: ptype.clone(), credentials: credentials.clone(), config: config.clone(), - credential_expires_at_ms: HashMap::default(), + credential_expiration_times: HashMap::default(), profile_workspace: workspace.clone(), credential_handles: HashMap::default(), }), @@ -1790,21 +1794,21 @@ fn spawn_update_provider(app: &App, tx: mpsc::UnboundedSender) { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { id: String::new(), name: name.clone(), - created_at_ms: 0, + created_time: None, labels: HashMap::new(), resource_version: 0, annotations: HashMap::new(), workspace: workspace.clone(), - deletion_timestamp_ms: 0, + deletion_time: None, }), r#type: ptype, credentials, config, - credential_expires_at_ms: HashMap::default(), + credential_expiration_times: HashMap::default(), profile_workspace: String::new(), credential_handles: HashMap::default(), }), - credential_expires_at_ms: HashMap::default(), + credential_expiration_times: HashMap::default(), workspace, }; @@ -2545,7 +2549,9 @@ async fn refresh_sandboxes(app: &mut App) { .map(|s| { s.metadata .as_ref() - .map_or_else(|| "?".to_string(), |m| format_age(m.created_at_ms)) + .and_then(|m| m.created_time.as_ref()) + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .map_or_else(|| "?".to_string(), format_age) }) .collect(); app.sandbox_created = sandboxes @@ -2553,7 +2559,9 @@ async fn refresh_sandboxes(app: &mut App) { .map(|s| { s.metadata .as_ref() - .map_or_else(|| "?".to_string(), |m| format_timestamp(m.created_at_ms)) + .and_then(|m| m.created_time.as_ref()) + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .map_or_else(|| "?".to_string(), format_timestamp) }) .collect(); diff --git a/crates/openshell-tui/src/ui/sandbox_draft.rs b/crates/openshell-tui/src/ui/sandbox_draft.rs index 470463b7d4..dd2360b4d8 100644 --- a/crates/openshell-tui/src/ui/sandbox_draft.rs +++ b/crates/openshell-tui/src/ui/sandbox_draft.rs @@ -282,8 +282,20 @@ pub fn draw_detail_popup( ); let denied_seen = format!( "(first {} / last {})", - format_short_time(chunk.first_seen_ms), - format_short_time(chunk.last_seen_ms), + format_short_time( + chunk + .first_seen_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or_default(), + ), + format_short_time( + chunk + .last_seen_time + .as_ref() + .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) + .unwrap_or_default(), + ), ); let denied_width = display_width(denied_label) + display_width(&denied_count) @@ -1301,8 +1313,8 @@ mod tests { rule_name: "allow-github".to_string(), confidence: 0.82, hit_count: 3, - first_seen_ms: 1_700_000_000_000, - last_seen_ms: 1_700_000_100_000, + first_seen_time: openshell_core::time::timestamp_from_millis(1_700_000_000_000).ok(), + last_seen_time: openshell_core::time::timestamp_from_millis(1_700_000_100_000).ok(), ..Default::default() } } diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 22b64e9e97..c89d4bae17 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -209,7 +209,7 @@ Local Docker, Podman, and VM gateways can also set `[openshell.gateway.mtls_auth `[openshell.gateway] policy_validation_failure_mode` controls what sandbox supervisors do when a complete candidate policy fails runtime validation. The default, `fail_closed`, deactivates the previous network policy, closes relays pinned to it, and denies new egress until a valid generation loads. `retain_last_valid` leaves the previous valid generation active. Both modes reject the candidate atomically; startup always fails closed when no previous valid generation exists. Gateway mutation paths that can preflight a known effective scope reject invalid candidates before persistence and leave the active policy unchanged regardless of this setting. Changing the value requires restarting the gateway so it can reload `gateway.toml` and distribute the new posture to sandbox supervisors. -`[openshell.gateway.gateway_jwt] ttl_secs` controls gateway-minted sandbox JWT lifetime. When omitted, it defaults to `0`: the token `exp` claim and `expires_at_ms` response field become `0`, and the sandbox JWT does not expire. Use that default only for local single-player Docker, Podman, or VM gateways. Kubernetes and other shared deployments should set a positive TTL; Helm renders `3600` seconds by default, and the gateway logs a warning when a Kubernetes gateway uses `0`. +`[openshell.gateway.gateway_jwt] ttl_secs` controls gateway-minted sandbox JWT lifetime. When omitted, it defaults to `0`: the token has no `exp` claim, the response omits `expiration_time`, and the sandbox JWT does not expire. Use that default only for local single-player Docker, Podman, or VM gateways. Kubernetes and other shared deployments should set a positive TTL; Helm renders `3600` seconds by default, and the gateway logs a warning when a Kubernetes gateway uses `0`. `[openshell.gateway.auth] allow_unauthenticated_users = true` is an unsafe local-development and trusted-proxy escape hatch. It accepts user-facing CLI/API calls without OIDC or mTLS credentials while sandbox supervisors still authenticate with gateway-minted sandbox JWTs. Leave it false for shared and production gateways. diff --git a/docs/reference/protobuf-time-types.mdx b/docs/reference/protobuf-time-types.mdx new file mode 100644 index 0000000000..585fc2afc4 --- /dev/null +++ b/docs/reference/protobuf-time-types.mdx @@ -0,0 +1,52 @@ +--- +title: Protobuf time types +description: Timestamp and duration representation in the OpenShell API +--- + +OpenShell represents absolute times with `google.protobuf.Timestamp` and +elapsed times with `google.protobuf.Duration` in its protobuf APIs. + +In protobuf JSON, timestamps are RFC 3339 strings and durations are strings +ending in `s`: + +```json +{ + "createdTime": "2026-08-31T14:05:06.123456789Z", + "executionTimeout": "1.500s" +} +``` + +An absent message means that no timestamp or duration was supplied. It is +different from the Unix epoch (`1970-01-01T00:00:00Z`) and from a zero duration +(`0s`). OpenShell validates protobuf timestamp and duration bounds at API +boundaries. + +## Upgrade from scalar time fields + +The well-known time fields replace earlier public fields encoded as Unix +milliseconds, integer seconds, or duration strings. Their names and protobuf +tags changed, so clients generated from the old schema are not wire-compatible +with a gateway generated from the new schema. Upgrade the gateway, CLI, and all +SDK clients together. + +Before upgrading a gateway with persisted state: + +1. Stop all gateway replicas. +2. Back up the SQLite database or PostgreSQL database. +3. Upgrade every gateway replica and client from the same OpenShell release. +4. Start one gateway replica and wait for startup to complete before starting + the remaining replicas. + +At startup, the gateway migrates affected protobuf payloads in one database +transaction. Legacy zero timestamps become absent. Zero expiry-map values are +removed, and the legacy maximum-integer refresh sentinel becomes an absent +next-refresh timestamp. The database columns `created_at_ms` and +`updated_at_ms` remain unchanged because they are internal ordering metadata. + +If a legacy payload is malformed or outside protobuf time bounds, startup +fails with the affected object type and ID and rolls back the transaction. +Restore the backup or repair the reported record with the previous OpenShell +version before retrying the upgrade. + +User-facing timeout options and gateway TOML settings keep their existing +units. The CLI and SDKs convert those values at the protobuf boundary. diff --git a/examples/governance-interceptor/Cargo.lock b/examples/governance-interceptor/Cargo.lock index 1f23482a7b..97a136c44c 100644 --- a/examples/governance-interceptor/Cargo.lock +++ b/examples/governance-interceptor/Cargo.lock @@ -1063,6 +1063,7 @@ dependencies = [ "glob", "openshell-core", "openshell-policy", + "prost-types", "serde", "serde_json", "serde_yml", diff --git a/proto/compute_driver.proto b/proto/compute_driver.proto index eff5cfa5ea..6253ed9e16 100644 --- a/proto/compute_driver.proto +++ b/proto/compute_driver.proto @@ -6,6 +6,7 @@ syntax = "proto3"; package openshell.compute.v1; import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; import "options.proto"; // Gateway/compute-driver extension contract. @@ -257,6 +258,8 @@ message DriverSandboxStatus { // Raw compute-platform condition. message DriverCondition { + reserved 5; + reserved "last_transition_time"; // Condition class reported by the compute platform. string type = 1; // Condition status value such as `True`, `False`, or `Unknown`. @@ -265,14 +268,16 @@ message DriverCondition { string reason = 3; // Human-readable condition message. string message = 4; - // Timestamp reported by the platform for the last transition. - string last_transition_time = 5; + // Time reported by the platform for the last transition. + google.protobuf.Timestamp transition_time = 105; } // Raw compute-platform event correlated to a sandbox. message DriverPlatformEvent { - // Event timestamp in milliseconds since epoch. - int64 timestamp_ms = 1; + reserved 1; + reserved "timestamp_ms"; + // Time when the event occurred. + google.protobuf.Timestamp event_time = 101; // Event source (for example `kubernetes`). string source = 2; // Event type or severity (for example `Normal` or `Warning`). diff --git a/proto/credential_driver.proto b/proto/credential_driver.proto index b25e9256d0..fd7bb1b92c 100644 --- a/proto/credential_driver.proto +++ b/proto/credential_driver.proto @@ -6,6 +6,7 @@ syntax = "proto3"; package openshell.credentials.v1; import "datamodel.proto"; +import "google/protobuf/timestamp.proto"; // Internal credential-driver contract used by the gateway. // @@ -43,7 +44,7 @@ message GetCredentialDriverCapabilitiesResponse { string backend_kind = 3; // True when ListCredentials is supported. bool supports_list = 4; - // True when ResolveCredentials may return expires_at_ms values. + // True when ResolveCredentials may return expiration_time values. bool supports_expires_at = 5; } @@ -111,12 +112,14 @@ message ResolveCredentialsResponse { } message ResolvedCredential { + reserved 3; + reserved "expires_at_ms"; // Echoes ResolveCredentialRequest.request_id. string request_id = 1; // Secret string value. Drivers must never log this field. string value = 2; - // Expiration timestamp in milliseconds since Unix epoch, or zero when absent. - int64 expires_at_ms = 3; + // Expiration time. Absence means the credential does not expire. + google.protobuf.Timestamp expiration_time = 103; } message ListCredentialsRequest {} diff --git a/proto/datamodel.proto b/proto/datamodel.proto index b990f05768..da02c600db 100644 --- a/proto/datamodel.proto +++ b/proto/datamodel.proto @@ -6,6 +6,7 @@ syntax = "proto3"; package openshell.datamodel.v1; import "options.proto"; +import "google/protobuf/timestamp.proto"; // Kubernetes-style metadata shared by all top-level OpenShell domain objects. // @@ -13,14 +14,16 @@ import "options.proto"; // timestamps, resource versioning) across Sandbox, Provider, SshSession, and // other resources. message ObjectMeta { + reserved 3, 8; + reserved "created_at_ms", "deletion_timestamp_ms"; // Stable object ID generated by the gateway. string id = 1; // Human-readable object name (unique per object type). string name = 2; - // Milliseconds since Unix epoch when the object was created. - int64 created_at_ms = 3; + // Time when the object was created. + google.protobuf.Timestamp created_time = 103; // Key-value labels for filtering and organization. // Labels must follow Kubernetes conventions: alphanumeric + `-._/`, max 63 chars per segment. @@ -38,10 +41,10 @@ message ObjectMeta { // gateway. Immutable after creation. string workspace = 7; - // Milliseconds since Unix epoch when graceful deletion was initiated. - // Zero means the object is not being deleted. Once set, this field is + // Time when graceful deletion was initiated. Absence means the object is + // not being deleted. Once set, this field is // immutable — the only path forward is completing deletion. - int64 deletion_timestamp_ms = 8; + google.protobuf.Timestamp deletion_time = 108; } // Phase of a workspace's lifecycle. @@ -81,6 +84,8 @@ message CredentialHandle { // Provider model stored by OpenShell. message Provider { + reserved 5; + reserved "credential_expires_at_ms"; // Kubernetes-style metadata (id, name, labels, timestamps, resource version). ObjectMeta metadata = 1; // Canonical provider type slug (for example: "claude", "gitlab"). @@ -89,9 +94,9 @@ message Provider { map credentials = 3 [(openshell.options.v1.secret) = true]; // Non-secret provider configuration. map config = 4; - // Expiration timestamps for credential values, keyed by credential/env var - // name. A zero or missing value means the credential does not expire. - map credential_expires_at_ms = 5; + // Expiration times for credential values, keyed by credential/env var name. + // A missing key means the credential does not expire. + map credential_expiration_times = 105; // Workspace where this provider's type profile is stored. // Empty string = platform/global scope. Must be empty or match // metadata.workspace; cross-workspace references are rejected. diff --git a/proto/inference.proto b/proto/inference.proto index a28d7149e5..8a6a5685ff 100644 --- a/proto/inference.proto +++ b/proto/inference.proto @@ -6,6 +6,8 @@ syntax = "proto3"; package openshell.inference.v1; import "datamodel.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; import "options.proto"; // Inference service provides workspace-scoped inference route configuration and bundle delivery. @@ -57,12 +59,14 @@ service Inference { // Only `provider_name` and `model_id` are stored; endpoint, protocols, // credentials, and auth style are resolved from the provider at bundle time. message InferenceRouteConfig { + reserved 3; + reserved "timeout_secs"; // Provider record name backing this route. string provider_name = 1; // Model identifier to force on generation calls. string model_id = 2; - // Per-route request timeout in seconds. 0 means use default (60s). - uint64 timeout_secs = 3; + // Per-route request timeout. Absence means use the default (60s). + google.protobuf.Duration request_timeout = 103; } // Storage envelope for a workspace-scoped inference route. @@ -74,6 +78,8 @@ message InferenceRoute { } message SetInferenceRouteRequest { + reserved 6; + reserved "timeout_secs"; // Provider record name to use for credentials + endpoint mapping. string provider_name = 1; // Model identifier to force on generation calls. @@ -85,8 +91,8 @@ message SetInferenceRouteRequest { bool verify = 4; // Skip synchronous endpoint validation before persistence. bool no_verify = 5; - // Per-route request timeout in seconds. 0 means use default (60s). - uint64 timeout_secs = 6; + // Per-route request timeout. Absence means use the default (60s). + google.protobuf.Duration request_timeout = 106; // Target workspace. Empty string defaults to "default". string workspace = 7; } @@ -97,6 +103,8 @@ message ValidatedEndpoint { } message SetInferenceRouteResponse { + reserved 7; + reserved "timeout_secs"; string provider_name = 1; string model_id = 2; uint64 version = 3; @@ -106,8 +114,8 @@ message SetInferenceRouteResponse { bool validation_performed = 5; // The concrete endpoints that were probed during validation, when available. repeated ValidatedEndpoint validated_endpoints = 6; - // Per-route request timeout in seconds that was persisted. - uint64 timeout_secs = 7; + // Per-route request timeout that was persisted. + google.protobuf.Duration request_timeout = 107; // Workspace the route was configured in. string workspace = 8; } @@ -121,13 +129,15 @@ message GetInferenceRouteRequest { } message GetInferenceRouteResponse { + reserved 5; + reserved "timeout_secs"; string provider_name = 1; string model_id = 2; uint64 version = 3; // Route name that was queried. string route_name = 4; - // Per-route request timeout in seconds. 0 means default (60s). - uint64 timeout_secs = 5; + // Per-route request timeout. Absence means the default (60s). + google.protobuf.Duration request_timeout = 105; // Workspace the route belongs to. string workspace = 6; } @@ -149,14 +159,16 @@ message GetInferenceBundleRequest {} // A single resolved route ready for sandbox-local execution. message ResolvedRoute { + reserved 7; + reserved "timeout_secs"; string name = 1; string base_url = 2; repeated string protocols = 3; string api_key = 4 [(openshell.options.v1.secret) = true]; string model_id = 5; string provider_type = 6; - // Per-route request timeout in seconds. 0 means use default (60s). - uint64 timeout_secs = 7; + // Per-route request timeout. Absence means use the default (60s). + google.protobuf.Duration request_timeout = 107; // When true, the model identifier is embedded in the URL path (e.g. Vertex AI). bool model_in_path = 8; // Optional override for the request path. When set, replaces the protocol-derived path. @@ -165,9 +177,11 @@ message ResolvedRoute { } message GetInferenceBundleResponse { + reserved 3; + reserved "generated_at_ms"; repeated ResolvedRoute routes = 1; // Opaque revision tag for cache freshness checks. string revision = 2; - // Timestamp (epoch ms) when this bundle was generated. - int64 generated_at_ms = 3; + // Time when this bundle was generated. + google.protobuf.Timestamp generated_time = 103; } diff --git a/proto/openshell.proto b/proto/openshell.proto index 42e635927f..62e021b58f 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -6,7 +6,9 @@ syntax = "proto3"; package openshell.v1; import "datamodel.proto"; +import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; import "options.proto"; import "sandbox.proto"; @@ -699,11 +701,12 @@ message IssueSandboxTokenRequest {} // memory and presents it as `Authorization: Bearer` on every subsequent // gateway RPC. message IssueSandboxTokenResponse { + reserved 2; + reserved "expires_at_ms"; // Gateway-minted JWT bound to the calling sandbox's UUID. string token = 1 [(openshell.options.v1.secret) = true]; - // Absolute expiry of the issued token, milliseconds since the epoch. 0 means - // the token is non-expiring. - int64 expires_at_ms = 2; + // Absolute expiry of the issued token. Absence means the token is non-expiring. + google.protobuf.Timestamp expiration_time = 102; } // RefreshSandboxToken request. The calling principal must already be a @@ -720,11 +723,12 @@ message RefreshSandboxTokenRequest { // RefreshSandboxToken response. The new token replaces the supervisor's // in-memory bearer credential. message RefreshSandboxTokenResponse { + reserved 2; + reserved "expires_at_ms"; // Fresh gateway-minted JWT bound to the same sandbox UUID. string token = 1 [(openshell.options.v1.secret) = true]; - // Absolute expiry of the new token, milliseconds since the epoch. 0 means - // the token is non-expiring. - int64 expires_at_ms = 2; + // Absolute expiry of the new token. Absence means the token is non-expiring. + google.protobuf.Timestamp expiration_time = 102; // Fresh credentials for the requested, policy-authorized extension // services. These remain in supervisor memory and are never persisted. repeated ExtensionServiceCredential extension_credentials = 3; @@ -922,6 +926,8 @@ message SandboxStatus { // User-facing sandbox condition derived from driver-native conditions. message SandboxCondition { + reserved 5; + reserved "last_transition_time"; // Condition class, typically mirroring the underlying platform condition type. string type = 1; // Condition status value such as `True`, `False`, or `Unknown`. @@ -931,7 +937,7 @@ message SandboxCondition { // Human-readable condition message. string message = 4; // Timestamp reported by the underlying platform for the last transition. - string last_transition_time = 5; + google.protobuf.Timestamp transition_time = 105; } // High-level sandbox lifecycle phase derived by the gateway. @@ -954,8 +960,10 @@ enum SandboxPhase { // Public platform event exposed on the sandbox watch stream. message PlatformEvent { - // Event timestamp in milliseconds since epoch. - int64 timestamp_ms = 1; + reserved 1; + reserved "timestamp_ms"; + // Time when the event occurred. + google.protobuf.Timestamp event_time = 101; // Event source (e.g. "kubernetes", "docker", "process"). string source = 2; // Event type/severity (e.g. "Normal", "Warning"). @@ -1115,6 +1123,8 @@ message CreateSshSessionRequest { // violate it. The client's own escaping provides defense-in-depth, but // narrow charsets close injection vectors at the trust boundary. message CreateSshSessionResponse { + reserved 8; + reserved "expires_at_ms"; // Sandbox id. [A-Za-z0-9._-]{1,128}. string sandbox_id = 1; @@ -1137,8 +1147,8 @@ message CreateSshSessionResponse { // Optional host key fingerprint. If non-empty, [A-Za-z0-9:+/=-] only. string host_key_fingerprint = 7; - // Expiry timestamp in milliseconds since epoch. 0 means no expiry. - int64 expires_at_ms = 8; + // Absolute expiry. Absence means no expiry. + google.protobuf.Timestamp expiration_time = 108; } // Request to expose an HTTP service running inside a sandbox. @@ -1236,6 +1246,8 @@ message RevokeSshSessionResponse { // Execute command request. message ExecSandboxRequest { + reserved 5; + reserved "timeout_seconds"; // Sandbox id. string sandbox_id = 1; @@ -1248,8 +1260,8 @@ message ExecSandboxRequest { // Optional environment overrides. map environment = 4; - // Optional timeout in seconds. 0 means no timeout. - uint32 timeout_seconds = 5; + // Optional execution timeout. Absence means no timeout. + google.protobuf.Duration execution_timeout = 105; // Optional stdin payload passed to the command. bytes stdin = 6; @@ -1340,6 +1352,8 @@ message ExecSandboxWindowResize { // SSH session record stored in persistence. message SshSession { + reserved 4; + reserved "expires_at_ms"; // Kubernetes-style metadata (id, name, labels, timestamps, resource version). openshell.datamodel.v1.ObjectMeta metadata = 1; @@ -1349,9 +1363,8 @@ message SshSession { // Session token. string token = 3 [(openshell.options.v1.secret) = true]; - // Expiry timestamp in milliseconds since epoch. 0 means no expiry - // (backward-compatible default for sessions created before this field existed). - int64 expires_at_ms = 4; + // Absolute expiry. Absence means no expiry. + google.protobuf.Timestamp expiration_time = 104; // Revoked flag. bool revoked = 5; @@ -1359,6 +1372,8 @@ message SshSession { // Watch sandbox request. message WatchSandboxRequest { + reserved 8; + reserved "log_since_ms"; // Sandbox id. string id = 1; @@ -1381,9 +1396,9 @@ message WatchSandboxRequest { // (COMPLETED, STOPPED, or ERROR). bool stop_on_terminal = 7; - // Only include log lines with timestamp >= this value (milliseconds since epoch). - // 0 means no time filter. Applies to both tail replay and live streaming. - int64 log_since_ms = 8; + // Only include log lines at or after this time. Absence means no time filter. + // Applies to both tail replay and live streaming. + google.protobuf.Timestamp since_time = 108; // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. repeated string log_sources = 9; @@ -1410,8 +1425,10 @@ message SandboxStreamEvent { // Log line correlated to a sandbox. message SandboxLogLine { + reserved 2; + reserved "timestamp_ms"; string sandbox_id = 1; - int64 timestamp_ms = 2; + google.protobuf.Timestamp event_time = 102; string level = 3; string target = 4; string message = 5; @@ -1452,10 +1469,12 @@ message ListProvidersRequest { // Update provider request. message UpdateProviderRequest { + reserved 2; + reserved "credential_expires_at_ms"; openshell.datamodel.v1.Provider provider = 1; // Optional per-credential expiry timestamps to merge into the provider. - // A zero value removes the expiry for that credential. - map credential_expires_at_ms = 2; + // An absent map entry removes the expiry for that credential. + map credential_expiration_times = 102; // Workspace scope. Empty defaults to "default". string workspace = 3; } @@ -1550,6 +1569,8 @@ message ProviderCredentialTokenGrantSubjectToken { } message ProviderCredentialTokenGrant { + reserved 4; + reserved "cache_ttl_seconds"; // OAuth2 token endpoint URL (e.g., https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token) string token_endpoint = 1; @@ -1563,9 +1584,8 @@ message ProviderCredentialTokenGrant { // Optional: OAuth2 scopes to request repeated string scopes = 3; - // Optional: override token cache TTL (seconds) - // If 0 or omitted, use expires_in from token response - int64 cache_ttl_seconds = 4; + // Optional token cache TTL override. If absent, use expires_in from the token response. + google.protobuf.Duration cache_ttl = 104; // Optional: endpoint-specific resource audience overrides. repeated ProviderCredentialTokenGrantAudienceOverride audience_overrides = 5; @@ -1627,28 +1647,30 @@ message ProviderCredentialRefreshOutput { } message ProviderCredentialRefresh { + reserved 4, 5; + reserved "refresh_before_seconds", "max_lifetime_seconds"; ProviderCredentialRefreshStrategy strategy = 1; string token_url = 2; repeated string scopes = 3; - int64 refresh_before_seconds = 4; - int64 max_lifetime_seconds = 5; + google.protobuf.Duration refresh_before = 104; + google.protobuf.Duration max_lifetime = 105; repeated ProviderCredentialRefreshMaterial material = 6; repeated ProviderCredentialRefreshOutput additional_outputs = 7; } message ProviderCredentialRefreshStatus { + reserved 6, 7, 8, 13; + reserved "expires_at_ms", "next_refresh_at_ms", "last_refresh_at_ms", "last_error_at_ms"; string provider_name = 1; string provider_id = 2; string credential_key = 3; ProviderCredentialRefreshStrategy strategy = 4; string status = 5; - int64 expires_at_ms = 6; - // Next automatic refresh time in Unix epoch milliseconds. A value of - // 9223372036854775807 (int64 max) means no automatic retry is scheduled; - // consumers should render it as unset and use recovery_action to determine - // the required recovery workflow. - int64 next_refresh_at_ms = 7; - int64 last_refresh_at_ms = 8; + google.protobuf.Timestamp expiration_time = 106; + // Next automatic refresh time. Absence means no automatic retry is scheduled; + // use recovery_action to determine the required recovery workflow. + google.protobuf.Timestamp next_refresh_time = 107; + google.protobuf.Timestamp last_refresh_time = 108; string last_error = 9; ProviderCredentialRefreshRecoveryAction recovery_action = 10; // Stable gateway-owned failure identifier, for example @@ -1659,7 +1681,7 @@ message ProviderCredentialRefreshStatus { // do not need a separate provider_error field. Unknown provider-controlled // values are not persisted or returned. string provider_error_subtype = 12; - int64 last_error_at_ms = 13; + google.protobuf.Timestamp last_error_time = 113; } // Provider profile local discovery declaration. @@ -1736,6 +1758,8 @@ message GetProviderRefreshStatusResponse { } message ConfigureProviderRefreshRequest { + reserved 6; + reserved "expires_at_ms"; string provider = 1; string credential_key = 2; ProviderCredentialRefreshStrategy strategy = 3; @@ -1744,7 +1768,7 @@ message ConfigureProviderRefreshRequest { // name must be present in material. The server also classifies secrets from // the authoritative provider profile and refresh strategy. repeated string secret_material_keys = 5; - optional int64 expires_at_ms = 6; + google.protobuf.Timestamp expiration_time = 106; // Workspace scope. Empty defaults to "default". string workspace = 7; } @@ -1931,12 +1955,14 @@ message StaticCredentialBinding { // Get sandbox provider environment response. message GetSandboxProviderEnvironmentResponse { + reserved 3; + reserved "credential_expires_at_ms"; // Provider credential environment variables. map environment = 1 [(openshell.options.v1.secret) = true]; // Fingerprint for the provider credential inputs that produced environment. uint64 provider_env_revision = 2; // Expiration timestamps for returned environment variables. - map credential_expires_at_ms = 3; + map credential_expiration_times = 103; // Dynamic credentials that require token grants or other runtime injection. // Maps endpoint-bound provider metadata to credential metadata. // Supervisor uses this to inject Authorization headers for token grant credentials. @@ -1967,8 +1993,10 @@ message ExchangeProviderSubjectTokenRequest { } message ExchangeProviderSubjectTokenResponse { + reserved 2; + reserved "expires_in"; string access_token = 1 [(openshell.options.v1.secret) = true]; - int64 expires_in = 2; + google.protobuf.Duration expires_after = 102; string token_type = 3; } @@ -2129,6 +2157,8 @@ message ReportPolicyStatusResponse {} // A versioned policy revision with metadata. message SandboxPolicyRevision { + reserved 5, 6; + reserved "created_at_ms", "loaded_at_ms"; // Policy version (monotonically increasing per sandbox). uint32 version = 1; // SHA-256 hash of the serialized policy payload. @@ -2137,10 +2167,10 @@ message SandboxPolicyRevision { PolicyStatus status = 3; // Error message if status is FAILED. string load_error = 4; - // Milliseconds since epoch when this revision was created. - int64 created_at_ms = 5; - // Milliseconds since epoch when this revision was loaded by the sandbox. - int64 loaded_at_ms = 6; + // Time when this revision was created. + google.protobuf.Timestamp created_time = 105; + // Time when this revision was loaded by the sandbox. Absent if not loaded. + google.protobuf.Timestamp loaded_time = 106; // The full policy (only populated when explicitly requested). openshell.sandbox.v1.SandboxPolicy policy = 7; // Immutable provenance supplied with this policy revision. @@ -2166,12 +2196,14 @@ enum PolicyStatus { // Get sandbox logs request (one-shot fetch). message GetSandboxLogsRequest { + reserved 3; + reserved "since_ms"; // Sandbox id. string sandbox_id = 1; // Maximum number of log lines to return. 0 means use default (2000). uint32 lines = 2; - // Only include logs with timestamp >= this value (ms since epoch). 0 means no filter. - int64 since_ms = 3; + // Only include logs at or after this time. Absence means no filter. + google.protobuf.Timestamp since_time = 103; // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. repeated string sources = 4; // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. @@ -2234,10 +2266,12 @@ message SupervisorHello { // Gateway accepts the supervisor session. message SessionAccepted { + reserved 2; + reserved "heartbeat_interval_secs"; // Gateway-assigned session ID for this connection. string session_id = 1; - // Recommended heartbeat interval in seconds. - uint32 heartbeat_interval_secs = 2; + // Recommended heartbeat interval. + google.protobuf.Duration heartbeat_interval = 102; } // Gateway rejects the supervisor session. @@ -2367,6 +2401,8 @@ message L7RequestSample { // Structured denial summary from sandbox aggregator. message DenialSummary { + reserved 7, 8; + reserved "first_seen_ms", "last_seen_ms"; // Sandbox ID that produced this summary. string sandbox_id = 1; // Denied destination host. @@ -2379,10 +2415,10 @@ message DenialSummary { repeated string ancestors = 5; // Denial reason from OPA evaluation. string deny_reason = 6; - // First denial timestamp (ms since epoch). - int64 first_seen_ms = 7; - // Most recent denial timestamp (ms since epoch). - int64 last_seen_ms = 8; + // Time of the first denial. + google.protobuf.Timestamp first_seen_time = 107; + // Time of the most recent denial. + google.protobuf.Timestamp last_seen_time = 108; // Number of denials in the current window. uint32 count = 9; // Events dropped during aggregator cooldown. @@ -2424,6 +2460,8 @@ message NetworkActivitySummary { // A proposed policy rule with rationale and approval status. message PolicyChunk { + reserved 9, 10, 14, 15; + reserved "created_at_ms", "decided_at_ms", "first_seen_ms", "last_seen_ms"; // Unique chunk identifier. string id = 1; // Approval status: "pending", "approved", "rejected". @@ -2440,20 +2478,20 @@ message PolicyChunk { float confidence = 7; // IDs of denial summaries that led to this chunk. repeated string denial_summary_ids = 8; - // Creation timestamp (ms since epoch). - int64 created_at_ms = 9; - // When the user approved/rejected (ms since epoch). 0 if undecided. - int64 decided_at_ms = 10; + // Time when this chunk was created. + google.protobuf.Timestamp created_time = 109; + // Time when the user approved or rejected the chunk. Absent if undecided. + google.protobuf.Timestamp decided_time = 110; // Recommendation stage: "initial" or "refined" (progressive L7 visibility). string stage = 11; // For stage="refined": the initial chunk this replaces. string supersedes_chunk_id = 12; // How many times this endpoint has been seen across denial flush cycles. int32 hit_count = 13; - // First time this endpoint was proposed (ms since epoch). - int64 first_seen_ms = 14; - // Most recent time this endpoint was re-proposed (ms since epoch). - int64 last_seen_ms = 15; + // First time this endpoint was proposed. + google.protobuf.Timestamp first_seen_time = 114; + // Most recent time this endpoint was proposed again. + google.protobuf.Timestamp last_seen_time = 115; // Binary path that triggered the denial (denormalized for display convenience). string binary = 16; // Validation verdict from gateway-side static checks (prover output). @@ -2539,14 +2577,16 @@ message GetDraftPolicyRequest { } message GetDraftPolicyResponse { + reserved 4; + reserved "last_analyzed_at_ms"; // Draft policy chunks. repeated PolicyChunk chunks = 1; // LLM-generated summary of all analysis (empty in mechanistic mode). string rolling_summary = 2; // Current draft version. uint64 draft_version = 3; - // When the last analysis completed (ms since epoch). - int64 last_analyzed_at_ms = 4; + // Time when the last analysis completed. + google.protobuf.Timestamp last_analyzed_time = 104; } // Approve a single draft chunk. @@ -2666,8 +2706,10 @@ message GetDraftHistoryRequest { } message DraftHistoryEntry { - // Event timestamp (ms since epoch). - int64 timestamp_ms = 1; + reserved 1; + reserved "timestamp_ms"; + // Time when the event occurred. + google.protobuf.Timestamp event_time = 101; // Event type: "denial_detected", "analysis_cycle", "approved", // "rejected", "edited", "undone", "cleared". string event_type = 2; @@ -2906,11 +2948,13 @@ message ListWorkspaceMembersResponse { // Kept at the end of the file so adding it does not renumber existing // generated message descriptors. message ExtensionServiceCredential { + reserved 3; + reserved "expires_at_ms"; // Operator registration name used to correlate the credential with the // stable service registration delivered by GetSandboxConfig. string service_name = 1; // Gateway-minted JWT with an audience derived from the registration. string token = 2 [(openshell.options.v1.secret) = true]; - // Absolute expiry of the token, milliseconds since the epoch. - int64 expires_at_ms = 3; + // Absolute expiry of the token. + google.protobuf.Timestamp expiration_time = 103; } diff --git a/proto/sandbox.proto b/proto/sandbox.proto index 51139ba461..e3507cc433 100644 --- a/proto/sandbox.proto +++ b/proto/sandbox.proto @@ -6,6 +6,7 @@ syntax = "proto3"; package openshell.sandbox.v1; import "google/protobuf/struct.proto"; +import "google/protobuf/duration.proto"; // Sandbox-supervisor configuration and policy messages. // @@ -399,6 +400,8 @@ message GetSandboxConfigResponse { // Connection details for one operator-registered supervisor middleware service. // V1 supports plaintext and server-authenticated TLS gRPC. message SupervisorMiddlewareService { + reserved 4; + reserved "timeout"; // Operator-owned registration name used by policy attachments and diagnostics. string name = 1; // gRPC endpoint reachable from the sandbox supervisor. @@ -406,10 +409,9 @@ message SupervisorMiddlewareService { // Operator-owned logical payload limit applied to every binding exposed by // the service. This caps HTTP bodies and complete WebSocket messages. uint64 max_payload_bytes = 3; - // Default RPC timeout for this service. Empty uses the platform default of - // 500ms. Values use an integer with an `ms` or `s` suffix and must be - // between 10ms and 30s. - string timeout = 4; + // Default RPC timeout for this service. Absence uses the platform default of + // 500ms. Values must be between 10ms and 30s. + google.protobuf.Duration request_timeout = 104; // PEM-encoded trust roots loaded by the gateway from the operator-configured // tls_ca_cert_path. Empty uses the platform trust store. bytes tls_ca_cert_pem = 5; diff --git a/proto/supervisor_middleware.proto b/proto/supervisor_middleware.proto index 27fd804bdf..0575a1917d 100644 --- a/proto/supervisor_middleware.proto +++ b/proto/supervisor_middleware.proto @@ -7,6 +7,7 @@ package openshell.middleware.v1; import "google/protobuf/empty.proto"; import "google/protobuf/struct.proto"; +import "google/protobuf/duration.proto"; // SupervisorMiddleware lets an operator-run service inspect and transform // sandbox HTTP requests and client WebSocket text messages before OpenShell @@ -55,6 +56,8 @@ message MiddlewareManifest { // MiddlewareBinding declares one operation and phase supported by a service. message MiddlewareBinding { + reserved 4; + reserved "timeout"; // Supported operation. SupervisorMiddlewareOperation operation = 1; // Supported evaluation phase. PR 1 supports PRE_CREDENTIALS. PRE_RETURN is @@ -68,9 +71,8 @@ message MiddlewareBinding { // Optional binding-specific RPC timeout. Empty uses the operator-configured // service timeout, or the 500ms platform default when that is also omitted. // A non-empty value may shorten but cannot extend the operator timeout. - // Values use an integer with an `ms` or `s` suffix and must be between - // 10ms and 30s. - string timeout = 4; + // Values must be between 10ms and 30s. + google.protobuf.Duration request_timeout = 104; } // ValidateConfigRequest contains one policy configuration to validate. diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index 5653d8dc5e..8177f5a6cb 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -927,10 +927,11 @@ def exec_stream( command=list(command), workdir=workdir or "", environment=dict(env or {}), - timeout_seconds=timeout_seconds or 0, stdin=stdin or b"", no_login_shell=no_login_shell, ) + if timeout_seconds: + request.execution_timeout.seconds = timeout_seconds # Use whichever is larger: the default client timeout or the command # timeout plus headroom for SSH setup / teardown overhead. grpc_deadline = self._timeout diff --git a/sdk/go/openshell/v1/inference_client_test.go b/sdk/go/openshell/v1/inference_client_test.go index a75e6e97fe..b1fc78f1d8 100644 --- a/sdk/go/openshell/v1/inference_client_test.go +++ b/sdk/go/openshell/v1/inference_client_test.go @@ -7,6 +7,7 @@ import ( "context" "net" "testing" + "time" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/inferencev1" "github.com/stretchr/testify/assert" @@ -16,8 +17,13 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/types/known/durationpb" ) +func testDuration(seconds int64) *durationpb.Duration { + return durationpb.New(time.Duration(seconds) * time.Second) +} + type mockInferenceServer struct { pb.UnimplementedInferenceServer @@ -92,8 +98,8 @@ func TestSetRoute_Success(t *testing.T) { ValidatedEndpoints: []*pb.ValidatedEndpoint{ {Url: "https://api.openai.com/v1", Protocol: "openai"}, }, - TimeoutSecs: 120, - Workspace: "team-alpha", + RequestTimeout: testDuration(120), + Workspace: "team-alpha", }, } conn, cleanup := newMockInferenceServer(mock) @@ -126,7 +132,7 @@ func TestSetRoute_Success(t *testing.T) { assert.Equal(t, "gpt-4", mock.lastSetReq.GetModelId()) assert.Equal(t, "my-route", mock.lastSetReq.GetRouteName()) assert.Equal(t, "team-alpha", mock.lastSetReq.GetWorkspace()) - assert.Equal(t, uint64(120), mock.lastSetReq.GetTimeoutSecs()) + assert.Equal(t, int64(120), mock.lastSetReq.GetRequestTimeout().GetSeconds()) } func TestSetRoute_EmptyWorkspace(t *testing.T) { @@ -256,12 +262,12 @@ func TestSetRoute_NoVerify(t *testing.T) { func TestGetRoute_Success(t *testing.T) { mock := &mockInferenceServer{ getResp: &pb.GetInferenceRouteResponse{ - ProviderName: "vertex", - ModelId: "gemini-pro", - Version: 3, - RouteName: "default", - TimeoutSecs: 60, - Workspace: "prod", + ProviderName: "vertex", + ModelId: "gemini-pro", + Version: 3, + RouteName: "default", + RequestTimeout: testDuration(60), + Workspace: "prod", }, } conn, cleanup := newMockInferenceServer(mock) diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index ea9add6d21..2b6707c4e3 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -72,11 +72,11 @@ func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) { func TestConverterCoversAllProtoFields_SandboxCondition(t *testing.T) { handled := fieldSet{ - "type": true, - "status": true, - "reason": true, - "message": true, - "last_transition_time": true, + "type": true, + "status": true, + "reason": true, + "message": true, + "transition_time": true, } assertAllFieldsCovered(t, (&pb.SandboxCondition{}).ProtoReflect().Descriptor(), handled, nil) @@ -182,13 +182,13 @@ func TestConverterCoversAllProtoFields_L7DenyRule(t *testing.T) { func TestConverterCoversAllProtoFields_Provider(t *testing.T) { handled := fieldSet{ - "metadata": true, - "type": true, - "credentials": true, - "config": true, - "credential_expires_at_ms": true, - "profile_workspace": true, - "credential_handles": true, + "metadata": true, + "type": true, + "credentials": true, + "config": true, + "credential_expiration_times": true, + "profile_workspace": true, + "credential_handles": true, } assertAllFieldsCovered(t, (&dm.Provider{}).ProtoReflect().Descriptor(), handled, nil) @@ -206,14 +206,14 @@ func TestConverterCoversAllProtoFields_CredentialHandle(t *testing.T) { func TestConverterCoversAllProtoFields_SandboxPolicyRevision(t *testing.T) { handled := fieldSet{ - "version": true, - "policy_hash": true, - "status": true, - "load_error": true, - "created_at_ms": true, - "loaded_at_ms": true, - "policy": true, - "provenance": true, + "version": true, + "policy_hash": true, + "status": true, + "load_error": true, + "created_time": true, + "loaded_time": true, + "policy": true, + "provenance": true, } assertAllFieldsCovered(t, (&pb.SandboxPolicyRevision{}).ProtoReflect().Descriptor(), handled, nil) @@ -262,7 +262,7 @@ func TestConverterCoversAllProtoFields_ProviderCredentialTokenGrant(t *testing.T "audience": true, "jwt_svid_audience": true, "scopes": true, - "cache_ttl_seconds": true, + "cache_ttl": true, "audience_overrides": true, "client_assertion_type": true, "grant_type": true, diff --git a/sdk/go/openshell/v1/internal/converter/inference.go b/sdk/go/openshell/v1/internal/converter/inference.go index 8d5f2aebd3..8e52892a11 100644 --- a/sdk/go/openshell/v1/internal/converter/inference.go +++ b/sdk/go/openshell/v1/internal/converter/inference.go @@ -15,12 +15,12 @@ func InferenceRouteConfigToProto(workspace string, cfg *types.InferenceRouteConf return &pb.SetInferenceRouteRequest{Workspace: workspace} } return &pb.SetInferenceRouteRequest{ - ProviderName: cfg.ProviderName, - ModelId: cfg.ModelID, - RouteName: cfg.RouteName, - NoVerify: cfg.NoVerify, - TimeoutSecs: cfg.TimeoutSecs, - Workspace: workspace, + ProviderName: cfg.ProviderName, + ModelId: cfg.ModelID, + RouteName: cfg.RouteName, + NoVerify: cfg.NoVerify, + RequestTimeout: DurationFromSeconds(cfg.TimeoutSecs), + Workspace: workspace, } } @@ -35,7 +35,7 @@ func InferenceRouteFromSetResponse(resp *pb.SetInferenceRouteResponse) *types.In ModelID: resp.GetModelId(), Version: resp.GetVersion(), RouteName: resp.GetRouteName(), - TimeoutSecs: resp.GetTimeoutSecs(), + TimeoutSecs: DurationSecondsFromProto(resp.GetRequestTimeout()), Workspace: resp.GetWorkspace(), ValidationPerformed: resp.GetValidationPerformed(), ValidatedEndpoints: validatedEndpointsFromProto(resp.GetValidatedEndpoints()), @@ -53,7 +53,7 @@ func InferenceRouteFromGetResponse(resp *pb.GetInferenceRouteResponse) *types.In ModelID: resp.GetModelId(), Version: resp.GetVersion(), RouteName: resp.GetRouteName(), - TimeoutSecs: resp.GetTimeoutSecs(), + TimeoutSecs: DurationSecondsFromProto(resp.GetRequestTimeout()), Workspace: resp.GetWorkspace(), } } diff --git a/sdk/go/openshell/v1/internal/converter/inference_test.go b/sdk/go/openshell/v1/internal/converter/inference_test.go index c5b9befb62..ab14d2883d 100644 --- a/sdk/go/openshell/v1/internal/converter/inference_test.go +++ b/sdk/go/openshell/v1/internal/converter/inference_test.go @@ -28,7 +28,7 @@ func TestInferenceRouteConfigToProto(t *testing.T) { assert.Equal(t, "my-route", req.GetRouteName()) assert.True(t, req.GetNoVerify()) assert.False(t, req.GetVerify()) - assert.Equal(t, uint64(120), req.GetTimeoutSecs()) + assert.Equal(t, int64(120), req.GetRequestTimeout().GetSeconds()) assert.Equal(t, "team-alpha", req.GetWorkspace()) } @@ -62,8 +62,8 @@ func TestInferenceRouteFromSetResponse(t *testing.T) { {Url: "https://api.openai.com/v1", Protocol: "openai"}, {Url: "https://backup.openai.com/v1", Protocol: "openai"}, }, - TimeoutSecs: 120, - Workspace: "team-alpha", + RequestTimeout: DurationFromSeconds(120), + Workspace: "team-alpha", } route := InferenceRouteFromSetResponse(resp) @@ -104,12 +104,12 @@ func TestInferenceRouteFromSetResponse_NoEndpoints(t *testing.T) { func TestInferenceRouteFromGetResponse(t *testing.T) { resp := &pb.GetInferenceRouteResponse{ - ProviderName: "vertex", - ModelId: "gemini-pro", - Version: 3, - RouteName: "default", - TimeoutSecs: 60, - Workspace: "prod", + ProviderName: "vertex", + ModelId: "gemini-pro", + Version: 3, + RouteName: "default", + RequestTimeout: DurationFromSeconds(60), + Workspace: "prod", } route := InferenceRouteFromGetResponse(resp) @@ -163,6 +163,6 @@ func TestInferenceRoundTrip(t *testing.T) { assert.Equal(t, cfg.ModelID, req.GetModelId()) assert.Equal(t, cfg.RouteName, req.GetRouteName()) assert.Equal(t, cfg.NoVerify, req.GetNoVerify()) - assert.Equal(t, cfg.TimeoutSecs, req.GetTimeoutSecs()) + assert.Equal(t, int64(cfg.TimeoutSecs), req.GetRequestTimeout().GetSeconds()) assert.Equal(t, "my-ws", req.GetWorkspace()) } diff --git a/sdk/go/openshell/v1/internal/converter/log.go b/sdk/go/openshell/v1/internal/converter/log.go index 42f530fb1c..9a10e51987 100644 --- a/sdk/go/openshell/v1/internal/converter/log.go +++ b/sdk/go/openshell/v1/internal/converter/log.go @@ -16,7 +16,7 @@ func LogLineFromProto(l *pb.SandboxLogLine) *types.LogLine { return nil } return &types.LogLine{ - Timestamp: TimeFromMillis(l.GetTimestampMs()), + Timestamp: TimeFromProto(l.GetEventTime()), Level: l.GetLevel(), Target: l.GetTarget(), Message: l.GetMessage(), diff --git a/sdk/go/openshell/v1/internal/converter/log_test.go b/sdk/go/openshell/v1/internal/converter/log_test.go index 7462396262..6bd285b763 100644 --- a/sdk/go/openshell/v1/internal/converter/log_test.go +++ b/sdk/go/openshell/v1/internal/converter/log_test.go @@ -15,12 +15,12 @@ import ( func TestLogLineFromProto(t *testing.T) { proto := &pb.SandboxLogLine{ - SandboxId: "sbx-1", - TimestampMs: 1700000000000, - Level: "INFO", - Target: "network", - Message: "Connection established", - Source: "sandbox-agent", + SandboxId: "sbx-1", + EventTime: TimestampFromMillis(1700000000000), + Level: "INFO", + Target: "network", + Message: "Connection established", + Source: "sandbox-agent", Fields: map[string]string{ "host": "api.example.com", "port": "443", @@ -45,9 +45,9 @@ func TestLogLineFromProto_Nil(t *testing.T) { func TestLogLineDeepCopy(t *testing.T) { proto := &pb.SandboxLogLine{ - TimestampMs: 1700000000000, - Level: "WARN", - Message: "test", + EventTime: TimestampFromMillis(1700000000000), + Level: "WARN", + Message: "test", Fields: map[string]string{ "key": "value", }, @@ -64,8 +64,8 @@ func TestLogLineDeepCopy(t *testing.T) { func TestLogResultFromProto(t *testing.T) { proto := &pb.GetSandboxLogsResponse{ Logs: []*pb.SandboxLogLine{ - {TimestampMs: 1700000000000, Level: "INFO", Message: "first"}, - {TimestampMs: 1700000001000, Level: "DEBUG", Message: "second"}, + {EventTime: TimestampFromMillis(1700000000000), Level: "INFO", Message: "first"}, + {EventTime: TimestampFromMillis(1700000001000), Level: "DEBUG", Message: "second"}, }, BufferTotal: 100, } diff --git a/sdk/go/openshell/v1/internal/converter/policy.go b/sdk/go/openshell/v1/internal/converter/policy.go index 8d68ca2f57..1a473654e9 100644 --- a/sdk/go/openshell/v1/internal/converter/policy.go +++ b/sdk/go/openshell/v1/internal/converter/policy.go @@ -62,13 +62,13 @@ func PolicyChunkFromProto(c *pb.PolicyChunk) *types.PolicyChunk { SecurityNotes: c.GetSecurityNotes(), Confidence: c.GetConfidence(), DenialSummaryIDs: CopyStringSlice(c.GetDenialSummaryIds()), - CreatedAt: TimeFromMillis(c.GetCreatedAtMs()), - DecidedAt: TimeFromMillis(c.GetDecidedAtMs()), + CreatedAt: TimeFromProto(c.GetCreatedTime()), + DecidedAt: TimeFromProto(c.GetDecidedTime()), Stage: c.GetStage(), SupersedesChunkID: c.GetSupersedesChunkId(), HitCount: c.GetHitCount(), - FirstSeen: TimeFromMillis(c.GetFirstSeenMs()), - LastSeen: TimeFromMillis(c.GetLastSeenMs()), + FirstSeen: TimeFromProto(c.GetFirstSeenTime()), + LastSeen: TimeFromProto(c.GetLastSeenTime()), Binary: c.GetBinary(), ValidationResult: c.GetValidationResult(), RejectionReason: c.GetRejectionReason(), @@ -91,7 +91,7 @@ func DraftPolicyFromProto(r *pb.GetDraftPolicyResponse) *types.DraftPolicy { result := &types.DraftPolicy{ RollingSummary: r.GetRollingSummary(), DraftVersion: r.GetDraftVersion(), - LastAnalyzedAt: TimeFromMillis(r.GetLastAnalyzedAtMs()), + LastAnalyzedAt: TimeFromProto(r.GetLastAnalyzedTime()), } if chunks := r.GetChunks(); len(chunks) > 0 { result.Chunks = make([]types.PolicyChunk, 0, len(chunks)) @@ -299,8 +299,8 @@ func SandboxPolicyRevisionFromProto(r *pb.SandboxPolicyRevision) *types.SandboxP PolicyHash: r.GetPolicyHash(), Status: PolicyLoadStatusFromProto(r.GetStatus()), LoadError: r.GetLoadError(), - CreatedAt: TimeFromMillis(r.GetCreatedAtMs()), - LoadedAt: TimeFromMillis(r.GetLoadedAtMs()), + CreatedAt: TimeFromProto(r.GetCreatedTime()), + LoadedAt: TimeFromProto(r.GetLoadedTime()), Policy: SandboxPolicyFromProto(r.GetPolicy()), Provenance: CopyStringMap(r.GetProvenance()), } @@ -383,7 +383,7 @@ func DraftHistoryEntryFromProto(e *pb.DraftHistoryEntry) *types.DraftHistoryEntr return nil } return &types.DraftHistoryEntry{ - Timestamp: TimeFromMillis(e.GetTimestampMs()), + Timestamp: TimeFromProto(e.GetEventTime()), EventType: e.GetEventType(), Description: e.GetDescription(), ChunkID: e.GetChunkId(), diff --git a/sdk/go/openshell/v1/internal/converter/policy_test.go b/sdk/go/openshell/v1/internal/converter/policy_test.go index 6935b3def0..dde2205773 100644 --- a/sdk/go/openshell/v1/internal/converter/policy_test.go +++ b/sdk/go/openshell/v1/internal/converter/policy_test.go @@ -75,13 +75,13 @@ func TestPolicyChunkFromProto(t *testing.T) { SecurityNotes: "No concerns", Confidence: 0.95, DenialSummaryIds: []string{"d1", "d2"}, - CreatedAtMs: 1700000000000, - DecidedAtMs: 1700000001000, + CreatedTime: TimestampFromMillis(1700000000000), + DecidedTime: TimestampFromMillis(1700000001000), Stage: "initial", SupersedesChunkId: "chunk-0", HitCount: 5, - FirstSeenMs: 1699999999000, - LastSeenMs: 1700000000500, + FirstSeenTime: TimestampFromMillis(1699999999000), + LastSeenTime: TimestampFromMillis(1700000000500), Binary: "/usr/bin/curl", ValidationResult: "valid", RejectionReason: "", @@ -146,7 +146,7 @@ func TestDraftPolicyFromProto(t *testing.T) { }, RollingSummary: "Analysis summary", DraftVersion: 42, - LastAnalyzedAtMs: 1700000000000, + LastAnalyzedTime: TimestampFromMillis(1700000000000), } draft := DraftPolicyFromProto(proto) @@ -442,8 +442,8 @@ func TestSandboxPolicyRevisionFromProto(t *testing.T) { PolicyHash: "sha256:abc123", Status: pb.PolicyStatus_POLICY_STATUS_LOADED, LoadError: "", - CreatedAtMs: 1700000000000, - LoadedAtMs: 1700000001000, + CreatedTime: TimestampFromMillis(1700000000000), + LoadedTime: TimestampFromMillis(1700000001000), Provenance: map[string]string{"source": "api", "user": "admin"}, } @@ -594,7 +594,7 @@ func TestClearResultFromProto_Nil(t *testing.T) { func TestDraftHistoryEntryFromProto(t *testing.T) { proto := &pb.DraftHistoryEntry{ - TimestampMs: 1700000000000, + EventTime: TimestampFromMillis(1700000000000), EventType: "approved", Description: "Chunk c1 approved", ChunkId: "c1", diff --git a/sdk/go/openshell/v1/internal/converter/profile.go b/sdk/go/openshell/v1/internal/converter/profile.go index 8bbb346829..ff93a94230 100644 --- a/sdk/go/openshell/v1/internal/converter/profile.go +++ b/sdk/go/openshell/v1/internal/converter/profile.go @@ -176,8 +176,8 @@ func profileCredentialRefreshFromProto(r *pb.ProviderCredentialRefresh) *types.P } result := &types.ProfileCredentialRefresh{ Strategy: RefreshStrategyFromProto(r.GetStrategy()), TokenURL: r.GetTokenUrl(), - Scopes: CopyStringSlice(r.GetScopes()), RefreshBeforeSeconds: r.GetRefreshBeforeSeconds(), - MaxLifetimeSeconds: r.GetMaxLifetimeSeconds(), + Scopes: CopyStringSlice(r.GetScopes()), RefreshBeforeSeconds: int64(DurationSecondsFromProto(r.GetRefreshBefore())), + MaxLifetimeSeconds: int64(DurationSecondsFromProto(r.GetMaxLifetime())), } for _, material := range r.GetMaterial() { result.Material = append(result.Material, types.ProfileCredentialRefreshMaterial{Name: material.GetName(), Description: material.GetDescription(), Required: material.GetRequired(), Secret: material.GetSecret()}) @@ -194,8 +194,8 @@ func profileCredentialRefreshToProto(r *types.ProfileCredentialRefresh) *pb.Prov } result := &pb.ProviderCredentialRefresh{ Strategy: RefreshStrategyToProto(r.Strategy), TokenUrl: r.TokenURL, - Scopes: CopyStringSlice(r.Scopes), RefreshBeforeSeconds: r.RefreshBeforeSeconds, - MaxLifetimeSeconds: r.MaxLifetimeSeconds, + Scopes: CopyStringSlice(r.Scopes), RefreshBefore: DurationFromSeconds(uint64(r.RefreshBeforeSeconds)), + MaxLifetime: DurationFromSeconds(uint64(r.MaxLifetimeSeconds)), } for _, material := range r.Material { result.Material = append(result.Material, &pb.ProviderCredentialRefreshMaterial{Name: material.Name, Description: material.Description, Required: material.Required, Secret: material.Secret}) @@ -215,7 +215,7 @@ func tokenGrantFromProto(tg *pb.ProviderCredentialTokenGrant) *types.CredentialT Audience: tg.GetAudience(), JWTSVIDAudience: tg.GetJwtSvidAudience(), Scopes: CopyStringSlice(tg.GetScopes()), - CacheTTLSeconds: tg.GetCacheTtlSeconds(), + CacheTTLSeconds: int64(DurationSecondsFromProto(tg.GetCacheTtl())), ClientAssertionType: tg.GetClientAssertionType(), GrantType: CredentialTokenGrantTypeFromProto(tg.GetGrantType()), SubjectToken: subjectTokenFromProto(tg.GetSubjectToken()), @@ -239,7 +239,7 @@ func tokenGrantToProto(tg *types.CredentialTokenGrant) *pb.ProviderCredentialTok Audience: tg.Audience, JwtSvidAudience: tg.JWTSVIDAudience, Scopes: CopyStringSlice(tg.Scopes), - CacheTtlSeconds: tg.CacheTTLSeconds, + CacheTtl: DurationFromSeconds(uint64(tg.CacheTTLSeconds)), ClientAssertionType: tg.ClientAssertionType, GrantType: CredentialTokenGrantTypeToProto(tg.GrantType), SubjectToken: subjectTokenToProto(tg.SubjectToken), diff --git a/sdk/go/openshell/v1/internal/converter/profile_test.go b/sdk/go/openshell/v1/internal/converter/profile_test.go index 5d52c0a9ee..330fb2f617 100644 --- a/sdk/go/openshell/v1/internal/converter/profile_test.go +++ b/sdk/go/openshell/v1/internal/converter/profile_test.go @@ -154,7 +154,7 @@ func TestProfileCredentialFromProto(t *testing.T) { Audience: "https://api.example.com", JwtSvidAudience: "spiffe://example.com", Scopes: []string{"read", "write"}, - CacheTtlSeconds: 300, + CacheTtl: DurationFromSeconds(300), ClientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", GrantType: pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE, SubjectToken: &pb.ProviderCredentialTokenGrantSubjectToken{ @@ -310,7 +310,7 @@ func TestProfileCredentialToProto(t *testing.T) { assert.Equal(t, "https://api.example.com", proto.TokenGrant.Audience) assert.Equal(t, "spiffe://example.com", proto.TokenGrant.JwtSvidAudience) assert.Equal(t, []string{"read"}, proto.TokenGrant.Scopes) - assert.Equal(t, int64(300), proto.TokenGrant.CacheTtlSeconds) + assert.Equal(t, int64(300), proto.TokenGrant.CacheTtl.GetSeconds()) assert.Equal(t, "urn:custom", proto.TokenGrant.ClientAssertionType) assert.Equal(t, pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE, proto.TokenGrant.GrantType) require.NotNil(t, proto.TokenGrant.SubjectToken) diff --git a/sdk/go/openshell/v1/internal/converter/provider.go b/sdk/go/openshell/v1/internal/converter/provider.go index 42799feab0..28edd21fe2 100644 --- a/sdk/go/openshell/v1/internal/converter/provider.go +++ b/sdk/go/openshell/v1/internal/converter/provider.go @@ -8,6 +8,7 @@ import ( "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" + "google.golang.org/protobuf/types/known/timestamppb" ) // ProviderFromProto converts a proto Provider to an SDK Provider. @@ -27,18 +28,18 @@ func ProviderFromProto(p *dm.Provider) *types.Provider { if m := p.GetMetadata(); m != nil { result.ID = m.GetId() result.Name = m.GetName() - result.CreatedAt = TimeFromMillis(m.GetCreatedAtMs()) + result.CreatedAt = TimeFromProto(m.GetCreatedTime()) result.Labels = CopyStringMap(m.GetLabels()) result.Annotations = CopyStringMap(m.GetAnnotations()) result.ResourceVersion = m.GetResourceVersion() result.Workspace = m.GetWorkspace() - result.DeletionTimestamp = TimeFromMillisPtr(m.GetDeletionTimestampMs()) + result.DeletionTimestamp = TimePtrFromProto(m.GetDeletionTime()) } - if expires := p.GetCredentialExpiresAtMs(); len(expires) > 0 { + if expires := p.GetCredentialExpirationTimes(); len(expires) > 0 { result.Spec.CredentialExpiresAt = make(map[string]time.Time, len(expires)) - for k, ms := range expires { - result.Spec.CredentialExpiresAt[k] = TimeFromMillis(ms) + for k, timestamp := range expires { + result.Spec.CredentialExpiresAt[k] = TimeFromProto(timestamp) } } @@ -64,14 +65,14 @@ func ProviderToProto(p *types.Provider) *dm.Provider { result := &dm.Provider{ Metadata: &dm.ObjectMeta{ - Id: p.ID, - Name: p.Name, - CreatedAtMs: MillisFromTime(p.CreatedAt), - Labels: CopyStringMap(p.Labels), - Annotations: CopyStringMap(p.Annotations), - ResourceVersion: p.ResourceVersion, - Workspace: p.Workspace, - DeletionTimestampMs: MillisFromTimePtr(p.DeletionTimestamp), + Id: p.ID, + Name: p.Name, + CreatedTime: TimestampFromTime(p.CreatedAt), + Labels: CopyStringMap(p.Labels), + Annotations: CopyStringMap(p.Annotations), + ResourceVersion: p.ResourceVersion, + Workspace: p.Workspace, + DeletionTime: TimestampFromTimePtr(p.DeletionTimestamp), }, Type: p.Type, Credentials: CopyStringMap(p.Spec.Credentials), @@ -80,9 +81,9 @@ func ProviderToProto(p *types.Provider) *dm.Provider { } if len(p.Spec.CredentialExpiresAt) > 0 { - result.CredentialExpiresAtMs = make(map[string]int64, len(p.Spec.CredentialExpiresAt)) + result.CredentialExpirationTimes = make(map[string]*timestamppb.Timestamp, len(p.Spec.CredentialExpiresAt)) for k, t := range p.Spec.CredentialExpiresAt { - result.CredentialExpiresAtMs[k] = MillisFromTime(t) + result.CredentialExpirationTimes[k] = TimestampFromTime(t) } } diff --git a/sdk/go/openshell/v1/internal/converter/provider_test.go b/sdk/go/openshell/v1/internal/converter/provider_test.go index 84411830c4..c0dc53ce4e 100644 --- a/sdk/go/openshell/v1/internal/converter/provider_test.go +++ b/sdk/go/openshell/v1/internal/converter/provider_test.go @@ -11,6 +11,7 @@ import ( dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestProviderFromProto_Nil(t *testing.T) { @@ -22,7 +23,7 @@ func TestProviderFromProto_Full(t *testing.T) { Metadata: &dm.ObjectMeta{ Id: "prov-1", Name: "claude-provider", - CreatedAtMs: 1700000000000, + CreatedTime: TimestampFromMillis(1700000000000), Labels: map[string]string{"env": "prod"}, Annotations: map[string]string{"note": "test"}, ResourceVersion: 42, @@ -32,8 +33,8 @@ func TestProviderFromProto_Full(t *testing.T) { Credentials: map[string]string{"api_key": "secret"}, Config: map[string]string{"base_url": "https://api.example.com"}, ProfileWorkspace: "shared", - CredentialExpiresAtMs: map[string]int64{ - "api_key": 1700003600000, + CredentialExpirationTimes: map[string]*timestamppb.Timestamp{ + "api_key": TimestampFromMillis(1700003600000), }, CredentialHandles: map[string]*dm.CredentialHandle{ "api_key": { @@ -133,8 +134,8 @@ func TestProviderToProto_Full(t *testing.T) { assert.Equal(t, map[string]string{"token": "abc"}, result.Credentials) assert.Equal(t, map[string]string{"url": "https://example.com"}, result.Config) - require.Len(t, result.CredentialExpiresAtMs, 1) - assert.Greater(t, result.CredentialExpiresAtMs["token"], int64(0)) + require.Len(t, result.CredentialExpirationTimes, 1) + assert.Greater(t, MillisFromProto(result.CredentialExpirationTimes["token"]), int64(0)) require.Len(t, result.CredentialHandles, 1) h := result.CredentialHandles["token"] diff --git a/sdk/go/openshell/v1/internal/converter/refresh.go b/sdk/go/openshell/v1/internal/converter/refresh.go index be5ee8d03a..a3e98c3bce 100644 --- a/sdk/go/openshell/v1/internal/converter/refresh.go +++ b/sdk/go/openshell/v1/internal/converter/refresh.go @@ -4,9 +4,6 @@ package converter import ( - "math" - "time" - "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" ) @@ -51,13 +48,6 @@ func RefreshRecoveryActionFromProto(a pb.ProviderCredentialRefreshRecoveryAction } } -func refreshNextTimeFromMillis(ms int64) time.Time { - if ms == math.MaxInt64 { - return time.Time{} - } - return TimeFromMillis(ms) -} - // RefreshStrategyToProto converts an SDK RefreshStrategy to a proto ProviderCredentialRefreshStrategy. func RefreshStrategyToProto(s types.RefreshStrategy) pb.ProviderCredentialRefreshStrategy { switch s { @@ -91,14 +81,14 @@ func RefreshStatusFromProto(s *pb.ProviderCredentialRefreshStatus) *types.Refres CredentialKey: s.GetCredentialKey(), Strategy: RefreshStrategyFromProto(s.GetStrategy()), Status: s.GetStatus(), - ExpiresAt: TimeFromMillis(s.GetExpiresAtMs()), - NextRefreshAt: refreshNextTimeFromMillis(s.GetNextRefreshAtMs()), - LastRefreshAt: TimeFromMillis(s.GetLastRefreshAtMs()), + ExpiresAt: TimeFromProto(s.GetExpirationTime()), + NextRefreshAt: TimeFromProto(s.GetNextRefreshTime()), + LastRefreshAt: TimeFromProto(s.GetLastRefreshTime()), LastError: s.GetLastError(), RecoveryAction: RefreshRecoveryActionFromProto(s.GetRecoveryAction()), FailureCode: s.GetFailureCode(), ProviderErrorSubtype: s.GetProviderErrorSubtype(), - LastErrorAt: TimeFromMillis(s.GetLastErrorAtMs()), + LastErrorAt: TimeFromProto(s.GetLastErrorTime()), } } @@ -120,8 +110,7 @@ func RefreshConfigToProto(c *types.RefreshConfig) *pb.ConfigureProviderRefreshRe } if c.ExpiresAt != nil { - ms := MillisFromTime(*c.ExpiresAt) - result.ExpiresAtMs = &ms + result.ExpirationTime = TimestampFromTime(*c.ExpiresAt) } return result diff --git a/sdk/go/openshell/v1/internal/converter/refresh_test.go b/sdk/go/openshell/v1/internal/converter/refresh_test.go index 9ffa6e590a..16e98c96a2 100644 --- a/sdk/go/openshell/v1/internal/converter/refresh_test.go +++ b/sdk/go/openshell/v1/internal/converter/refresh_test.go @@ -83,14 +83,14 @@ func TestRefreshStatusFromProto(t *testing.T) { CredentialKey: "API_KEY", Strategy: pb.ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_REFRESH_TOKEN, Status: "active", - ExpiresAtMs: 1700000000000, - NextRefreshAtMs: 1699999000000, - LastRefreshAtMs: 1699998000000, + ExpirationTime: TimestampFromMillis(1700000000000), + NextRefreshTime: TimestampFromMillis(1699999000000), + LastRefreshTime: TimestampFromMillis(1699998000000), LastError: "none", RecoveryAction: pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE, FailureCode: "oauth_invalid_grant", ProviderErrorSubtype: "invalid_rapt", - LastErrorAtMs: 1699997000000, + LastErrorTime: TimestampFromMillis(1699997000000), } status := RefreshStatusFromProto(proto) @@ -138,7 +138,7 @@ func TestRefreshStatusFromProto_ParkedRefreshHasNoNextTime(t *testing.T) { proto := &pb.ProviderCredentialRefreshStatus{ ProviderName: "test", CredentialKey: "KEY", - NextRefreshAtMs: math.MaxInt64, + NextRefreshTime: nil, RecoveryAction: pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE, } @@ -189,8 +189,8 @@ func TestRefreshConfigToProto(t *testing.T) { assert.Equal(t, "client_secret", proto.SecretMaterialKeys[0], "secret keys must be deep copied") // ExpiresAt conversion - require.NotNil(t, proto.ExpiresAtMs) - assert.Equal(t, MillisFromTime(expiresAt), *proto.ExpiresAtMs) + require.NotNil(t, proto.ExpirationTime) + assert.Equal(t, MillisFromTime(expiresAt), MillisFromProto(proto.ExpirationTime)) } func TestRefreshConfigToProto_NilExpiresAt(t *testing.T) { @@ -203,7 +203,7 @@ func TestRefreshConfigToProto_NilExpiresAt(t *testing.T) { proto := RefreshConfigToProto(config) require.NotNil(t, proto) - assert.Nil(t, proto.ExpiresAtMs) + assert.Nil(t, proto.ExpirationTime) } func TestRefreshConfigToProto_Nil(t *testing.T) { diff --git a/sdk/go/openshell/v1/internal/converter/sandbox.go b/sdk/go/openshell/v1/internal/converter/sandbox.go index 6b61053295..0791bfde4d 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox.go @@ -23,12 +23,12 @@ func SandboxFromProto(s *pb.Sandbox) *types.Sandbox { if m := s.GetMetadata(); m != nil { result.ID = m.GetId() result.Name = m.GetName() - result.CreatedAt = TimeFromMillis(m.GetCreatedAtMs()) + result.CreatedAt = TimeFromProto(m.GetCreatedTime()) result.Labels = CopyStringMap(m.GetLabels()) result.Annotations = CopyStringMap(m.GetAnnotations()) result.ResourceVersion = m.GetResourceVersion() result.Workspace = m.GetWorkspace() - result.DeletionTimestamp = TimeFromMillisPtr(m.GetDeletionTimestampMs()) + result.DeletionTimestamp = TimePtrFromProto(m.GetDeletionTime()) } if spec := s.GetSpec(); spec != nil { @@ -98,7 +98,7 @@ func sandboxStatusFromProto(status *pb.SandboxStatus) types.SandboxStatus { Status: c.GetStatus(), Reason: c.GetReason(), Message: c.GetMessage(), - LastTransitionTime: c.GetLastTransitionTime(), + LastTransitionTime: TimestampStringFromProto(c.GetTransitionTime()), }) } result.ExitCode = CopyInt32Ptr(status.ExitCode) @@ -166,14 +166,14 @@ func SandboxToProto(s *types.Sandbox) *pb.Sandbox { return &pb.Sandbox{ Metadata: &dm.ObjectMeta{ - Id: s.ID, - Name: s.Name, - CreatedAtMs: MillisFromTime(s.CreatedAt), - Labels: CopyStringMap(s.Labels), - Annotations: CopyStringMap(s.Annotations), - ResourceVersion: s.ResourceVersion, - Workspace: s.Workspace, - DeletionTimestampMs: MillisFromTimePtr(s.DeletionTimestamp), + Id: s.ID, + Name: s.Name, + CreatedTime: TimestampFromTime(s.CreatedAt), + Labels: CopyStringMap(s.Labels), + Annotations: CopyStringMap(s.Annotations), + ResourceVersion: s.ResourceVersion, + Workspace: s.Workspace, + DeletionTime: TimestampFromTimePtr(s.DeletionTimestamp), }, Spec: SandboxSpecToProto(&s.Spec), } diff --git a/sdk/go/openshell/v1/internal/converter/sandbox_test.go b/sdk/go/openshell/v1/internal/converter/sandbox_test.go index 9f68013845..2a9c2c9983 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox_test.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox_test.go @@ -22,14 +22,14 @@ func TestSandboxFromProto(t *testing.T) { exitCode := int32(0) proto := &pb.Sandbox{ Metadata: &dm.ObjectMeta{ - Id: "sb-1", - Name: "my-sandbox", - CreatedAtMs: 1700000000000, - Labels: map[string]string{"env": "dev"}, - Annotations: map[string]string{"owner": "team-a"}, - ResourceVersion: 3, - Workspace: "prod", - DeletionTimestampMs: 1700000060000, + Id: "sb-1", + Name: "my-sandbox", + CreatedTime: TimestampFromMillis(1700000000000), + Labels: map[string]string{"env": "dev"}, + Annotations: map[string]string{"owner": "team-a"}, + ResourceVersion: 3, + Workspace: "prod", + DeletionTime: TimestampFromMillis(1700000060000), }, Spec: &pb.SandboxSpec{ LogLevel: "debug", @@ -71,11 +71,11 @@ func TestSandboxFromProto(t *testing.T) { ExitCode: &exitCode, Conditions: []*pb.SandboxCondition{ { - Type: "Ready", - Status: "True", - Reason: "AllGood", - Message: "Sandbox is ready", - LastTransitionTime: "2024-01-01T00:00:00Z", + Type: "Ready", + Status: "True", + Reason: "AllGood", + Message: "Sandbox is ready", + TransitionTime: TimestampFromMillis(1704067200000), }, }, }, @@ -265,12 +265,12 @@ func TestSandboxToProto(t *testing.T) { require.NotNil(t, p.Metadata) assert.Equal(t, "sb-1", p.Metadata.Id) assert.Equal(t, "my-sandbox", p.Metadata.Name) - assert.Equal(t, int64(1700000000000), p.Metadata.CreatedAtMs) + assert.Equal(t, int64(1700000000000), MillisFromProto(p.Metadata.CreatedTime)) assert.Equal(t, map[string]string{"env": "dev"}, p.Metadata.Labels) assert.Equal(t, map[string]string{"owner": "team-a"}, p.Metadata.Annotations) assert.Equal(t, uint64(3), p.Metadata.ResourceVersion) assert.Equal(t, "prod", p.Metadata.Workspace) - assert.Equal(t, int64(1700000060000), p.Metadata.DeletionTimestampMs) + assert.Equal(t, int64(1700000060000), MillisFromProto(p.Metadata.DeletionTime)) require.NotNil(t, p.Spec) assert.Equal(t, "info", p.Spec.LogLevel) diff --git a/sdk/go/openshell/v1/internal/converter/ssh.go b/sdk/go/openshell/v1/internal/converter/ssh.go index 538c1d17f5..e437d8808d 100644 --- a/sdk/go/openshell/v1/internal/converter/ssh.go +++ b/sdk/go/openshell/v1/internal/converter/ssh.go @@ -20,7 +20,7 @@ func SSHSessionFromProto(resp *pb.CreateSshSessionResponse) *v1.SSHSession { GatewayPort: resp.GetGatewayPort(), GatewayScheme: resp.GetGatewayScheme(), HostKeyFingerprint: resp.GetHostKeyFingerprint(), - ExpiresAtMs: resp.GetExpiresAtMs(), + ExpiresAtMs: MillisFromProto(resp.GetExpirationTime()), } } @@ -37,6 +37,6 @@ func SSHSessionToProto(session *v1.SSHSession) *pb.CreateSshSessionResponse { GatewayPort: session.GatewayPort, GatewayScheme: session.GatewayScheme, HostKeyFingerprint: session.HostKeyFingerprint, - ExpiresAtMs: session.ExpiresAtMs, + ExpirationTime: TimestampFromMillis(session.ExpiresAtMs), } } diff --git a/sdk/go/openshell/v1/internal/converter/ssh_test.go b/sdk/go/openshell/v1/internal/converter/ssh_test.go index 11ce395d75..dbff7f194e 100644 --- a/sdk/go/openshell/v1/internal/converter/ssh_test.go +++ b/sdk/go/openshell/v1/internal/converter/ssh_test.go @@ -20,7 +20,7 @@ func TestSSHSessionFromProto(t *testing.T) { GatewayPort: 2222, GatewayScheme: "https", HostKeyFingerprint: "SHA256:abc123", - ExpiresAtMs: 1700000000000, + ExpirationTime: TimestampFromMillis(1700000000000), } session := SSHSessionFromProto(resp) @@ -80,7 +80,7 @@ func TestSSHSessionToProto(t *testing.T) { assert.Equal(t, uint32(2222), resp.GatewayPort) assert.Equal(t, "https", resp.GatewayScheme) assert.Equal(t, "SHA256:abc123", resp.HostKeyFingerprint) - assert.Equal(t, int64(1700000000000), resp.ExpiresAtMs) + assert.Equal(t, int64(1700000000000), MillisFromProto(resp.ExpirationTime)) } func TestSSHSessionToProto_Nil(t *testing.T) { diff --git a/sdk/go/openshell/v1/internal/converter/time.go b/sdk/go/openshell/v1/internal/converter/time.go index 28a633cdff..12163166f9 100644 --- a/sdk/go/openshell/v1/internal/converter/time.go +++ b/sdk/go/openshell/v1/internal/converter/time.go @@ -3,7 +3,87 @@ package converter -import "time" +import ( + "time" + + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// TimeFromProto converts a valid protobuf timestamp to UTC time. +func TimeFromProto(value *timestamppb.Timestamp) time.Time { + if value == nil || value.CheckValid() != nil { + return time.Time{} + } + return value.AsTime().UTC() +} + +// TimePtrFromProto converts a valid protobuf timestamp to a UTC time pointer. +func TimePtrFromProto(value *timestamppb.Timestamp) *time.Time { + converted := TimeFromProto(value) + if converted.IsZero() { + return nil + } + return &converted +} + +// TimestampFromTime converts a non-zero time to a protobuf timestamp. +func TimestampFromTime(value time.Time) *timestamppb.Timestamp { + if value.IsZero() { + return nil + } + return timestamppb.New(value) +} + +// TimestampFromTimePtr converts a non-nil time pointer to a protobuf timestamp. +func TimestampFromTimePtr(value *time.Time) *timestamppb.Timestamp { + if value == nil { + return nil + } + return TimestampFromTime(*value) +} + +// MillisFromProto converts a valid protobuf timestamp to Unix milliseconds. +func MillisFromProto(value *timestamppb.Timestamp) int64 { + converted := TimeFromProto(value) + if converted.IsZero() { + return 0 + } + return converted.UnixMilli() +} + +// TimestampFromMillis converts non-zero Unix milliseconds to a protobuf timestamp. +func TimestampFromMillis(value int64) *timestamppb.Timestamp { + if value == 0 { + return nil + } + return timestamppb.New(time.UnixMilli(value)) +} + +// TimestampStringFromProto formats a valid protobuf timestamp as RFC 3339. +func TimestampStringFromProto(value *timestamppb.Timestamp) string { + converted := TimeFromProto(value) + if converted.IsZero() { + return "" + } + return converted.Format(time.RFC3339Nano) +} + +// DurationSecondsFromProto converts a valid non-negative protobuf duration to seconds. +func DurationSecondsFromProto(value *durationpb.Duration) uint64 { + if value == nil || value.CheckValid() != nil || value.AsDuration() < 0 { + return 0 + } + return uint64(value.AsDuration() / time.Second) +} + +// DurationFromSeconds converts non-zero seconds to a protobuf duration. +func DurationFromSeconds(value uint64) *durationpb.Duration { + if value == 0 || value > uint64((time.Duration(1<<63-1))/time.Second) { + return nil + } + return durationpb.New(time.Duration(value) * time.Second) +} // TimeFromMillis converts a millisecond epoch timestamp to time.Time. // A zero value returns the zero time. diff --git a/sdk/go/openshell/v1/internal/converter/workspace.go b/sdk/go/openshell/v1/internal/converter/workspace.go index 80925a338b..71e6129345 100644 --- a/sdk/go/openshell/v1/internal/converter/workspace.go +++ b/sdk/go/openshell/v1/internal/converter/workspace.go @@ -20,12 +20,12 @@ func WorkspaceFromProto(w *dm.Workspace) *types.Workspace { if m := w.GetMetadata(); m != nil { result.ID = m.GetId() result.Name = m.GetName() - result.CreatedAt = TimeFromMillis(m.GetCreatedAtMs()) + result.CreatedAt = TimeFromProto(m.GetCreatedTime()) result.Labels = CopyStringMap(m.GetLabels()) result.Annotations = CopyStringMap(m.GetAnnotations()) result.ResourceVersion = m.GetResourceVersion() result.Workspace = m.GetWorkspace() - result.DeletionTimestamp = TimeFromMillisPtr(m.GetDeletionTimestampMs()) + result.DeletionTimestamp = TimePtrFromProto(m.GetDeletionTime()) } if status := w.GetStatus(); status != nil { @@ -63,7 +63,7 @@ func WorkspaceMemberFromProto(m *pb.WorkspaceMember) *types.WorkspaceMember { if meta := m.GetMetadata(); meta != nil { result.ID = meta.GetId() result.Name = meta.GetName() - result.CreatedAt = TimeFromMillis(meta.GetCreatedAtMs()) + result.CreatedAt = TimeFromProto(meta.GetCreatedTime()) result.Labels = CopyStringMap(meta.GetLabels()) result.Annotations = CopyStringMap(meta.GetAnnotations()) result.ResourceVersion = meta.GetResourceVersion() diff --git a/sdk/go/openshell/v1/internal/converter/workspace_test.go b/sdk/go/openshell/v1/internal/converter/workspace_test.go index e86ec443c0..9d39f25fed 100644 --- a/sdk/go/openshell/v1/internal/converter/workspace_test.go +++ b/sdk/go/openshell/v1/internal/converter/workspace_test.go @@ -17,14 +17,14 @@ import ( func TestWorkspaceFromProto(t *testing.T) { proto := &dm.Workspace{ Metadata: &dm.ObjectMeta{ - Id: "ws-1", - Name: "my-workspace", - CreatedAtMs: 1700000000000, - Labels: map[string]string{"team": "platform"}, - Annotations: map[string]string{"managed-by": "sdk"}, - ResourceVersion: 3, - Workspace: "", - DeletionTimestampMs: 1700000060000, + Id: "ws-1", + Name: "my-workspace", + CreatedTime: TimestampFromMillis(1700000000000), + Labels: map[string]string{"team": "platform"}, + Annotations: map[string]string{"managed-by": "sdk"}, + ResourceVersion: 3, + Workspace: "", + DeletionTime: TimestampFromMillis(1700000060000), }, Status: &dm.WorkspaceStatus{ Phase: dm.WorkspacePhase_WORKSPACE_PHASE_ACTIVE, @@ -115,7 +115,7 @@ func TestWorkspaceMemberFromProto(t *testing.T) { Metadata: &dm.ObjectMeta{ Id: "mem-1", Name: "member-auto-name", - CreatedAtMs: 1700000000000, + CreatedTime: TimestampFromMillis(1700000000000), Annotations: map[string]string{"source": "cli"}, ResourceVersion: 2, }, diff --git a/sdk/go/openshell/v1/policy_client_test.go b/sdk/go/openshell/v1/policy_client_test.go index d572ca8e33..519eb160c1 100644 --- a/sdk/go/openshell/v1/policy_client_test.go +++ b/sdk/go/openshell/v1/policy_client_test.go @@ -8,6 +8,7 @@ import ( "net" "sync" "testing" + "time" "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" @@ -19,8 +20,13 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/types/known/timestamppb" ) +func testTimestamp(milliseconds int64) *timestamppb.Timestamp { + return timestamppb.New(time.UnixMilli(milliseconds)) +} + // --- Mock server for Policy RPCs --- type mockPolicyServer struct { @@ -206,7 +212,7 @@ func TestPolicyGetDraft(t *testing.T) { Rationale: "DNS access needed", Confidence: 0.95, DenialSummaryIds: []string{"ds-1", "ds-2"}, - CreatedAtMs: 1700000000000, + CreatedTime: testTimestamp(1700000000000), Stage: "initial", HitCount: 3, Binary: "/usr/bin/curl", @@ -222,7 +228,7 @@ func TestPolicyGetDraft(t *testing.T) { }, RollingSummary: "Two rules proposed", DraftVersion: 5, - LastAnalyzedAtMs: 1700000001000, + LastAnalyzedTime: testTimestamp(1700000001000), } client, cleanup := setupPolicyTest(t, mock) @@ -493,13 +499,13 @@ func TestPolicyGetDraftHistory(t *testing.T) { mock.historyResp = &pb.GetDraftHistoryResponse{ Entries: []*pb.DraftHistoryEntry{ { - TimestampMs: 1700000000000, + EventTime: testTimestamp(1700000000000), EventType: "approved", Description: "Chunk chunk-1 approved", ChunkId: "chunk-1", }, { - TimestampMs: 1700000001000, + EventTime: testTimestamp(1700000001000), EventType: "rejected", Description: "Chunk chunk-2 rejected: too broad", ChunkId: "chunk-2", @@ -567,8 +573,8 @@ func TestPolicyGetStatus(t *testing.T) { Version: 3, PolicyHash: "sha256:rev3", Status: pb.PolicyStatus_POLICY_STATUS_LOADED, - CreatedAtMs: 1700000000000, - LoadedAtMs: 1700000001000, + CreatedTime: testTimestamp(1700000000000), + LoadedTime: testTimestamp(1700000001000), }, ActiveVersion: 3, } @@ -758,14 +764,14 @@ func TestPolicyList(t *testing.T) { Version: 1, PolicyHash: "sha256:v1", Status: pb.PolicyStatus_POLICY_STATUS_SUPERSEDED, - CreatedAtMs: 1700000000000, + CreatedTime: testTimestamp(1700000000000), }, { Version: 2, PolicyHash: "sha256:v2", Status: pb.PolicyStatus_POLICY_STATUS_LOADED, - CreatedAtMs: 1700000001000, - LoadedAtMs: 1700000002000, + CreatedTime: testTimestamp(1700000001000), + LoadedTime: testTimestamp(1700000002000), }, }, } diff --git a/sdk/go/openshell/v1/provider_client.go b/sdk/go/openshell/v1/provider_client.go index 19784b6346..c47efadd5a 100644 --- a/sdk/go/openshell/v1/provider_client.go +++ b/sdk/go/openshell/v1/provider_client.go @@ -90,7 +90,7 @@ func (p *providerClient) Update(ctx context.Context, workspace string, provider Workspace: workspace, } if proto != nil { - req.CredentialExpiresAtMs = proto.CredentialExpiresAtMs + req.CredentialExpirationTimes = proto.CredentialExpirationTimes } resp, err := p.client.UpdateProvider(ctx, req) diff --git a/sdk/go/openshell/v1/refresh_client_test.go b/sdk/go/openshell/v1/refresh_client_test.go index 7c69cb093a..013de70b85 100644 --- a/sdk/go/openshell/v1/refresh_client_test.go +++ b/sdk/go/openshell/v1/refresh_client_test.go @@ -18,6 +18,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/types/known/timestamppb" ) // --- Mock server for credential refresh --- @@ -75,12 +76,12 @@ func (s *mockRefreshServer) ConfigureProviderRefresh(_ context.Context, req *pb. } st := &pb.ProviderCredentialRefreshStatus{ - ProviderName: req.GetProvider(), - ProviderId: "prov-id-" + req.GetProvider(), - CredentialKey: req.GetCredentialKey(), - Strategy: req.GetStrategy(), - Status: "active", - ExpiresAtMs: req.GetExpiresAtMs(), + ProviderName: req.GetProvider(), + ProviderId: "prov-id-" + req.GetProvider(), + CredentialKey: req.GetCredentialKey(), + Strategy: req.GetStrategy(), + Status: "active", + ExpirationTime: req.GetExpirationTime(), } s.statuses[refreshKey(req.GetProvider(), req.GetCredentialKey())] = st return &pb.ConfigureProviderRefreshResponse{Status: st}, nil @@ -99,7 +100,7 @@ func (s *mockRefreshServer) RotateProviderCredential(_ context.Context, req *pb. return nil, status.Errorf(codes.NotFound, "refresh config %q not found", key) } st.Status = "rotated" - st.LastRefreshAtMs = time.Now().UnixMilli() + st.LastRefreshTime = timestamppb.Now() return &pb.RotateProviderCredentialResponse{Status: st}, nil } diff --git a/sdk/go/openshell/v1/sandbox_client.go b/sdk/go/openshell/v1/sandbox_client.go index f31cd40af9..7fa1fb4383 100644 --- a/sdk/go/openshell/v1/sandbox_client.go +++ b/sdk/go/openshell/v1/sandbox_client.go @@ -327,7 +327,7 @@ func (s *sandboxClient) GetLogs(ctx context.Context, workspace, sandboxName stri Workspace: workspace, } if !cfg.Since().IsZero() { - req.SinceMs = converter.MillisFromTime(cfg.Since()) + req.SinceTime = converter.TimestampFromTime(cfg.Since()) } resp, err := s.client.GetSandboxLogs(ctx, req) diff --git a/sdk/go/openshell/v1/sandbox_client_test.go b/sdk/go/openshell/v1/sandbox_client_test.go index 91e80db148..2976eb0b90 100644 --- a/sdk/go/openshell/v1/sandbox_client_test.go +++ b/sdk/go/openshell/v1/sandbox_client_test.go @@ -20,6 +20,7 @@ import ( "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) type mockSandboxServer struct { @@ -63,7 +64,7 @@ func (s *mockSandboxServer) CreateSandbox(_ context.Context, req *pb.CreateSandb Metadata: &dm.ObjectMeta{ Id: "sb-" + req.GetName(), Name: req.GetName(), - CreatedAtMs: 1700000000000, + CreatedTime: timestamppb.New(time.UnixMilli(1700000000000)), Labels: req.GetLabels(), ResourceVersion: 1, }, @@ -1073,8 +1074,8 @@ func TestSandboxGetLogs(t *testing.T) { } mock.getLogsResp = &pb.GetSandboxLogsResponse{ Logs: []*pb.SandboxLogLine{ - {TimestampMs: 1700000000000, Level: "INFO", Target: "gateway", Message: "connected", Source: "gateway"}, - {TimestampMs: 1700000001000, Level: "DEBUG", Target: "sandbox", Message: "init done", Source: "sandbox"}, + {EventTime: timestamppb.New(time.UnixMilli(1700000000000)), Level: "INFO", Target: "gateway", Message: "connected", Source: "gateway"}, + {EventTime: timestamppb.New(time.UnixMilli(1700000001000)), Level: "DEBUG", Target: "sandbox", Message: "init done", Source: "sandbox"}, }, BufferTotal: 42, } @@ -1106,7 +1107,7 @@ func TestSandboxGetLogs_WithOptions(t *testing.T) { Status: &pb.SandboxStatus{Phase: pb.SandboxPhase_SANDBOX_PHASE_READY}, } mock.getLogsResp = &pb.GetSandboxLogsResponse{ - Logs: []*pb.SandboxLogLine{{TimestampMs: 1700000000000, Level: "WARN", Message: "high cpu"}}, + Logs: []*pb.SandboxLogLine{{EventTime: timestamppb.New(time.UnixMilli(1700000000000)), Level: "WARN", Message: "high cpu"}}, BufferTotal: 100, } client, cleanup := setupSandboxTest(t, mock) @@ -1131,7 +1132,7 @@ func TestSandboxGetLogs_WithOptions(t *testing.T) { mock.mu.Unlock() assert.Equal(t, "sb-id-opts", req.GetSandboxId()) assert.Equal(t, uint32(50), req.GetLines()) - assert.Equal(t, since.UnixMilli(), req.GetSinceMs()) + assert.Equal(t, since, req.GetSinceTime().AsTime()) assert.Equal(t, []string{"gateway", "sandbox"}, req.GetSources()) assert.Equal(t, "WARN", req.GetMinLevel()) } @@ -1193,11 +1194,11 @@ func TestSandboxGetLogs_SinceZeroNotSent(t *testing.T) { client, cleanup := setupSandboxTest(t, mock) defer cleanup() - // Call without WithLogSince — SinceMs should be 0 (not set) + // Call without WithLogSince — SinceTime should be unset. _, err := client.GetLogs(context.Background(), "default", "zero-sb") require.NoError(t, err) mock.mu.Lock() - assert.Equal(t, int64(0), mock.getLogsRequest.GetSinceMs()) + assert.Nil(t, mock.getLogsRequest.GetSinceTime()) mock.mu.Unlock() } diff --git a/sdk/go/openshell/v1/ssh_client_test.go b/sdk/go/openshell/v1/ssh_client_test.go index 5b600b3370..2d20a7b6a2 100644 --- a/sdk/go/openshell/v1/ssh_client_test.go +++ b/sdk/go/openshell/v1/ssh_client_test.go @@ -19,6 +19,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/types/known/timestamppb" ) // --- Mock server for SSH sessions --- @@ -63,7 +64,7 @@ func (s *mockSSHServer) CreateSshSession(_ context.Context, req *pb.CreateSshSes GatewayPort: 2222, GatewayScheme: "https", HostKeyFingerprint: "SHA256:abc123", - ExpiresAtMs: 1700000000000, + ExpirationTime: timestamppb.New(time.UnixMilli(1700000000000)), } s.sessions[req.GetSandboxId()] = resp s.tokens[token] = true diff --git a/sdk/go/openshell/v1/workspace_test.go b/sdk/go/openshell/v1/workspace_test.go index f64a76e998..32ff4a856e 100644 --- a/sdk/go/openshell/v1/workspace_test.go +++ b/sdk/go/openshell/v1/workspace_test.go @@ -7,6 +7,7 @@ import ( "context" "net" "testing" + "time" dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" @@ -17,6 +18,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/types/known/timestamppb" ) type mockWorkspaceServer struct { @@ -118,7 +120,7 @@ func testWorkspace() *dm.Workspace { Metadata: &dm.ObjectMeta{ Id: "ws-1", Name: "test-ws", - CreatedAtMs: 1700000000000, + CreatedTime: timestamppb.New(time.UnixMilli(1700000000000)), Labels: map[string]string{"team": "platform"}, ResourceVersion: 1, }, @@ -296,7 +298,7 @@ func testMember() *pb.WorkspaceMember { Metadata: &dm.ObjectMeta{ Id: "mem-1", Name: "member-auto", - CreatedAtMs: 1700000000000, + CreatedTime: timestamppb.New(time.UnixMilli(1700000000000)), ResourceVersion: 1, }, PrincipalSubject: "user@example.com", diff --git a/sdk/go/proto/datamodelv1/datamodel.pb.go b/sdk/go/proto/datamodelv1/datamodel.pb.go index a672bf3d8b..14aed14d33 100644 --- a/sdk/go/proto/datamodelv1/datamodel.pb.go +++ b/sdk/go/proto/datamodelv1/datamodel.pb.go @@ -13,6 +13,7 @@ import ( _ "github.com/NVIDIA/OpenShell/sdk/go/proto/optionsv1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -86,8 +87,8 @@ type ObjectMeta struct { Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // Human-readable object name (unique per object type). Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - // Milliseconds since Unix epoch when the object was created. - CreatedAtMs int64 `protobuf:"varint,3,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` + // Time when the object was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,103,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` // Key-value labels for filtering and organization. // Labels must follow Kubernetes conventions: alphanumeric + `-._/`, max 63 chars per segment. Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` @@ -100,12 +101,12 @@ type ObjectMeta struct { // Workspace that owns this resource. Empty is normalized to "default" by the // gateway. Immutable after creation. Workspace string `protobuf:"bytes,7,opt,name=workspace,proto3" json:"workspace,omitempty"` - // Milliseconds since Unix epoch when graceful deletion was initiated. - // Zero means the object is not being deleted. Once set, this field is + // Time when graceful deletion was initiated. Absence means the object is + // not being deleted. Once set, this field is // immutable — the only path forward is completing deletion. - DeletionTimestampMs int64 `protobuf:"varint,8,opt,name=deletion_timestamp_ms,json=deletionTimestampMs,proto3" json:"deletion_timestamp_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + DeletionTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=deletion_time,json=deletionTime,proto3" json:"deletion_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ObjectMeta) Reset() { @@ -152,11 +153,11 @@ func (x *ObjectMeta) GetName() string { return "" } -func (x *ObjectMeta) GetCreatedAtMs() int64 { +func (x *ObjectMeta) GetCreatedTime() *timestamppb.Timestamp { if x != nil { - return x.CreatedAtMs + return x.CreatedTime } - return 0 + return nil } func (x *ObjectMeta) GetLabels() map[string]string { @@ -187,11 +188,11 @@ func (x *ObjectMeta) GetWorkspace() string { return "" } -func (x *ObjectMeta) GetDeletionTimestampMs() int64 { +func (x *ObjectMeta) GetDeletionTime() *timestamppb.Timestamp { if x != nil { - return x.DeletionTimestampMs + return x.DeletionTime } - return 0 + return nil } // Status of a workspace. @@ -373,9 +374,9 @@ type Provider struct { Credentials map[string]string `protobuf:"bytes,3,rep,name=credentials,proto3" json:"credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Non-secret provider configuration. Config map[string]string `protobuf:"bytes,4,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Expiration timestamps for credential values, keyed by credential/env var - // name. A zero or missing value means the credential does not expire. - CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,5,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + // Expiration times for credential values, keyed by credential/env var name. + // A missing key means the credential does not expire. + CredentialExpirationTimes map[string]*timestamppb.Timestamp `protobuf:"bytes,105,rep,name=credential_expiration_times,json=credentialExpirationTimes,proto3" json:"credential_expiration_times,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Workspace where this provider's type profile is stored. // Empty string = platform/global scope. Must be empty or match // metadata.workspace; cross-workspace references are rejected. @@ -445,9 +446,9 @@ func (x *Provider) GetConfig() map[string]string { return nil } -func (x *Provider) GetCredentialExpiresAtMs() map[string]int64 { +func (x *Provider) GetCredentialExpirationTimes() map[string]*timestamppb.Timestamp { if x != nil { - return x.CredentialExpiresAtMs + return x.CredentialExpirationTimes } return nil } @@ -470,23 +471,23 @@ var File_datamodel_proto protoreflect.FileDescriptor const file_datamodel_proto_rawDesc = "" + "\n" + - "\x0fdatamodel.proto\x12\x16openshell.datamodel.v1\x1a\roptions.proto\"\xeb\x03\n" + + "\x0fdatamodel.proto\x12\x16openshell.datamodel.v1\x1a\roptions.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc5\x04\n" + "\n" + "ObjectMeta\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x12\"\n" + - "\rcreated_at_ms\x18\x03 \x01(\x03R\vcreatedAtMs\x12F\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12=\n" + + "\fcreated_time\x18g \x01(\v2\x1a.google.protobuf.TimestampR\vcreatedTime\x12F\n" + "\x06labels\x18\x04 \x03(\v2..openshell.datamodel.v1.ObjectMeta.LabelsEntryR\x06labels\x12)\n" + "\x10resource_version\x18\x05 \x01(\x04R\x0fresourceVersion\x12U\n" + "\vannotations\x18\x06 \x03(\v23.openshell.datamodel.v1.ObjectMeta.AnnotationsEntryR\vannotations\x12\x1c\n" + - "\tworkspace\x18\a \x01(\tR\tworkspace\x122\n" + - "\x15deletion_timestamp_ms\x18\b \x01(\x03R\x13deletionTimestampMs\x1a9\n" + + "\tworkspace\x18\a \x01(\tR\tworkspace\x12?\n" + + "\rdeletion_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\fdeletionTime\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"O\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x03\x10\x04J\x04\b\b\x10\tR\rcreated_at_msR\x15deletion_timestamp_ms\"O\n" + "\x0fWorkspaceStatus\x12<\n" + "\x05phase\x18\x01 \x01(\x0e2&.openshell.datamodel.v1.WorkspacePhaseR\x05phase\"\x8c\x01\n" + "\tWorkspace\x12>\n" + @@ -498,13 +499,13 @@ const file_datamodel_proto_rawDesc = "" + "\bmetadata\x18\x03 \x03(\v26.openshell.datamodel.v1.CredentialHandle.MetadataEntryR\bmetadata\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbf\x06\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x8a\a\n" + "\bProvider\x12>\n" + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x12\n" + "\x04type\x18\x02 \x01(\tR\x04type\x12Y\n" + "\vcredentials\x18\x03 \x03(\v21.openshell.datamodel.v1.Provider.CredentialsEntryB\x04\x88\xb5\x18\x01R\vcredentials\x12D\n" + - "\x06config\x18\x04 \x03(\v2,.openshell.datamodel.v1.Provider.ConfigEntryR\x06config\x12t\n" + - "\x18credential_expires_at_ms\x18\x05 \x03(\v2;.openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntryR\x15credentialExpiresAtMs\x12+\n" + + "\x06config\x18\x04 \x03(\v2,.openshell.datamodel.v1.Provider.ConfigEntryR\x06config\x12\x7f\n" + + "\x1bcredential_expiration_times\x18i \x03(\v2?.openshell.datamodel.v1.Provider.CredentialExpirationTimesEntryR\x19credentialExpirationTimes\x12+\n" + "\x11profile_workspace\x18\x06 \x01(\tR\x10profileWorkspace\x12f\n" + "\x12credential_handles\x18\a \x03(\v27.openshell.datamodel.v1.Provider.CredentialHandlesEntryR\x11credentialHandles\x1a>\n" + "\x10CredentialsEntry\x12\x10\n" + @@ -512,13 +513,13 @@ const file_datamodel_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a9\n" + "\vConfigEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aH\n" + - "\x1aCredentialExpiresAtMsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\x1an\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1ah\n" + + "\x1eCredentialExpirationTimesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + + "\x05value\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05value:\x028\x01\x1an\n" + "\x16CredentialHandlesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12>\n" + - "\x05value\x18\x02 \x01(\v2(.openshell.datamodel.v1.CredentialHandleR\x05value:\x028\x01*n\n" + + "\x05value\x18\x02 \x01(\v2(.openshell.datamodel.v1.CredentialHandleR\x05value:\x028\x01J\x04\b\x05\x10\x06R\x18credential_expires_at_ms*n\n" + "\x0eWorkspacePhase\x12\x1f\n" + "\x1bWORKSPACE_PHASE_UNSPECIFIED\x10\x00\x12\x1a\n" + "\x16WORKSPACE_PHASE_ACTIVE\x10\x01\x12\x1f\n" + @@ -539,38 +540,42 @@ func file_datamodel_proto_rawDescGZIP() []byte { var file_datamodel_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_datamodel_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_datamodel_proto_goTypes = []any{ - (WorkspacePhase)(0), // 0: openshell.datamodel.v1.WorkspacePhase - (*ObjectMeta)(nil), // 1: openshell.datamodel.v1.ObjectMeta - (*WorkspaceStatus)(nil), // 2: openshell.datamodel.v1.WorkspaceStatus - (*Workspace)(nil), // 3: openshell.datamodel.v1.Workspace - (*CredentialHandle)(nil), // 4: openshell.datamodel.v1.CredentialHandle - (*Provider)(nil), // 5: openshell.datamodel.v1.Provider - nil, // 6: openshell.datamodel.v1.ObjectMeta.LabelsEntry - nil, // 7: openshell.datamodel.v1.ObjectMeta.AnnotationsEntry - nil, // 8: openshell.datamodel.v1.CredentialHandle.MetadataEntry - nil, // 9: openshell.datamodel.v1.Provider.CredentialsEntry - nil, // 10: openshell.datamodel.v1.Provider.ConfigEntry - nil, // 11: openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry - nil, // 12: openshell.datamodel.v1.Provider.CredentialHandlesEntry + (WorkspacePhase)(0), // 0: openshell.datamodel.v1.WorkspacePhase + (*ObjectMeta)(nil), // 1: openshell.datamodel.v1.ObjectMeta + (*WorkspaceStatus)(nil), // 2: openshell.datamodel.v1.WorkspaceStatus + (*Workspace)(nil), // 3: openshell.datamodel.v1.Workspace + (*CredentialHandle)(nil), // 4: openshell.datamodel.v1.CredentialHandle + (*Provider)(nil), // 5: openshell.datamodel.v1.Provider + nil, // 6: openshell.datamodel.v1.ObjectMeta.LabelsEntry + nil, // 7: openshell.datamodel.v1.ObjectMeta.AnnotationsEntry + nil, // 8: openshell.datamodel.v1.CredentialHandle.MetadataEntry + nil, // 9: openshell.datamodel.v1.Provider.CredentialsEntry + nil, // 10: openshell.datamodel.v1.Provider.ConfigEntry + nil, // 11: openshell.datamodel.v1.Provider.CredentialExpirationTimesEntry + nil, // 12: openshell.datamodel.v1.Provider.CredentialHandlesEntry + (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp } var file_datamodel_proto_depIdxs = []int32{ - 6, // 0: openshell.datamodel.v1.ObjectMeta.labels:type_name -> openshell.datamodel.v1.ObjectMeta.LabelsEntry - 7, // 1: openshell.datamodel.v1.ObjectMeta.annotations:type_name -> openshell.datamodel.v1.ObjectMeta.AnnotationsEntry - 0, // 2: openshell.datamodel.v1.WorkspaceStatus.phase:type_name -> openshell.datamodel.v1.WorkspacePhase - 1, // 3: openshell.datamodel.v1.Workspace.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 2, // 4: openshell.datamodel.v1.Workspace.status:type_name -> openshell.datamodel.v1.WorkspaceStatus - 8, // 5: openshell.datamodel.v1.CredentialHandle.metadata:type_name -> openshell.datamodel.v1.CredentialHandle.MetadataEntry - 1, // 6: openshell.datamodel.v1.Provider.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 9, // 7: openshell.datamodel.v1.Provider.credentials:type_name -> openshell.datamodel.v1.Provider.CredentialsEntry - 10, // 8: openshell.datamodel.v1.Provider.config:type_name -> openshell.datamodel.v1.Provider.ConfigEntry - 11, // 9: openshell.datamodel.v1.Provider.credential_expires_at_ms:type_name -> openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry - 12, // 10: openshell.datamodel.v1.Provider.credential_handles:type_name -> openshell.datamodel.v1.Provider.CredentialHandlesEntry - 4, // 11: openshell.datamodel.v1.Provider.CredentialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 13, // 0: openshell.datamodel.v1.ObjectMeta.created_time:type_name -> google.protobuf.Timestamp + 6, // 1: openshell.datamodel.v1.ObjectMeta.labels:type_name -> openshell.datamodel.v1.ObjectMeta.LabelsEntry + 7, // 2: openshell.datamodel.v1.ObjectMeta.annotations:type_name -> openshell.datamodel.v1.ObjectMeta.AnnotationsEntry + 13, // 3: openshell.datamodel.v1.ObjectMeta.deletion_time:type_name -> google.protobuf.Timestamp + 0, // 4: openshell.datamodel.v1.WorkspaceStatus.phase:type_name -> openshell.datamodel.v1.WorkspacePhase + 1, // 5: openshell.datamodel.v1.Workspace.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 2, // 6: openshell.datamodel.v1.Workspace.status:type_name -> openshell.datamodel.v1.WorkspaceStatus + 8, // 7: openshell.datamodel.v1.CredentialHandle.metadata:type_name -> openshell.datamodel.v1.CredentialHandle.MetadataEntry + 1, // 8: openshell.datamodel.v1.Provider.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 9, // 9: openshell.datamodel.v1.Provider.credentials:type_name -> openshell.datamodel.v1.Provider.CredentialsEntry + 10, // 10: openshell.datamodel.v1.Provider.config:type_name -> openshell.datamodel.v1.Provider.ConfigEntry + 11, // 11: openshell.datamodel.v1.Provider.credential_expiration_times:type_name -> openshell.datamodel.v1.Provider.CredentialExpirationTimesEntry + 12, // 12: openshell.datamodel.v1.Provider.credential_handles:type_name -> openshell.datamodel.v1.Provider.CredentialHandlesEntry + 13, // 13: openshell.datamodel.v1.Provider.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 4, // 14: openshell.datamodel.v1.Provider.CredentialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 15, // [15:15] is the sub-list for method output_type + 15, // [15:15] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_datamodel_proto_init() } diff --git a/sdk/go/proto/inferencev1/inference.pb.go b/sdk/go/proto/inferencev1/inference.pb.go index decc6c4f39..f728a3dd53 100644 --- a/sdk/go/proto/inferencev1/inference.pb.go +++ b/sdk/go/proto/inferencev1/inference.pb.go @@ -14,6 +14,8 @@ import ( _ "github.com/NVIDIA/OpenShell/sdk/go/proto/optionsv1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -36,10 +38,10 @@ type InferenceRouteConfig struct { ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` // Model identifier to force on generation calls. ModelId string `protobuf:"bytes,2,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` - // Per-route request timeout in seconds. 0 means use default (60s). - TimeoutSecs uint64 `protobuf:"varint,3,opt,name=timeout_secs,json=timeoutSecs,proto3" json:"timeout_secs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Per-route request timeout. Absence means use the default (60s). + RequestTimeout *durationpb.Duration `protobuf:"bytes,103,opt,name=request_timeout,json=requestTimeout,proto3" json:"request_timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *InferenceRouteConfig) Reset() { @@ -86,11 +88,11 @@ func (x *InferenceRouteConfig) GetModelId() string { return "" } -func (x *InferenceRouteConfig) GetTimeoutSecs() uint64 { +func (x *InferenceRouteConfig) GetRequestTimeout() *durationpb.Duration { if x != nil { - return x.TimeoutSecs + return x.RequestTimeout } - return 0 + return nil } // Storage envelope for a workspace-scoped inference route. @@ -168,8 +170,8 @@ type SetInferenceRouteRequest struct { Verify bool `protobuf:"varint,4,opt,name=verify,proto3" json:"verify,omitempty"` // Skip synchronous endpoint validation before persistence. NoVerify bool `protobuf:"varint,5,opt,name=no_verify,json=noVerify,proto3" json:"no_verify,omitempty"` - // Per-route request timeout in seconds. 0 means use default (60s). - TimeoutSecs uint64 `protobuf:"varint,6,opt,name=timeout_secs,json=timeoutSecs,proto3" json:"timeout_secs,omitempty"` + // Per-route request timeout. Absence means use the default (60s). + RequestTimeout *durationpb.Duration `protobuf:"bytes,106,opt,name=request_timeout,json=requestTimeout,proto3" json:"request_timeout,omitempty"` // Target workspace. Empty string defaults to "default". Workspace string `protobuf:"bytes,7,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields @@ -241,11 +243,11 @@ func (x *SetInferenceRouteRequest) GetNoVerify() bool { return false } -func (x *SetInferenceRouteRequest) GetTimeoutSecs() uint64 { +func (x *SetInferenceRouteRequest) GetRequestTimeout() *durationpb.Duration { if x != nil { - return x.TimeoutSecs + return x.RequestTimeout } - return 0 + return nil } func (x *SetInferenceRouteRequest) GetWorkspace() string { @@ -318,8 +320,8 @@ type SetInferenceRouteResponse struct { ValidationPerformed bool `protobuf:"varint,5,opt,name=validation_performed,json=validationPerformed,proto3" json:"validation_performed,omitempty"` // The concrete endpoints that were probed during validation, when available. ValidatedEndpoints []*ValidatedEndpoint `protobuf:"bytes,6,rep,name=validated_endpoints,json=validatedEndpoints,proto3" json:"validated_endpoints,omitempty"` - // Per-route request timeout in seconds that was persisted. - TimeoutSecs uint64 `protobuf:"varint,7,opt,name=timeout_secs,json=timeoutSecs,proto3" json:"timeout_secs,omitempty"` + // Per-route request timeout that was persisted. + RequestTimeout *durationpb.Duration `protobuf:"bytes,107,opt,name=request_timeout,json=requestTimeout,proto3" json:"request_timeout,omitempty"` // Workspace the route was configured in. Workspace string `protobuf:"bytes,8,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields @@ -398,11 +400,11 @@ func (x *SetInferenceRouteResponse) GetValidatedEndpoints() []*ValidatedEndpoint return nil } -func (x *SetInferenceRouteResponse) GetTimeoutSecs() uint64 { +func (x *SetInferenceRouteResponse) GetRequestTimeout() *durationpb.Duration { if x != nil { - return x.TimeoutSecs + return x.RequestTimeout } - return 0 + return nil } func (x *SetInferenceRouteResponse) GetWorkspace() string { @@ -474,8 +476,8 @@ type GetInferenceRouteResponse struct { Version uint64 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` // Route name that was queried. RouteName string `protobuf:"bytes,4,opt,name=route_name,json=routeName,proto3" json:"route_name,omitempty"` - // Per-route request timeout in seconds. 0 means default (60s). - TimeoutSecs uint64 `protobuf:"varint,5,opt,name=timeout_secs,json=timeoutSecs,proto3" json:"timeout_secs,omitempty"` + // Per-route request timeout. Absence means the default (60s). + RequestTimeout *durationpb.Duration `protobuf:"bytes,105,opt,name=request_timeout,json=requestTimeout,proto3" json:"request_timeout,omitempty"` // Workspace the route belongs to. Workspace string `protobuf:"bytes,6,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields @@ -540,11 +542,11 @@ func (x *GetInferenceRouteResponse) GetRouteName() string { return "" } -func (x *GetInferenceRouteResponse) GetTimeoutSecs() uint64 { +func (x *GetInferenceRouteResponse) GetRequestTimeout() *durationpb.Duration { if x != nil { - return x.TimeoutSecs + return x.RequestTimeout } - return 0 + return nil } func (x *GetInferenceRouteResponse) GetWorkspace() string { @@ -699,8 +701,8 @@ type ResolvedRoute struct { ApiKey string `protobuf:"bytes,4,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` ModelId string `protobuf:"bytes,5,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` ProviderType string `protobuf:"bytes,6,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"` - // Per-route request timeout in seconds. 0 means use default (60s). - TimeoutSecs uint64 `protobuf:"varint,7,opt,name=timeout_secs,json=timeoutSecs,proto3" json:"timeout_secs,omitempty"` + // Per-route request timeout. Absence means use the default (60s). + RequestTimeout *durationpb.Duration `protobuf:"bytes,107,opt,name=request_timeout,json=requestTimeout,proto3" json:"request_timeout,omitempty"` // When true, the model identifier is embedded in the URL path (e.g. Vertex AI). ModelInPath bool `protobuf:"varint,8,opt,name=model_in_path,json=modelInPath,proto3" json:"model_in_path,omitempty"` // Optional override for the request path. When set, replaces the protocol-derived path. @@ -782,11 +784,11 @@ func (x *ResolvedRoute) GetProviderType() string { return "" } -func (x *ResolvedRoute) GetTimeoutSecs() uint64 { +func (x *ResolvedRoute) GetRequestTimeout() *durationpb.Duration { if x != nil { - return x.TimeoutSecs + return x.RequestTimeout } - return 0 + return nil } func (x *ResolvedRoute) GetModelInPath() bool { @@ -808,8 +810,8 @@ type GetInferenceBundleResponse struct { Routes []*ResolvedRoute `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` // Opaque revision tag for cache freshness checks. Revision string `protobuf:"bytes,2,opt,name=revision,proto3" json:"revision,omitempty"` - // Timestamp (epoch ms) when this bundle was generated. - GeneratedAtMs int64 `protobuf:"varint,3,opt,name=generated_at_ms,json=generatedAtMs,proto3" json:"generated_at_ms,omitempty"` + // Time when this bundle was generated. + GeneratedTime *timestamppb.Timestamp `protobuf:"bytes,103,opt,name=generated_time,json=generatedTime,proto3" json:"generated_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -858,38 +860,38 @@ func (x *GetInferenceBundleResponse) GetRevision() string { return "" } -func (x *GetInferenceBundleResponse) GetGeneratedAtMs() int64 { +func (x *GetInferenceBundleResponse) GetGeneratedTime() *timestamppb.Timestamp { if x != nil { - return x.GeneratedAtMs + return x.GeneratedTime } - return 0 + return nil } var File_inference_proto protoreflect.FileDescriptor const file_inference_proto_rawDesc = "" + "\n" + - "\x0finference.proto\x12\x16openshell.inference.v1\x1a\x0fdatamodel.proto\x1a\roptions.proto\"y\n" + + "\x0finference.proto\x12\x16openshell.inference.v1\x1a\x0fdatamodel.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\roptions.proto\"\xae\x01\n" + "\x14InferenceRouteConfig\x12#\n" + "\rprovider_name\x18\x01 \x01(\tR\fproviderName\x12\x19\n" + - "\bmodel_id\x18\x02 \x01(\tR\amodelId\x12!\n" + - "\ftimeout_secs\x18\x03 \x01(\x04R\vtimeoutSecs\"\xb0\x01\n" + + "\bmodel_id\x18\x02 \x01(\tR\amodelId\x12B\n" + + "\x0frequest_timeout\x18g \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeoutJ\x04\b\x03\x10\x04R\ftimeout_secs\"\xb0\x01\n" + "\x0eInferenceRoute\x12>\n" + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12D\n" + "\x06config\x18\x02 \x01(\v2,.openshell.inference.v1.InferenceRouteConfigR\x06config\x12\x18\n" + - "\aversion\x18\x03 \x01(\x04R\aversion\"\xef\x01\n" + + "\aversion\x18\x03 \x01(\x04R\aversion\"\xa4\x02\n" + "\x18SetInferenceRouteRequest\x12#\n" + "\rprovider_name\x18\x01 \x01(\tR\fproviderName\x12\x19\n" + "\bmodel_id\x18\x02 \x01(\tR\amodelId\x12\x1d\n" + "\n" + "route_name\x18\x03 \x01(\tR\trouteName\x12\x16\n" + "\x06verify\x18\x04 \x01(\bR\x06verify\x12\x1b\n" + - "\tno_verify\x18\x05 \x01(\bR\bnoVerify\x12!\n" + - "\ftimeout_secs\x18\x06 \x01(\x04R\vtimeoutSecs\x12\x1c\n" + - "\tworkspace\x18\a \x01(\tR\tworkspace\"A\n" + + "\tno_verify\x18\x05 \x01(\bR\bnoVerify\x12B\n" + + "\x0frequest_timeout\x18j \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeout\x12\x1c\n" + + "\tworkspace\x18\a \x01(\tR\tworkspaceJ\x04\b\x06\x10\aR\ftimeout_secs\"A\n" + "\x11ValidatedEndpoint\x12\x10\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n" + - "\bprotocol\x18\x02 \x01(\tR\bprotocol\"\xe4\x02\n" + + "\bprotocol\x18\x02 \x01(\tR\bprotocol\"\x99\x03\n" + "\x19SetInferenceRouteResponse\x12#\n" + "\rprovider_name\x18\x01 \x01(\tR\fproviderName\x12\x19\n" + "\bmodel_id\x18\x02 \x01(\tR\amodelId\x12\x18\n" + @@ -897,43 +899,43 @@ const file_inference_proto_rawDesc = "" + "\n" + "route_name\x18\x04 \x01(\tR\trouteName\x121\n" + "\x14validation_performed\x18\x05 \x01(\bR\x13validationPerformed\x12Z\n" + - "\x13validated_endpoints\x18\x06 \x03(\v2).openshell.inference.v1.ValidatedEndpointR\x12validatedEndpoints\x12!\n" + - "\ftimeout_secs\x18\a \x01(\x04R\vtimeoutSecs\x12\x1c\n" + - "\tworkspace\x18\b \x01(\tR\tworkspace\"W\n" + + "\x13validated_endpoints\x18\x06 \x03(\v2).openshell.inference.v1.ValidatedEndpointR\x12validatedEndpoints\x12B\n" + + "\x0frequest_timeout\x18k \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeout\x12\x1c\n" + + "\tworkspace\x18\b \x01(\tR\tworkspaceJ\x04\b\a\x10\bR\ftimeout_secs\"W\n" + "\x18GetInferenceRouteRequest\x12\x1d\n" + "\n" + "route_name\x18\x01 \x01(\tR\trouteName\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\xd5\x01\n" + + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\x8a\x02\n" + "\x19GetInferenceRouteResponse\x12#\n" + "\rprovider_name\x18\x01 \x01(\tR\fproviderName\x12\x19\n" + "\bmodel_id\x18\x02 \x01(\tR\amodelId\x12\x18\n" + "\aversion\x18\x03 \x01(\x04R\aversion\x12\x1d\n" + "\n" + - "route_name\x18\x04 \x01(\tR\trouteName\x12!\n" + - "\ftimeout_secs\x18\x05 \x01(\x04R\vtimeoutSecs\x12\x1c\n" + - "\tworkspace\x18\x06 \x01(\tR\tworkspace\"Z\n" + + "route_name\x18\x04 \x01(\tR\trouteName\x12B\n" + + "\x0frequest_timeout\x18i \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeout\x12\x1c\n" + + "\tworkspace\x18\x06 \x01(\tR\tworkspaceJ\x04\b\x05\x10\x06R\ftimeout_secs\"Z\n" + "\x1bDeleteInferenceRouteRequest\x12\x1d\n" + "\n" + "route_name\x18\x01 \x01(\tR\trouteName\x12\x1c\n" + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"8\n" + "\x1cDeleteInferenceRouteResponse\x12\x18\n" + "\adeleted\x18\x01 \x01(\bR\adeleted\"\x1b\n" + - "\x19GetInferenceBundleRequest\"\xd5\x02\n" + + "\x19GetInferenceBundleRequest\"\x8a\x03\n" + "\rResolvedRoute\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\bbase_url\x18\x02 \x01(\tR\abaseUrl\x12\x1c\n" + "\tprotocols\x18\x03 \x03(\tR\tprotocols\x12\x1d\n" + "\aapi_key\x18\x04 \x01(\tB\x04\x88\xb5\x18\x01R\x06apiKey\x12\x19\n" + "\bmodel_id\x18\x05 \x01(\tR\amodelId\x12#\n" + - "\rprovider_type\x18\x06 \x01(\tR\fproviderType\x12!\n" + - "\ftimeout_secs\x18\a \x01(\x04R\vtimeoutSecs\x12\"\n" + + "\rprovider_type\x18\x06 \x01(\tR\fproviderType\x12B\n" + + "\x0frequest_timeout\x18k \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeout\x12\"\n" + "\rmodel_in_path\x18\b \x01(\bR\vmodelInPath\x127\n" + "\x15request_path_override\x18\t \x01(\tH\x00R\x13requestPathOverride\x88\x01\x01B\x18\n" + - "\x16_request_path_override\"\x9f\x01\n" + + "\x16_request_path_overrideJ\x04\b\a\x10\bR\ftimeout_secs\"\xd1\x01\n" + "\x1aGetInferenceBundleResponse\x12=\n" + "\x06routes\x18\x01 \x03(\v2%.openshell.inference.v1.ResolvedRouteR\x06routes\x12\x1a\n" + - "\brevision\x18\x02 \x01(\tR\brevision\x12&\n" + - "\x0fgenerated_at_ms\x18\x03 \x01(\x03R\rgeneratedAtMs2\x82\x05\n" + + "\brevision\x18\x02 \x01(\tR\brevision\x12A\n" + + "\x0egenerated_time\x18g \x01(\v2\x1a.google.protobuf.TimestampR\rgeneratedTimeJ\x04\b\x03\x10\x04R\x0fgenerated_at_ms2\x82\x05\n" + "\tInference\x12\x8a\x01\n" + "\x12GetInferenceBundle\x121.openshell.inference.v1.GetInferenceBundleRequest\x1a2.openshell.inference.v1.GetInferenceBundleResponse\"\r\x82\xb5\x18\t\n" + "\asandbox\x12\x9e\x01\n" + @@ -970,26 +972,34 @@ var file_inference_proto_goTypes = []any{ (*GetInferenceBundleRequest)(nil), // 9: openshell.inference.v1.GetInferenceBundleRequest (*ResolvedRoute)(nil), // 10: openshell.inference.v1.ResolvedRoute (*GetInferenceBundleResponse)(nil), // 11: openshell.inference.v1.GetInferenceBundleResponse - (*datamodelv1.ObjectMeta)(nil), // 12: openshell.datamodel.v1.ObjectMeta + (*durationpb.Duration)(nil), // 12: google.protobuf.Duration + (*datamodelv1.ObjectMeta)(nil), // 13: openshell.datamodel.v1.ObjectMeta + (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp } var file_inference_proto_depIdxs = []int32{ - 12, // 0: openshell.inference.v1.InferenceRoute.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 0, // 1: openshell.inference.v1.InferenceRoute.config:type_name -> openshell.inference.v1.InferenceRouteConfig - 3, // 2: openshell.inference.v1.SetInferenceRouteResponse.validated_endpoints:type_name -> openshell.inference.v1.ValidatedEndpoint - 10, // 3: openshell.inference.v1.GetInferenceBundleResponse.routes:type_name -> openshell.inference.v1.ResolvedRoute - 9, // 4: openshell.inference.v1.Inference.GetInferenceBundle:input_type -> openshell.inference.v1.GetInferenceBundleRequest - 2, // 5: openshell.inference.v1.Inference.SetInferenceRoute:input_type -> openshell.inference.v1.SetInferenceRouteRequest - 5, // 6: openshell.inference.v1.Inference.GetInferenceRoute:input_type -> openshell.inference.v1.GetInferenceRouteRequest - 7, // 7: openshell.inference.v1.Inference.DeleteInferenceRoute:input_type -> openshell.inference.v1.DeleteInferenceRouteRequest - 11, // 8: openshell.inference.v1.Inference.GetInferenceBundle:output_type -> openshell.inference.v1.GetInferenceBundleResponse - 4, // 9: openshell.inference.v1.Inference.SetInferenceRoute:output_type -> openshell.inference.v1.SetInferenceRouteResponse - 6, // 10: openshell.inference.v1.Inference.GetInferenceRoute:output_type -> openshell.inference.v1.GetInferenceRouteResponse - 8, // 11: openshell.inference.v1.Inference.DeleteInferenceRoute:output_type -> openshell.inference.v1.DeleteInferenceRouteResponse - 8, // [8:12] is the sub-list for method output_type - 4, // [4:8] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 12, // 0: openshell.inference.v1.InferenceRouteConfig.request_timeout:type_name -> google.protobuf.Duration + 13, // 1: openshell.inference.v1.InferenceRoute.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 0, // 2: openshell.inference.v1.InferenceRoute.config:type_name -> openshell.inference.v1.InferenceRouteConfig + 12, // 3: openshell.inference.v1.SetInferenceRouteRequest.request_timeout:type_name -> google.protobuf.Duration + 3, // 4: openshell.inference.v1.SetInferenceRouteResponse.validated_endpoints:type_name -> openshell.inference.v1.ValidatedEndpoint + 12, // 5: openshell.inference.v1.SetInferenceRouteResponse.request_timeout:type_name -> google.protobuf.Duration + 12, // 6: openshell.inference.v1.GetInferenceRouteResponse.request_timeout:type_name -> google.protobuf.Duration + 12, // 7: openshell.inference.v1.ResolvedRoute.request_timeout:type_name -> google.protobuf.Duration + 10, // 8: openshell.inference.v1.GetInferenceBundleResponse.routes:type_name -> openshell.inference.v1.ResolvedRoute + 14, // 9: openshell.inference.v1.GetInferenceBundleResponse.generated_time:type_name -> google.protobuf.Timestamp + 9, // 10: openshell.inference.v1.Inference.GetInferenceBundle:input_type -> openshell.inference.v1.GetInferenceBundleRequest + 2, // 11: openshell.inference.v1.Inference.SetInferenceRoute:input_type -> openshell.inference.v1.SetInferenceRouteRequest + 5, // 12: openshell.inference.v1.Inference.GetInferenceRoute:input_type -> openshell.inference.v1.GetInferenceRouteRequest + 7, // 13: openshell.inference.v1.Inference.DeleteInferenceRoute:input_type -> openshell.inference.v1.DeleteInferenceRouteRequest + 11, // 14: openshell.inference.v1.Inference.GetInferenceBundle:output_type -> openshell.inference.v1.GetInferenceBundleResponse + 4, // 15: openshell.inference.v1.Inference.SetInferenceRoute:output_type -> openshell.inference.v1.SetInferenceRouteResponse + 6, // 16: openshell.inference.v1.Inference.GetInferenceRoute:output_type -> openshell.inference.v1.GetInferenceRouteResponse + 8, // 17: openshell.inference.v1.Inference.DeleteInferenceRoute:output_type -> openshell.inference.v1.DeleteInferenceRouteResponse + 14, // [14:18] is the sub-list for method output_type + 10, // [10:14] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_inference_proto_init() } diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 0d2b86fce0..5c5313d7e7 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -15,7 +15,9 @@ import ( sandboxv1 "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -548,11 +550,10 @@ type IssueSandboxTokenResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Gateway-minted JWT bound to the calling sandbox's UUID. Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - // Absolute expiry of the issued token, milliseconds since the epoch. 0 means - // the token is non-expiring. - ExpiresAtMs int64 `protobuf:"varint,2,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Absolute expiry of the issued token. Absence means the token is non-expiring. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,102,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *IssueSandboxTokenResponse) Reset() { @@ -592,11 +593,11 @@ func (x *IssueSandboxTokenResponse) GetToken() string { return "" } -func (x *IssueSandboxTokenResponse) GetExpiresAtMs() int64 { +func (x *IssueSandboxTokenResponse) GetExpirationTime() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAtMs + return x.ExpirationTime } - return 0 + return nil } // RefreshSandboxToken request. The calling principal must already be a @@ -656,9 +657,8 @@ type RefreshSandboxTokenResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Fresh gateway-minted JWT bound to the same sandbox UUID. Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - // Absolute expiry of the new token, milliseconds since the epoch. 0 means - // the token is non-expiring. - ExpiresAtMs int64 `protobuf:"varint,2,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + // Absolute expiry of the new token. Absence means the token is non-expiring. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,102,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` // Fresh credentials for the requested, policy-authorized extension // services. These remain in supervisor memory and are never persisted. ExtensionCredentials []*ExtensionServiceCredential `protobuf:"bytes,3,rep,name=extension_credentials,json=extensionCredentials,proto3" json:"extension_credentials,omitempty"` @@ -703,11 +703,11 @@ func (x *RefreshSandboxTokenResponse) GetToken() string { return "" } -func (x *RefreshSandboxTokenResponse) GetExpiresAtMs() int64 { +func (x *RefreshSandboxTokenResponse) GetExpirationTime() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAtMs + return x.ExpirationTime } - return 0 + return nil } func (x *RefreshSandboxTokenResponse) GetExtensionCredentials() []*ExtensionServiceCredential { @@ -1675,9 +1675,9 @@ type SandboxCondition struct { // Human-readable condition message. Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` // Timestamp reported by the underlying platform for the last transition. - LastTransitionTime string `protobuf:"bytes,5,opt,name=last_transition_time,json=lastTransitionTime,proto3" json:"last_transition_time,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + TransitionTime *timestamppb.Timestamp `protobuf:"bytes,105,opt,name=transition_time,json=transitionTime,proto3" json:"transition_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SandboxCondition) Reset() { @@ -1738,18 +1738,18 @@ func (x *SandboxCondition) GetMessage() string { return "" } -func (x *SandboxCondition) GetLastTransitionTime() string { +func (x *SandboxCondition) GetTransitionTime() *timestamppb.Timestamp { if x != nil { - return x.LastTransitionTime + return x.TransitionTime } - return "" + return nil } // Public platform event exposed on the sandbox watch stream. type PlatformEvent struct { state protoimpl.MessageState `protogen:"open.v1"` - // Event timestamp in milliseconds since epoch. - TimestampMs int64 `protobuf:"varint,1,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` + // Time when the event occurred. + EventTime *timestamppb.Timestamp `protobuf:"bytes,101,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"` // Event source (e.g. "kubernetes", "docker", "process"). Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` // Event type/severity (e.g. "Normal", "Warning"). @@ -1794,11 +1794,11 @@ func (*PlatformEvent) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{19} } -func (x *PlatformEvent) GetTimestampMs() int64 { +func (x *PlatformEvent) GetEventTime() *timestamppb.Timestamp { if x != nil { - return x.TimestampMs + return x.EventTime } - return 0 + return nil } func (x *PlatformEvent) GetSource() string { @@ -2794,10 +2794,10 @@ type CreateSshSessionResponse struct { GatewayScheme string `protobuf:"bytes,5,opt,name=gateway_scheme,json=gatewayScheme,proto3" json:"gateway_scheme,omitempty"` // Optional host key fingerprint. If non-empty, [A-Za-z0-9:+/=-] only. HostKeyFingerprint string `protobuf:"bytes,7,opt,name=host_key_fingerprint,json=hostKeyFingerprint,proto3" json:"host_key_fingerprint,omitempty"` - // Expiry timestamp in milliseconds since epoch. 0 means no expiry. - ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Absolute expiry. Absence means no expiry. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateSshSessionResponse) Reset() { @@ -2872,11 +2872,11 @@ func (x *CreateSshSessionResponse) GetHostKeyFingerprint() string { return "" } -func (x *CreateSshSessionResponse) GetExpiresAtMs() int64 { +func (x *CreateSshSessionResponse) GetExpirationTime() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAtMs + return x.ExpirationTime } - return 0 + return nil } // Request to expose an HTTP service running inside a sandbox. @@ -3509,8 +3509,8 @@ type ExecSandboxRequest struct { Workdir string `protobuf:"bytes,3,opt,name=workdir,proto3" json:"workdir,omitempty"` // Optional environment overrides. Environment map[string]string `protobuf:"bytes,4,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Optional timeout in seconds. 0 means no timeout. - TimeoutSeconds uint32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` + // Optional execution timeout. Absence means no timeout. + ExecutionTimeout *durationpb.Duration `protobuf:"bytes,105,opt,name=execution_timeout,json=executionTimeout,proto3" json:"execution_timeout,omitempty"` // Optional stdin payload passed to the command. Stdin []byte `protobuf:"bytes,6,opt,name=stdin,proto3" json:"stdin,omitempty"` // Request a pseudo-terminal for the remote command. @@ -3587,11 +3587,11 @@ func (x *ExecSandboxRequest) GetEnvironment() map[string]string { return nil } -func (x *ExecSandboxRequest) GetTimeoutSeconds() uint32 { +func (x *ExecSandboxRequest) GetExecutionTimeout() *durationpb.Duration { if x != nil { - return x.TimeoutSeconds + return x.ExecutionTimeout } - return 0 + return nil } func (x *ExecSandboxRequest) GetStdin() []byte { @@ -4223,9 +4223,8 @@ type SshSession struct { SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` // Session token. Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` - // Expiry timestamp in milliseconds since epoch. 0 means no expiry - // (backward-compatible default for sessions created before this field existed). - ExpiresAtMs int64 `protobuf:"varint,4,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + // Absolute expiry. Absence means no expiry. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,104,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` // Revoked flag. Revoked bool `protobuf:"varint,5,opt,name=revoked,proto3" json:"revoked,omitempty"` unknownFields protoimpl.UnknownFields @@ -4283,11 +4282,11 @@ func (x *SshSession) GetToken() string { return "" } -func (x *SshSession) GetExpiresAtMs() int64 { +func (x *SshSession) GetExpirationTime() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAtMs + return x.ExpirationTime } - return 0 + return nil } func (x *SshSession) GetRevoked() bool { @@ -4315,9 +4314,9 @@ type WatchSandboxRequest struct { // Stop streaming once the sandbox reaches READY or a terminal result phase // (COMPLETED, STOPPED, or ERROR). StopOnTerminal bool `protobuf:"varint,7,opt,name=stop_on_terminal,json=stopOnTerminal,proto3" json:"stop_on_terminal,omitempty"` - // Only include log lines with timestamp >= this value (milliseconds since epoch). - // 0 means no time filter. Applies to both tail replay and live streaming. - LogSinceMs int64 `protobuf:"varint,8,opt,name=log_since_ms,json=logSinceMs,proto3" json:"log_since_ms,omitempty"` + // Only include log lines at or after this time. Absence means no time filter. + // Applies to both tail replay and live streaming. + SinceTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=since_time,json=sinceTime,proto3" json:"since_time,omitempty"` // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. LogSources []string `protobuf:"bytes,9,rep,name=log_sources,json=logSources,proto3" json:"log_sources,omitempty"` // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. @@ -4405,11 +4404,11 @@ func (x *WatchSandboxRequest) GetStopOnTerminal() bool { return false } -func (x *WatchSandboxRequest) GetLogSinceMs() int64 { +func (x *WatchSandboxRequest) GetSinceTime() *timestamppb.Timestamp { if x != nil { - return x.LogSinceMs + return x.SinceTime } - return 0 + return nil } func (x *WatchSandboxRequest) GetLogSources() []string { @@ -4564,12 +4563,12 @@ func (*SandboxStreamEvent_DraftPolicyUpdate) isSandboxStreamEvent_Payload() {} // Log line correlated to a sandbox. type SandboxLogLine struct { - state protoimpl.MessageState `protogen:"open.v1"` - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - TimestampMs int64 `protobuf:"varint,2,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` - Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` - Target string `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` - Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + EventTime *timestamppb.Timestamp `protobuf:"bytes,102,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"` + Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` + Target string `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` // Log source: "gateway" (server-side) or "sandbox" (supervisor). // Empty is treated as "gateway" for backward compatibility. Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` @@ -4616,11 +4615,11 @@ func (x *SandboxLogLine) GetSandboxId() string { return "" } -func (x *SandboxLogLine) GetTimestampMs() int64 { +func (x *SandboxLogLine) GetEventTime() *timestamppb.Timestamp { if x != nil { - return x.TimestampMs + return x.EventTime } - return 0 + return nil } func (x *SandboxLogLine) GetLevel() string { @@ -4886,8 +4885,8 @@ type UpdateProviderRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` // Optional per-credential expiry timestamps to merge into the provider. - // A zero value removes the expiry for that credential. - CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,2,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + // An absent map entry removes the expiry for that credential. + CredentialExpirationTimes map[string]*timestamppb.Timestamp `protobuf:"bytes,102,rep,name=credential_expiration_times,json=credentialExpirationTimes,proto3" json:"credential_expiration_times,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Workspace scope. Empty defaults to "default". Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields @@ -4931,9 +4930,9 @@ func (x *UpdateProviderRequest) GetProvider() *datamodelv1.Provider { return nil } -func (x *UpdateProviderRequest) GetCredentialExpiresAtMs() map[string]int64 { +func (x *UpdateProviderRequest) GetCredentialExpirationTimes() map[string]*timestamppb.Timestamp { if x != nil { - return x.CredentialExpiresAtMs + return x.CredentialExpirationTimes } return nil } @@ -5496,9 +5495,8 @@ type ProviderCredentialTokenGrant struct { JwtSvidAudience string `protobuf:"bytes,6,opt,name=jwt_svid_audience,json=jwtSvidAudience,proto3" json:"jwt_svid_audience,omitempty"` // Optional: OAuth2 scopes to request Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` - // Optional: override token cache TTL (seconds) - // If 0 or omitted, use expires_in from token response - CacheTtlSeconds int64 `protobuf:"varint,4,opt,name=cache_ttl_seconds,json=cacheTtlSeconds,proto3" json:"cache_ttl_seconds,omitempty"` + // Optional token cache TTL override. If absent, use expires_in from the token response. + CacheTtl *durationpb.Duration `protobuf:"bytes,104,opt,name=cache_ttl,json=cacheTtl,proto3" json:"cache_ttl,omitempty"` // Optional: endpoint-specific resource audience overrides. AudienceOverrides []*ProviderCredentialTokenGrantAudienceOverride `protobuf:"bytes,5,rep,name=audience_overrides,json=audienceOverrides,proto3" json:"audience_overrides,omitempty"` // Optional: OAuth2 client_assertion_type value. If omitted, OpenShell uses @@ -5574,11 +5572,11 @@ func (x *ProviderCredentialTokenGrant) GetScopes() []string { return nil } -func (x *ProviderCredentialTokenGrant) GetCacheTtlSeconds() int64 { +func (x *ProviderCredentialTokenGrant) GetCacheTtl() *durationpb.Duration { if x != nil { - return x.CacheTtlSeconds + return x.CacheTtl } - return 0 + return nil } func (x *ProviderCredentialTokenGrant) GetAudienceOverrides() []*ProviderCredentialTokenGrantAudienceOverride { @@ -5858,16 +5856,16 @@ func (x *ProviderCredentialRefreshOutput) GetCredential() string { } type ProviderCredentialRefresh struct { - state protoimpl.MessageState `protogen:"open.v1"` - Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,1,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` - TokenUrl string `protobuf:"bytes,2,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` - Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` - RefreshBeforeSeconds int64 `protobuf:"varint,4,opt,name=refresh_before_seconds,json=refreshBeforeSeconds,proto3" json:"refresh_before_seconds,omitempty"` - MaxLifetimeSeconds int64 `protobuf:"varint,5,opt,name=max_lifetime_seconds,json=maxLifetimeSeconds,proto3" json:"max_lifetime_seconds,omitempty"` - Material []*ProviderCredentialRefreshMaterial `protobuf:"bytes,6,rep,name=material,proto3" json:"material,omitempty"` - AdditionalOutputs []*ProviderCredentialRefreshOutput `protobuf:"bytes,7,rep,name=additional_outputs,json=additionalOutputs,proto3" json:"additional_outputs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,1,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + TokenUrl string `protobuf:"bytes,2,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` + RefreshBefore *durationpb.Duration `protobuf:"bytes,104,opt,name=refresh_before,json=refreshBefore,proto3" json:"refresh_before,omitempty"` + MaxLifetime *durationpb.Duration `protobuf:"bytes,105,opt,name=max_lifetime,json=maxLifetime,proto3" json:"max_lifetime,omitempty"` + Material []*ProviderCredentialRefreshMaterial `protobuf:"bytes,6,rep,name=material,proto3" json:"material,omitempty"` + AdditionalOutputs []*ProviderCredentialRefreshOutput `protobuf:"bytes,7,rep,name=additional_outputs,json=additionalOutputs,proto3" json:"additional_outputs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProviderCredentialRefresh) Reset() { @@ -5921,18 +5919,18 @@ func (x *ProviderCredentialRefresh) GetScopes() []string { return nil } -func (x *ProviderCredentialRefresh) GetRefreshBeforeSeconds() int64 { +func (x *ProviderCredentialRefresh) GetRefreshBefore() *durationpb.Duration { if x != nil { - return x.RefreshBeforeSeconds + return x.RefreshBefore } - return 0 + return nil } -func (x *ProviderCredentialRefresh) GetMaxLifetimeSeconds() int64 { +func (x *ProviderCredentialRefresh) GetMaxLifetime() *durationpb.Duration { if x != nil { - return x.MaxLifetimeSeconds + return x.MaxLifetime } - return 0 + return nil } func (x *ProviderCredentialRefresh) GetMaterial() []*ProviderCredentialRefreshMaterial { @@ -5950,19 +5948,17 @@ func (x *ProviderCredentialRefresh) GetAdditionalOutputs() []*ProviderCredential } type ProviderCredentialRefreshStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` - ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` - Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` - Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` - ExpiresAtMs int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - // Next automatic refresh time in Unix epoch milliseconds. A value of - // 9223372036854775807 (int64 max) means no automatic retry is scheduled; - // consumers should render it as unset and use recovery_action to determine - // the required recovery workflow. - NextRefreshAtMs int64 `protobuf:"varint,7,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` - LastRefreshAtMs int64 `protobuf:"varint,8,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + // Next automatic refresh time. Absence means no automatic retry is scheduled; + // use recovery_action to determine the required recovery workflow. + NextRefreshTime *timestamppb.Timestamp `protobuf:"bytes,107,opt,name=next_refresh_time,json=nextRefreshTime,proto3" json:"next_refresh_time,omitempty"` + LastRefreshTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=last_refresh_time,json=lastRefreshTime,proto3" json:"last_refresh_time,omitempty"` LastError string `protobuf:"bytes,9,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` RecoveryAction ProviderCredentialRefreshRecoveryAction `protobuf:"varint,10,opt,name=recovery_action,json=recoveryAction,proto3,enum=openshell.v1.ProviderCredentialRefreshRecoveryAction" json:"recovery_action,omitempty"` // Stable gateway-owned failure identifier, for example @@ -5972,8 +5968,8 @@ type ProviderCredentialRefreshStatus struct { // A bounded, recognized provider subtype that refines failure_code; clients // do not need a separate provider_error field. Unknown provider-controlled // values are not persisted or returned. - ProviderErrorSubtype string `protobuf:"bytes,12,opt,name=provider_error_subtype,json=providerErrorSubtype,proto3" json:"provider_error_subtype,omitempty"` - LastErrorAtMs int64 `protobuf:"varint,13,opt,name=last_error_at_ms,json=lastErrorAtMs,proto3" json:"last_error_at_ms,omitempty"` + ProviderErrorSubtype string `protobuf:"bytes,12,opt,name=provider_error_subtype,json=providerErrorSubtype,proto3" json:"provider_error_subtype,omitempty"` + LastErrorTime *timestamppb.Timestamp `protobuf:"bytes,113,opt,name=last_error_time,json=lastErrorTime,proto3" json:"last_error_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -6043,25 +6039,25 @@ func (x *ProviderCredentialRefreshStatus) GetStatus() string { return "" } -func (x *ProviderCredentialRefreshStatus) GetExpiresAtMs() int64 { +func (x *ProviderCredentialRefreshStatus) GetExpirationTime() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAtMs + return x.ExpirationTime } - return 0 + return nil } -func (x *ProviderCredentialRefreshStatus) GetNextRefreshAtMs() int64 { +func (x *ProviderCredentialRefreshStatus) GetNextRefreshTime() *timestamppb.Timestamp { if x != nil { - return x.NextRefreshAtMs + return x.NextRefreshTime } - return 0 + return nil } -func (x *ProviderCredentialRefreshStatus) GetLastRefreshAtMs() int64 { +func (x *ProviderCredentialRefreshStatus) GetLastRefreshTime() *timestamppb.Timestamp { if x != nil { - return x.LastRefreshAtMs + return x.LastRefreshTime } - return 0 + return nil } func (x *ProviderCredentialRefreshStatus) GetLastError() string { @@ -6092,11 +6088,11 @@ func (x *ProviderCredentialRefreshStatus) GetProviderErrorSubtype() string { return "" } -func (x *ProviderCredentialRefreshStatus) GetLastErrorAtMs() int64 { +func (x *ProviderCredentialRefreshStatus) GetLastErrorTime() *timestamppb.Timestamp { if x != nil { - return x.LastErrorAtMs + return x.LastErrorTime } - return 0 + return nil } // Provider profile local discovery declaration. @@ -6563,8 +6559,8 @@ type ConfigureProviderRefreshRequest struct { // Additional material names the caller requests be stored as secrets. Every // name must be present in material. The server also classifies secrets from // the authoritative provider profile and refresh strategy. - SecretMaterialKeys []string `protobuf:"bytes,5,rep,name=secret_material_keys,json=secretMaterialKeys,proto3" json:"secret_material_keys,omitempty"` - ExpiresAtMs *int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3,oneof" json:"expires_at_ms,omitempty"` + SecretMaterialKeys []string `protobuf:"bytes,5,rep,name=secret_material_keys,json=secretMaterialKeys,proto3" json:"secret_material_keys,omitempty"` + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` // Workspace scope. Empty defaults to "default". Workspace string `protobuf:"bytes,7,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields @@ -6636,11 +6632,11 @@ func (x *ConfigureProviderRefreshRequest) GetSecretMaterialKeys() []string { return nil } -func (x *ConfigureProviderRefreshRequest) GetExpiresAtMs() int64 { - if x != nil && x.ExpiresAtMs != nil { - return *x.ExpiresAtMs +func (x *ConfigureProviderRefreshRequest) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime } - return 0 + return nil } func (x *ConfigureProviderRefreshRequest) GetWorkspace() string { @@ -7897,7 +7893,7 @@ type GetSandboxProviderEnvironmentResponse struct { // Fingerprint for the provider credential inputs that produced environment. ProviderEnvRevision uint64 `protobuf:"varint,2,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` // Expiration timestamps for returned environment variables. - CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,3,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + CredentialExpirationTimes map[string]*timestamppb.Timestamp `protobuf:"bytes,103,rep,name=credential_expiration_times,json=credentialExpirationTimes,proto3" json:"credential_expiration_times,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Dynamic credentials that require token grants or other runtime injection. // Maps endpoint-bound provider metadata to credential metadata. // Supervisor uses this to inject Authorization headers for token grant credentials. @@ -7958,9 +7954,9 @@ func (x *GetSandboxProviderEnvironmentResponse) GetProviderEnvRevision() uint64 return 0 } -func (x *GetSandboxProviderEnvironmentResponse) GetCredentialExpiresAtMs() map[string]int64 { +func (x *GetSandboxProviderEnvironmentResponse) GetCredentialExpirationTimes() map[string]*timestamppb.Timestamp { if x != nil { - return x.CredentialExpiresAtMs + return x.CredentialExpirationTimes } return nil } @@ -8062,7 +8058,7 @@ func (x *ExchangeProviderSubjectTokenRequest) GetSupervisorJwtSvid() string { type ExchangeProviderSubjectTokenResponse struct { state protoimpl.MessageState `protogen:"open.v1"` AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` - ExpiresIn int64 `protobuf:"varint,2,opt,name=expires_in,json=expiresIn,proto3" json:"expires_in,omitempty"` + ExpiresAfter *durationpb.Duration `protobuf:"bytes,102,opt,name=expires_after,json=expiresAfter,proto3" json:"expires_after,omitempty"` TokenType string `protobuf:"bytes,3,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -8105,11 +8101,11 @@ func (x *ExchangeProviderSubjectTokenResponse) GetAccessToken() string { return "" } -func (x *ExchangeProviderSubjectTokenResponse) GetExpiresIn() int64 { +func (x *ExchangeProviderSubjectTokenResponse) GetExpiresAfter() *durationpb.Duration { if x != nil { - return x.ExpiresIn + return x.ExpiresAfter } - return 0 + return nil } func (x *ExchangeProviderSubjectTokenResponse) GetTokenType() string { @@ -9194,10 +9190,10 @@ type SandboxPolicyRevision struct { Status PolicyStatus `protobuf:"varint,3,opt,name=status,proto3,enum=openshell.v1.PolicyStatus" json:"status,omitempty"` // Error message if status is FAILED. LoadError string `protobuf:"bytes,4,opt,name=load_error,json=loadError,proto3" json:"load_error,omitempty"` - // Milliseconds since epoch when this revision was created. - CreatedAtMs int64 `protobuf:"varint,5,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` - // Milliseconds since epoch when this revision was loaded by the sandbox. - LoadedAtMs int64 `protobuf:"varint,6,opt,name=loaded_at_ms,json=loadedAtMs,proto3" json:"loaded_at_ms,omitempty"` + // Time when this revision was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,105,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` + // Time when this revision was loaded by the sandbox. Absent if not loaded. + LoadedTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=loaded_time,json=loadedTime,proto3" json:"loaded_time,omitempty"` // The full policy (only populated when explicitly requested). Policy *sandboxv1.SandboxPolicy `protobuf:"bytes,7,opt,name=policy,proto3" json:"policy,omitempty"` // Immutable provenance supplied with this policy revision. @@ -9264,18 +9260,18 @@ func (x *SandboxPolicyRevision) GetLoadError() string { return "" } -func (x *SandboxPolicyRevision) GetCreatedAtMs() int64 { +func (x *SandboxPolicyRevision) GetCreatedTime() *timestamppb.Timestamp { if x != nil { - return x.CreatedAtMs + return x.CreatedTime } - return 0 + return nil } -func (x *SandboxPolicyRevision) GetLoadedAtMs() int64 { +func (x *SandboxPolicyRevision) GetLoadedTime() *timestamppb.Timestamp { if x != nil { - return x.LoadedAtMs + return x.LoadedTime } - return 0 + return nil } func (x *SandboxPolicyRevision) GetPolicy() *sandboxv1.SandboxPolicy { @@ -9299,8 +9295,8 @@ type GetSandboxLogsRequest struct { SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` // Maximum number of log lines to return. 0 means use default (2000). Lines uint32 `protobuf:"varint,2,opt,name=lines,proto3" json:"lines,omitempty"` - // Only include logs with timestamp >= this value (ms since epoch). 0 means no filter. - SinceMs int64 `protobuf:"varint,3,opt,name=since_ms,json=sinceMs,proto3" json:"since_ms,omitempty"` + // Only include logs at or after this time. Absence means no filter. + SinceTime *timestamppb.Timestamp `protobuf:"bytes,103,opt,name=since_time,json=sinceTime,proto3" json:"since_time,omitempty"` // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. Sources []string `protobuf:"bytes,4,rep,name=sources,proto3" json:"sources,omitempty"` // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. @@ -9355,11 +9351,11 @@ func (x *GetSandboxLogsRequest) GetLines() uint32 { return 0 } -func (x *GetSandboxLogsRequest) GetSinceMs() int64 { +func (x *GetSandboxLogsRequest) GetSinceTime() *timestamppb.Timestamp { if x != nil { - return x.SinceMs + return x.SinceTime } - return 0 + return nil } func (x *GetSandboxLogsRequest) GetSources() []string { @@ -9836,10 +9832,10 @@ type SessionAccepted struct { state protoimpl.MessageState `protogen:"open.v1"` // Gateway-assigned session ID for this connection. SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - // Recommended heartbeat interval in seconds. - HeartbeatIntervalSecs uint32 `protobuf:"varint,2,opt,name=heartbeat_interval_secs,json=heartbeatIntervalSecs,proto3" json:"heartbeat_interval_secs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Recommended heartbeat interval. + HeartbeatInterval *durationpb.Duration `protobuf:"bytes,102,opt,name=heartbeat_interval,json=heartbeatInterval,proto3" json:"heartbeat_interval,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SessionAccepted) Reset() { @@ -9879,11 +9875,11 @@ func (x *SessionAccepted) GetSessionId() string { return "" } -func (x *SessionAccepted) GetHeartbeatIntervalSecs() uint32 { +func (x *SessionAccepted) GetHeartbeatInterval() *durationpb.Duration { if x != nil { - return x.HeartbeatIntervalSecs + return x.HeartbeatInterval } - return 0 + return nil } // Gateway rejects the supervisor session. @@ -10735,10 +10731,10 @@ type DenialSummary struct { Ancestors []string `protobuf:"bytes,5,rep,name=ancestors,proto3" json:"ancestors,omitempty"` // Denial reason from OPA evaluation. DenyReason string `protobuf:"bytes,6,opt,name=deny_reason,json=denyReason,proto3" json:"deny_reason,omitempty"` - // First denial timestamp (ms since epoch). - FirstSeenMs int64 `protobuf:"varint,7,opt,name=first_seen_ms,json=firstSeenMs,proto3" json:"first_seen_ms,omitempty"` - // Most recent denial timestamp (ms since epoch). - LastSeenMs int64 `protobuf:"varint,8,opt,name=last_seen_ms,json=lastSeenMs,proto3" json:"last_seen_ms,omitempty"` + // Time of the first denial. + FirstSeenTime *timestamppb.Timestamp `protobuf:"bytes,107,opt,name=first_seen_time,json=firstSeenTime,proto3" json:"first_seen_time,omitempty"` + // Time of the most recent denial. + LastSeenTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=last_seen_time,json=lastSeenTime,proto3" json:"last_seen_time,omitempty"` // Number of denials in the current window. Count uint32 `protobuf:"varint,9,opt,name=count,proto3" json:"count,omitempty"` // Events dropped during aggregator cooldown. @@ -10833,18 +10829,18 @@ func (x *DenialSummary) GetDenyReason() string { return "" } -func (x *DenialSummary) GetFirstSeenMs() int64 { +func (x *DenialSummary) GetFirstSeenTime() *timestamppb.Timestamp { if x != nil { - return x.FirstSeenMs + return x.FirstSeenTime } - return 0 + return nil } -func (x *DenialSummary) GetLastSeenMs() int64 { +func (x *DenialSummary) GetLastSeenTime() *timestamppb.Timestamp { if x != nil { - return x.LastSeenMs + return x.LastSeenTime } - return 0 + return nil } func (x *DenialSummary) GetCount() uint32 { @@ -11049,20 +11045,20 @@ type PolicyChunk struct { Confidence float32 `protobuf:"fixed32,7,opt,name=confidence,proto3" json:"confidence,omitempty"` // IDs of denial summaries that led to this chunk. DenialSummaryIds []string `protobuf:"bytes,8,rep,name=denial_summary_ids,json=denialSummaryIds,proto3" json:"denial_summary_ids,omitempty"` - // Creation timestamp (ms since epoch). - CreatedAtMs int64 `protobuf:"varint,9,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` - // When the user approved/rejected (ms since epoch). 0 if undecided. - DecidedAtMs int64 `protobuf:"varint,10,opt,name=decided_at_ms,json=decidedAtMs,proto3" json:"decided_at_ms,omitempty"` + // Time when this chunk was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,109,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` + // Time when the user approved or rejected the chunk. Absent if undecided. + DecidedTime *timestamppb.Timestamp `protobuf:"bytes,110,opt,name=decided_time,json=decidedTime,proto3" json:"decided_time,omitempty"` // Recommendation stage: "initial" or "refined" (progressive L7 visibility). Stage string `protobuf:"bytes,11,opt,name=stage,proto3" json:"stage,omitempty"` // For stage="refined": the initial chunk this replaces. SupersedesChunkId string `protobuf:"bytes,12,opt,name=supersedes_chunk_id,json=supersedesChunkId,proto3" json:"supersedes_chunk_id,omitempty"` // How many times this endpoint has been seen across denial flush cycles. HitCount int32 `protobuf:"varint,13,opt,name=hit_count,json=hitCount,proto3" json:"hit_count,omitempty"` - // First time this endpoint was proposed (ms since epoch). - FirstSeenMs int64 `protobuf:"varint,14,opt,name=first_seen_ms,json=firstSeenMs,proto3" json:"first_seen_ms,omitempty"` - // Most recent time this endpoint was re-proposed (ms since epoch). - LastSeenMs int64 `protobuf:"varint,15,opt,name=last_seen_ms,json=lastSeenMs,proto3" json:"last_seen_ms,omitempty"` + // First time this endpoint was proposed. + FirstSeenTime *timestamppb.Timestamp `protobuf:"bytes,114,opt,name=first_seen_time,json=firstSeenTime,proto3" json:"first_seen_time,omitempty"` + // Most recent time this endpoint was proposed again. + LastSeenTime *timestamppb.Timestamp `protobuf:"bytes,115,opt,name=last_seen_time,json=lastSeenTime,proto3" json:"last_seen_time,omitempty"` // Binary path that triggered the denial (denormalized for display convenience). Binary string `protobuf:"bytes,16,opt,name=binary,proto3" json:"binary,omitempty"` // Validation verdict from gateway-side static checks (prover output). @@ -11178,18 +11174,18 @@ func (x *PolicyChunk) GetDenialSummaryIds() []string { return nil } -func (x *PolicyChunk) GetCreatedAtMs() int64 { +func (x *PolicyChunk) GetCreatedTime() *timestamppb.Timestamp { if x != nil { - return x.CreatedAtMs + return x.CreatedTime } - return 0 + return nil } -func (x *PolicyChunk) GetDecidedAtMs() int64 { +func (x *PolicyChunk) GetDecidedTime() *timestamppb.Timestamp { if x != nil { - return x.DecidedAtMs + return x.DecidedTime } - return 0 + return nil } func (x *PolicyChunk) GetStage() string { @@ -11213,18 +11209,18 @@ func (x *PolicyChunk) GetHitCount() int32 { return 0 } -func (x *PolicyChunk) GetFirstSeenMs() int64 { +func (x *PolicyChunk) GetFirstSeenTime() *timestamppb.Timestamp { if x != nil { - return x.FirstSeenMs + return x.FirstSeenTime } - return 0 + return nil } -func (x *PolicyChunk) GetLastSeenMs() int64 { +func (x *PolicyChunk) GetLastSeenTime() *timestamppb.Timestamp { if x != nil { - return x.LastSeenMs + return x.LastSeenTime } - return 0 + return nil } func (x *PolicyChunk) GetBinary() string { @@ -11606,8 +11602,8 @@ type GetDraftPolicyResponse struct { RollingSummary string `protobuf:"bytes,2,opt,name=rolling_summary,json=rollingSummary,proto3" json:"rolling_summary,omitempty"` // Current draft version. DraftVersion uint64 `protobuf:"varint,3,opt,name=draft_version,json=draftVersion,proto3" json:"draft_version,omitempty"` - // When the last analysis completed (ms since epoch). - LastAnalyzedAtMs int64 `protobuf:"varint,4,opt,name=last_analyzed_at_ms,json=lastAnalyzedAtMs,proto3" json:"last_analyzed_at_ms,omitempty"` + // Time when the last analysis completed. + LastAnalyzedTime *timestamppb.Timestamp `protobuf:"bytes,104,opt,name=last_analyzed_time,json=lastAnalyzedTime,proto3" json:"last_analyzed_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -11663,11 +11659,11 @@ func (x *GetDraftPolicyResponse) GetDraftVersion() uint64 { return 0 } -func (x *GetDraftPolicyResponse) GetLastAnalyzedAtMs() int64 { +func (x *GetDraftPolicyResponse) GetLastAnalyzedTime() *timestamppb.Timestamp { if x != nil { - return x.LastAnalyzedAtMs + return x.LastAnalyzedTime } - return 0 + return nil } // Approve a single draft chunk. @@ -12490,8 +12486,8 @@ func (x *GetDraftHistoryRequest) GetWorkspace() string { type DraftHistoryEntry struct { state protoimpl.MessageState `protogen:"open.v1"` - // Event timestamp (ms since epoch). - TimestampMs int64 `protobuf:"varint,1,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` + // Time when the event occurred. + EventTime *timestamppb.Timestamp `protobuf:"bytes,101,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"` // Event type: "denial_detected", "analysis_cycle", "approved", // "rejected", "edited", "undone", "cleared". EventType string `protobuf:"bytes,2,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"` @@ -12533,11 +12529,11 @@ func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{172} } -func (x *DraftHistoryEntry) GetTimestampMs() int64 { +func (x *DraftHistoryEntry) GetEventTime() *timestamppb.Timestamp { if x != nil { - return x.TimestampMs + return x.EventTime } - return 0 + return nil } func (x *DraftHistoryEntry) GetEventType() string { @@ -14017,10 +14013,10 @@ type ExtensionServiceCredential struct { ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // Gateway-minted JWT with an audience derived from the registration. Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` - // Absolute expiry of the token, milliseconds since the epoch. - ExpiresAtMs int64 `protobuf:"varint,3,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Absolute expiry of the token. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,103,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExtensionServiceCredential) Reset() { @@ -14067,28 +14063,28 @@ func (x *ExtensionServiceCredential) GetToken() string { return "" } -func (x *ExtensionServiceCredential) GetExpiresAtMs() int64 { +func (x *ExtensionServiceCredential) GetExpirationTime() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAtMs + return x.ExpirationTime } - return 0 + return nil } var File_openshell_proto protoreflect.FileDescriptor const file_openshell_proto_rawDesc = "" + "\n" + - "\x0fopenshell.proto\x12\fopenshell.v1\x1a\x0fdatamodel.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\roptions.proto\x1a\rsandbox.proto\"\x1a\n" + - "\x18IssueSandboxTokenRequest\"[\n" + + "\x0fopenshell.proto\x12\fopenshell.v1\x1a\x0fdatamodel.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\roptions.proto\x1a\rsandbox.proto\"\x1a\n" + + "\x18IssueSandboxTokenRequest\"\x91\x01\n" + "\x19IssueSandboxTokenResponse\x12\x1a\n" + - "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12\"\n" + - "\rexpires_at_ms\x18\x02 \x01(\x03R\vexpiresAtMs\"T\n" + + "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12C\n" + + "\x0fexpiration_time\x18f \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTimeJ\x04\b\x02\x10\x03R\rexpires_at_ms\"T\n" + "\x1aRefreshSandboxTokenRequest\x126\n" + - "\x17extension_service_names\x18\x01 \x03(\tR\x15extensionServiceNames\"\xbc\x01\n" + + "\x17extension_service_names\x18\x01 \x03(\tR\x15extensionServiceNames\"\xf2\x01\n" + "\x1bRefreshSandboxTokenResponse\x12\x1a\n" + - "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12\"\n" + - "\rexpires_at_ms\x18\x02 \x01(\x03R\vexpiresAtMs\x12]\n" + - "\x15extension_credentials\x18\x03 \x03(\v2(.openshell.v1.ExtensionServiceCredentialR\x14extensionCredentials\"\x0f\n" + + "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12C\n" + + "\x0fexpiration_time\x18f \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12]\n" + + "\x15extension_credentials\x18\x03 \x03(\v2(.openshell.v1.ExtensionServiceCredentialR\x14extensionCredentialsJ\x04\b\x02\x10\x03R\rexpires_at_ms\"\x0f\n" + "\rHealthRequest\"_\n" + "\x0eHealthResponse\x123\n" + "\x06status\x18\x01 \x01(\x0e2\x1b.openshell.v1.ServiceStatusR\x06status\x12\x18\n" + @@ -14171,15 +14167,16 @@ const file_openshell_proto_rawDesc = "" + "\x18main_process_instance_id\x18\b \x01(\tR\x15mainProcessInstanceId\x12 \n" + "\texit_code\x18\t \x01(\x05H\x00R\bexitCode\x88\x01\x01B\f\n" + "\n" + - "_exit_code\"\xa2\x01\n" + + "_exit_code\"\xd1\x01\n" + "\x10SandboxCondition\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" + "\x06status\x18\x02 \x01(\tR\x06status\x12\x16\n" + "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x18\n" + - "\amessage\x18\x04 \x01(\tR\amessage\x120\n" + - "\x14last_transition_time\x18\x05 \x01(\tR\x12lastTransitionTime\"\x94\x02\n" + - "\rPlatformEvent\x12!\n" + - "\ftimestamp_ms\x18\x01 \x01(\x03R\vtimestampMs\x12\x16\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12C\n" + + "\x0ftransition_time\x18i \x01(\v2\x1a.google.protobuf.TimestampR\x0etransitionTimeJ\x04\b\x05\x10\x06R\x14last_transition_time\"\xc0\x02\n" + + "\rPlatformEvent\x129\n" + + "\n" + + "event_time\x18e \x01(\v2\x1a.google.protobuf.TimestampR\teventTime\x12\x16\n" + "\x06source\x18\x02 \x01(\tR\x06source\x12\x12\n" + "\x04type\x18\x03 \x01(\tR\x04type\x12\x16\n" + "\x06reason\x18\x04 \x01(\tR\x06reason\x12\x18\n" + @@ -14187,7 +14184,7 @@ const file_openshell_proto_rawDesc = "" + "\bmetadata\x18\x06 \x03(\v2).openshell.v1.PlatformEvent.MetadataEntryR\bmetadata\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd4\x03\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02R\ftimestamp_ms\"\xd4\x03\n" + "\x14CreateSandboxRequest\x12-\n" + "\x04spec\x18\x01 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12F\n" + @@ -14248,7 +14245,7 @@ const file_openshell_proto_rawDesc = "" + "\adeleted\x18\x01 \x01(\bR\adeleted\"8\n" + "\x17CreateSshSessionRequest\x12\x1d\n" + "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x98\x02\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\xce\x02\n" + "\x18CreateSshSessionResponse\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1a\n" + @@ -14256,8 +14253,8 @@ const file_openshell_proto_rawDesc = "" + "\fgateway_host\x18\x03 \x01(\tR\vgatewayHost\x12!\n" + "\fgateway_port\x18\x04 \x01(\rR\vgatewayPort\x12%\n" + "\x0egateway_scheme\x18\x05 \x01(\tR\rgatewayScheme\x120\n" + - "\x14host_key_fingerprint\x18\a \x01(\tR\x12hostKeyFingerprint\x12\"\n" + - "\rexpires_at_ms\x18\b \x01(\x03R\vexpiresAtMs\"\xa1\x01\n" + + "\x14host_key_fingerprint\x18\a \x01(\tR\x12hostKeyFingerprint\x12C\n" + + "\x0fexpiration_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTimeJ\x04\b\b\x10\tR\rexpires_at_ms\"\xa1\x01\n" + "\x14ExposeServiceRequest\x12\x18\n" + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + "\aservice\x18\x02 \x01(\tR\aservice\x12\x1f\n" + @@ -14298,14 +14295,14 @@ const file_openshell_proto_rawDesc = "" + "\x17RevokeSshSessionRequest\x12\x1a\n" + "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\"4\n" + "\x18RevokeSshSessionResponse\x12\x18\n" + - "\arevoked\x18\x01 \x01(\bR\arevoked\"\x9b\x03\n" + + "\arevoked\x18\x01 \x01(\bR\arevoked\"\xd1\x03\n" + "\x12ExecSandboxRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x18\n" + "\acommand\x18\x02 \x03(\tR\acommand\x12\x18\n" + "\aworkdir\x18\x03 \x01(\tR\aworkdir\x12S\n" + - "\venvironment\x18\x04 \x03(\v21.openshell.v1.ExecSandboxRequest.EnvironmentEntryR\venvironment\x12'\n" + - "\x0ftimeout_seconds\x18\x05 \x01(\rR\x0etimeoutSeconds\x12\x14\n" + + "\venvironment\x18\x04 \x03(\v21.openshell.v1.ExecSandboxRequest.EnvironmentEntryR\venvironment\x12F\n" + + "\x11execution_timeout\x18i \x01(\v2\x19.google.protobuf.DurationR\x10executionTimeout\x12\x14\n" + "\x05stdin\x18\x06 \x01(\fR\x05stdin\x12\x10\n" + "\x03tty\x18\a \x01(\bR\x03tty\x12\x12\n" + "\x04cols\x18\b \x01(\rR\x04cols\x12\x12\n" + @@ -14314,7 +14311,7 @@ const file_openshell_proto_rawDesc = "" + " \x01(\bR\fnoLoginShell\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"'\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x05\x10\x06R\x0ftimeout_seconds\"'\n" + "\x11ExecSandboxStdout\x12\x12\n" + "\x04data\x18\x01 \x01(\fR\x04data\"'\n" + "\x11ExecSandboxStderr\x12\x12\n" + @@ -14346,15 +14343,15 @@ const file_openshell_proto_rawDesc = "" + "\apayload\"A\n" + "\x17ExecSandboxWindowResize\x12\x12\n" + "\x04cols\x18\x01 \x01(\rR\x04cols\x12\x12\n" + - "\x04rows\x18\x02 \x01(\rR\x04rows\"\xc5\x01\n" + + "\x04rows\x18\x02 \x01(\rR\x04rows\"\xfb\x01\n" + "\n" + "SshSession\x12>\n" + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x1d\n" + "\n" + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12\x1a\n" + - "\x05token\x18\x03 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12\"\n" + - "\rexpires_at_ms\x18\x04 \x01(\x03R\vexpiresAtMs\x12\x18\n" + - "\arevoked\x18\x05 \x01(\bR\arevoked\"\xe6\x02\n" + + "\x05token\x18\x03 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12C\n" + + "\x0fexpiration_time\x18h \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12\x18\n" + + "\arevoked\x18\x05 \x01(\bR\arevokedJ\x04\b\x04\x10\x05R\rexpires_at_ms\"\x93\x03\n" + "\x13WatchSandboxRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12#\n" + "\rfollow_status\x18\x02 \x01(\bR\ffollowStatus\x12\x1f\n" + @@ -14364,24 +14361,25 @@ const file_openshell_proto_rawDesc = "" + "\x0elog_tail_lines\x18\x05 \x01(\rR\flogTailLines\x12\x1d\n" + "\n" + "event_tail\x18\x06 \x01(\rR\teventTail\x12(\n" + - "\x10stop_on_terminal\x18\a \x01(\bR\x0estopOnTerminal\x12 \n" + - "\flog_since_ms\x18\b \x01(\x03R\n" + - "logSinceMs\x12\x1f\n" + + "\x10stop_on_terminal\x18\a \x01(\bR\x0estopOnTerminal\x129\n" + + "\n" + + "since_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\tsinceTime\x12\x1f\n" + "\vlog_sources\x18\t \x03(\tR\n" + "logSources\x12\"\n" + "\rlog_min_level\x18\n" + - " \x01(\tR\vlogMinLevel\"\xcc\x02\n" + + " \x01(\tR\vlogMinLevelJ\x04\b\b\x10\tR\flog_since_ms\"\xcc\x02\n" + "\x12SandboxStreamEvent\x121\n" + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxH\x00R\asandbox\x120\n" + "\x03log\x18\x02 \x01(\v2\x1c.openshell.v1.SandboxLogLineH\x00R\x03log\x123\n" + "\x05event\x18\x03 \x01(\v2\x1b.openshell.v1.PlatformEventH\x00R\x05event\x12>\n" + "\awarning\x18\x04 \x01(\v2\".openshell.v1.SandboxStreamWarningH\x00R\awarning\x12Q\n" + "\x13draft_policy_update\x18\x05 \x01(\v2\x1f.openshell.v1.DraftPolicyUpdateH\x00R\x11draftPolicyUpdateB\t\n" + - "\apayload\"\xaf\x02\n" + + "\apayload\"\xdb\x02\n" + "\x0eSandboxLogLine\x12\x1d\n" + "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12!\n" + - "\ftimestamp_ms\x18\x02 \x01(\x03R\vtimestampMs\x12\x14\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x129\n" + + "\n" + + "event_time\x18f \x01(\v2\x1a.google.protobuf.TimestampR\teventTime\x12\x14\n" + "\x05level\x18\x03 \x01(\tR\x05level\x12\x16\n" + "\x06target\x18\x04 \x01(\tR\x06target\x12\x18\n" + "\amessage\x18\x05 \x01(\tR\amessage\x12\x16\n" + @@ -14389,7 +14387,7 @@ const file_openshell_proto_rawDesc = "" + "\x06fields\x18\a \x03(\v2(.openshell.v1.SandboxLogLine.FieldsEntryR\x06fields\x1a9\n" + "\vFieldsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"0\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x02\x10\x03R\ftimestamp_ms\"0\n" + "\x14SandboxStreamWarning\x12\x18\n" + "\amessage\x18\x01 \x01(\tR\amessage\"s\n" + "\x15CreateProviderRequest\x12<\n" + @@ -14402,14 +14400,14 @@ const file_openshell_proto_rawDesc = "" + "\x05limit\x18\x01 \x01(\rR\x05limit\x12\x16\n" + "\x06offset\x18\x02 \x01(\rR\x06offset\x12\x1c\n" + "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12%\n" + - "\x0eall_workspaces\x18\x04 \x01(\bR\rallWorkspaces\"\xb6\x02\n" + + "\x0eall_workspaces\x18\x04 \x01(\bR\rallWorkspaces\"\x82\x03\n" + "\x15UpdateProviderRequest\x12<\n" + - "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12w\n" + - "\x18credential_expires_at_ms\x18\x02 \x03(\v2>.openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntryR\x15credentialExpiresAtMs\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\x1aH\n" + - "\x1aCredentialExpiresAtMsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"I\n" + + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12\x82\x01\n" + + "\x1bcredential_expiration_times\x18f \x03(\v2B.openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntryR\x19credentialExpirationTimes\x12\x1c\n" + + "\tworkspace\x18\x03 \x01(\tR\tworkspace\x1ah\n" + + "\x1eCredentialExpirationTimesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + + "\x05value\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05value:\x028\x01J\x04\b\x02\x10\x03R\x18credential_expires_at_ms\"I\n" + "\x15DeleteProviderRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"P\n" + @@ -14445,20 +14443,20 @@ const file_openshell_proto_rawDesc = "" + "\n" + "credential\x18\x02 \x01(\tR\n" + "credential\x12,\n" + - "\x12subject_token_type\x18\x03 \x01(\tR\x10subjectTokenType\"\xce\x04\n" + + "\x12subject_token_type\x18\x03 \x01(\tR\x10subjectTokenType\"\xf3\x04\n" + "\x1cProviderCredentialTokenGrant\x12%\n" + "\x0etoken_endpoint\x18\x01 \x01(\tR\rtokenEndpoint\x12\x1a\n" + "\baudience\x18\x02 \x01(\tR\baudience\x12*\n" + "\x11jwt_svid_audience\x18\x06 \x01(\tR\x0fjwtSvidAudience\x12\x16\n" + - "\x06scopes\x18\x03 \x03(\tR\x06scopes\x12*\n" + - "\x11cache_ttl_seconds\x18\x04 \x01(\x03R\x0fcacheTtlSeconds\x12i\n" + + "\x06scopes\x18\x03 \x03(\tR\x06scopes\x126\n" + + "\tcache_ttl\x18h \x01(\v2\x19.google.protobuf.DurationR\bcacheTtl\x12i\n" + "\x12audience_overrides\x18\x05 \x03(\v2:.openshell.v1.ProviderCredentialTokenGrantAudienceOverrideR\x11audienceOverrides\x122\n" + "\x15client_assertion_type\x18\a \x01(\tR\x13clientAssertionType\x12M\n" + "\n" + "grant_type\x18\b \x01(\x0e2..openshell.v1.ProviderCredentialTokenGrantTypeR\tgrantType\x12[\n" + "\rsubject_token\x18\t \x01(\v26.openshell.v1.ProviderCredentialTokenGrantSubjectTokenR\fsubjectToken\x120\n" + "\x14requested_token_type\x18\n" + - " \x01(\tR\x12requestedTokenType\"\x9e\x03\n" + + " \x01(\tR\x12requestedTokenTypeJ\x04\b\x04\x10\x05R\x11cache_ttl_seconds\"\x9e\x03\n" + "\x19ProviderProfileCredential\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + @@ -14484,32 +14482,32 @@ const file_openshell_proto_rawDesc = "" + "\x06output\x18\x01 \x01(\tR\x06output\x12\x1e\n" + "\n" + "credential\x18\x02 \x01(\tR\n" + - "credential\"\xb0\x03\n" + + "credential\"\x82\x04\n" + "\x19ProviderCredentialRefresh\x12K\n" + "\bstrategy\x18\x01 \x01(\x0e2/.openshell.v1.ProviderCredentialRefreshStrategyR\bstrategy\x12\x1b\n" + "\ttoken_url\x18\x02 \x01(\tR\btokenUrl\x12\x16\n" + - "\x06scopes\x18\x03 \x03(\tR\x06scopes\x124\n" + - "\x16refresh_before_seconds\x18\x04 \x01(\x03R\x14refreshBeforeSeconds\x120\n" + - "\x14max_lifetime_seconds\x18\x05 \x01(\x03R\x12maxLifetimeSeconds\x12K\n" + + "\x06scopes\x18\x03 \x03(\tR\x06scopes\x12@\n" + + "\x0erefresh_before\x18h \x01(\v2\x19.google.protobuf.DurationR\rrefreshBefore\x12<\n" + + "\fmax_lifetime\x18i \x01(\v2\x19.google.protobuf.DurationR\vmaxLifetime\x12K\n" + "\bmaterial\x18\x06 \x03(\v2/.openshell.v1.ProviderCredentialRefreshMaterialR\bmaterial\x12\\\n" + - "\x12additional_outputs\x18\a \x03(\v2-.openshell.v1.ProviderCredentialRefreshOutputR\x11additionalOutputs\"\xf2\x04\n" + + "\x12additional_outputs\x18\a \x03(\v2-.openshell.v1.ProviderCredentialRefreshOutputR\x11additionalOutputsJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\x16refresh_before_secondsR\x14max_lifetime_seconds\"\xc5\x06\n" + "\x1fProviderCredentialRefreshStatus\x12#\n" + "\rprovider_name\x18\x01 \x01(\tR\fproviderName\x12\x1f\n" + "\vprovider_id\x18\x02 \x01(\tR\n" + "providerId\x12%\n" + "\x0ecredential_key\x18\x03 \x01(\tR\rcredentialKey\x12K\n" + "\bstrategy\x18\x04 \x01(\x0e2/.openshell.v1.ProviderCredentialRefreshStrategyR\bstrategy\x12\x16\n" + - "\x06status\x18\x05 \x01(\tR\x06status\x12\"\n" + - "\rexpires_at_ms\x18\x06 \x01(\x03R\vexpiresAtMs\x12+\n" + - "\x12next_refresh_at_ms\x18\a \x01(\x03R\x0fnextRefreshAtMs\x12+\n" + - "\x12last_refresh_at_ms\x18\b \x01(\x03R\x0flastRefreshAtMs\x12\x1d\n" + + "\x06status\x18\x05 \x01(\tR\x06status\x12C\n" + + "\x0fexpiration_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12F\n" + + "\x11next_refresh_time\x18k \x01(\v2\x1a.google.protobuf.TimestampR\x0fnextRefreshTime\x12F\n" + + "\x11last_refresh_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\x0flastRefreshTime\x12\x1d\n" + "\n" + "last_error\x18\t \x01(\tR\tlastError\x12^\n" + "\x0frecovery_action\x18\n" + " \x01(\x0e25.openshell.v1.ProviderCredentialRefreshRecoveryActionR\x0erecoveryAction\x12!\n" + "\ffailure_code\x18\v \x01(\tR\vfailureCode\x124\n" + - "\x16provider_error_subtype\x18\f \x01(\tR\x14providerErrorSubtype\x12'\n" + - "\x10last_error_at_ms\x18\r \x01(\x03R\rlastErrorAtMs\"<\n" + + "\x16provider_error_subtype\x18\f \x01(\tR\x14providerErrorSubtype\x12B\n" + + "\x0flast_error_time\x18q \x01(\v2\x1a.google.protobuf.TimestampR\rlastErrorTimeJ\x04\b\x06\x10\aJ\x04\b\a\x10\bJ\x04\b\b\x10\tJ\x04\b\r\x10\x0eR\rexpires_at_msR\x12next_refresh_at_msR\x12last_refresh_at_msR\x10last_error_at_ms\"<\n" + "\x18ProviderProfileDiscovery\x12 \n" + "\vcredentials\x18\x01 \x03(\tR\vcredentials\"\x89\r\n" + "$StoredProviderCredentialRefreshState\x12>\n" + @@ -14557,19 +14555,18 @@ const file_openshell_proto_rawDesc = "" + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12\x1c\n" + "\tworkspace\x18\x03 \x01(\tR\tworkspace\"s\n" + " GetProviderRefreshStatusResponse\x12O\n" + - "\vcredentials\x18\x01 \x03(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\vcredentials\"\xd8\x03\n" + + "\vcredentials\x18\x01 \x03(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\vcredentials\"\xf7\x03\n" + "\x1fConfigureProviderRefreshRequest\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12K\n" + "\bstrategy\x18\x03 \x01(\x0e2/.openshell.v1.ProviderCredentialRefreshStrategyR\bstrategy\x12]\n" + "\bmaterial\x18\x04 \x03(\v2;.openshell.v1.ConfigureProviderRefreshRequest.MaterialEntryB\x04\x88\xb5\x18\x01R\bmaterial\x120\n" + - "\x14secret_material_keys\x18\x05 \x03(\tR\x12secretMaterialKeys\x12'\n" + - "\rexpires_at_ms\x18\x06 \x01(\x03H\x00R\vexpiresAtMs\x88\x01\x01\x12\x1c\n" + + "\x14secret_material_keys\x18\x05 \x03(\tR\x12secretMaterialKeys\x12C\n" + + "\x0fexpiration_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12\x1c\n" + "\tworkspace\x18\a \x01(\tR\tworkspace\x1a;\n" + "\rMaterialEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x10\n" + - "\x0e_expires_at_ms\"i\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x06\x10\aR\rexpires_at_ms\"i\n" + " ConfigureProviderRefreshResponse\x12E\n" + "\x06status\x18\x01 \x01(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\x06status\"\x82\x01\n" + "\x1fRotateProviderCredentialRequest\x12\x1a\n" + @@ -14649,38 +14646,38 @@ const file_openshell_proto_rawDesc = "" + "\x17StaticCredentialBinding\x12K\n" + "\tendpoints\x18\x01 \x03(\v2-.openshell.v1.StaticCredentialEndpointBindingR\tendpoints\x12/\n" + "\x13credential_identity\x18\x02 \x01(\tR\x12credentialIdentity\x12<\n" + - "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\"\x90\b\n" + + "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\"\xdb\b\n" + "%GetSandboxProviderEnvironmentResponse\x12l\n" + "\venvironment\x18\x01 \x03(\v2D.openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntryB\x04\x88\xb5\x18\x01R\venvironment\x122\n" + - "\x15provider_env_revision\x18\x02 \x01(\x04R\x13providerEnvRevision\x12\x87\x01\n" + - "\x18credential_expires_at_ms\x18\x03 \x03(\v2N.openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntryR\x15credentialExpiresAtMs\x12|\n" + + "\x15provider_env_revision\x18\x02 \x01(\x04R\x13providerEnvRevision\x12\x92\x01\n" + + "\x1bcredential_expiration_times\x18g \x03(\v2R.openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntryR\x19credentialExpirationTimes\x12|\n" + "\x13dynamic_credentials\x18\x04 \x03(\v2K.openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntryR\x12dynamicCredentials\x12\x8f\x01\n" + "\x1astatic_credential_bindings\x18\x05 \x03(\v2Q.openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntryR\x18staticCredentialBindings\x12=\n" + "\x1bnon_secret_environment_keys\x18\x06 \x03(\tR\x18nonSecretEnvironmentKeys\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aH\n" + - "\x1aCredentialExpiresAtMsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\x1an\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1ah\n" + + "\x1eCredentialExpirationTimesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + + "\x05value\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05value:\x028\x01\x1an\n" + "\x17DynamicCredentialsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12=\n" + "\x05value\x18\x02 \x01(\v2'.openshell.v1.ProviderProfileCredentialR\x05value:\x028\x01\x1ar\n" + "\x1dStaticCredentialBindingsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12;\n" + - "\x05value\x18\x02 \x01(\v2%.openshell.v1.StaticCredentialBindingR\x05value:\x028\x01\"\xbd\x01\n" + + "\x05value\x18\x02 \x01(\v2%.openshell.v1.StaticCredentialBindingR\x05value:\x028\x01J\x04\b\x03\x10\x04R\x18credential_expires_at_ms\"\xbd\x01\n" + "#ExchangeProviderSubjectTokenRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1a\n" + "\bprovider\x18\x02 \x01(\tR\bprovider\x12%\n" + "\x0ecredential_key\x18\x03 \x01(\tR\rcredentialKey\x124\n" + - "\x13supervisor_jwt_svid\x18\x04 \x01(\tB\x04\x88\xb5\x18\x01R\x11supervisorJwtSvid\"\x8d\x01\n" + + "\x13supervisor_jwt_svid\x18\x04 \x01(\tB\x04\x88\xb5\x18\x01R\x11supervisorJwtSvid\"\xc0\x01\n" + "$ExchangeProviderSubjectTokenResponse\x12'\n" + - "\faccess_token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\vaccessToken\x12\x1d\n" + + "\faccess_token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\vaccessToken\x12>\n" + + "\rexpires_after\x18f \x01(\v2\x19.google.protobuf.DurationR\fexpiresAfter\x12\x1d\n" + "\n" + - "expires_in\x18\x02 \x01(\x03R\texpiresIn\x12\x1d\n" + - "\n" + - "token_type\x18\x03 \x01(\tR\ttokenType\"\xce\x04\n" + + "token_type\x18\x03 \x01(\tR\ttokenTypeJ\x04\b\x02\x10\x03R\n" + + "expires_in\"\xce\x04\n" + "\x13UpdateConfigRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + "\x06policy\x18\x02 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x1f\n" + @@ -14761,32 +14758,33 @@ const file_openshell_proto_rawDesc = "" + "\x06status\x18\x03 \x01(\x0e2\x1a.openshell.v1.PolicyStatusR\x06status\x12\x1d\n" + "\n" + "load_error\x18\x04 \x01(\tR\tloadError\"\x1c\n" + - "\x1aReportPolicyStatusResponse\"\xbc\x03\n" + + "\x1aReportPolicyStatusResponse\"\x9b\x04\n" + "\x15SandboxPolicyRevision\x12\x18\n" + "\aversion\x18\x01 \x01(\rR\aversion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + "policyHash\x122\n" + "\x06status\x18\x03 \x01(\x0e2\x1a.openshell.v1.PolicyStatusR\x06status\x12\x1d\n" + "\n" + - "load_error\x18\x04 \x01(\tR\tloadError\x12\"\n" + - "\rcreated_at_ms\x18\x05 \x01(\x03R\vcreatedAtMs\x12 \n" + - "\floaded_at_ms\x18\x06 \x01(\x03R\n" + - "loadedAtMs\x12;\n" + + "load_error\x18\x04 \x01(\tR\tloadError\x12=\n" + + "\fcreated_time\x18i \x01(\v2\x1a.google.protobuf.TimestampR\vcreatedTime\x12;\n" + + "\vloaded_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "loadedTime\x12;\n" + "\x06policy\x18\a \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12S\n" + "\n" + "provenance\x18\b \x03(\v23.openshell.v1.SandboxPolicyRevision.ProvenanceEntryR\n" + "provenance\x1a=\n" + "\x0fProvenanceEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbc\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x05\x10\x06J\x04\b\x06\x10\aR\rcreated_at_msR\floaded_at_ms\"\xec\x01\n" + "\x15GetSandboxLogsRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x14\n" + - "\x05lines\x18\x02 \x01(\rR\x05lines\x12\x19\n" + - "\bsince_ms\x18\x03 \x01(\x03R\asinceMs\x12\x18\n" + + "\x05lines\x18\x02 \x01(\rR\x05lines\x129\n" + + "\n" + + "since_time\x18g \x01(\v2\x1a.google.protobuf.TimestampR\tsinceTime\x12\x18\n" + "\asources\x18\x04 \x03(\tR\asources\x12\x1b\n" + "\tmin_level\x18\x05 \x01(\tR\bminLevel\x12\x1c\n" + - "\tworkspace\x18\x06 \x01(\tR\tworkspace\"i\n" + + "\tworkspace\x18\x06 \x01(\tR\tworkspaceJ\x04\b\x03\x10\x04R\bsince_ms\"i\n" + "\x16PushSandboxLogsRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x120\n" + @@ -14815,11 +14813,11 @@ const file_openshell_proto_rawDesc = "" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1f\n" + "\vinstance_id\x18\x02 \x01(\tR\n" + - "instanceId\"h\n" + + "instanceId\"\x99\x01\n" + "\x0fSessionAccepted\x12\x1d\n" + "\n" + - "session_id\x18\x01 \x01(\tR\tsessionId\x126\n" + - "\x17heartbeat_interval_secs\x18\x02 \x01(\rR\x15heartbeatIntervalSecs\")\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12H\n" + + "\x12heartbeat_interval\x18f \x01(\v2\x19.google.protobuf.DurationR\x11heartbeatIntervalJ\x04\b\x02\x10\x03R\x17heartbeat_interval_secs\")\n" + "\x0fSessionRejected\x12\x16\n" + "\x06reason\x18\x01 \x01(\tR\x06reason\"\x15\n" + "\x13SupervisorHeartbeat\"\x12\n" + @@ -14871,7 +14869,7 @@ const file_openshell_proto_rawDesc = "" + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x1a\n" + "\bdecision\x18\x03 \x01(\tR\bdecision\x12\x14\n" + - "\x05count\x18\x04 \x01(\rR\x05count\"\xe5\x04\n" + + "\x05count\x18\x04 \x01(\rR\x05count\"\xce\x05\n" + "\rDenialSummary\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" + @@ -14880,10 +14878,9 @@ const file_openshell_proto_rawDesc = "" + "\x06binary\x18\x04 \x01(\tR\x06binary\x12\x1c\n" + "\tancestors\x18\x05 \x03(\tR\tancestors\x12\x1f\n" + "\vdeny_reason\x18\x06 \x01(\tR\n" + - "denyReason\x12\"\n" + - "\rfirst_seen_ms\x18\a \x01(\x03R\vfirstSeenMs\x12 \n" + - "\flast_seen_ms\x18\b \x01(\x03R\n" + - "lastSeenMs\x12\x14\n" + + "denyReason\x12B\n" + + "\x0ffirst_seen_time\x18k \x01(\v2\x1a.google.protobuf.TimestampR\rfirstSeenTime\x12@\n" + + "\x0elast_seen_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\flastSeenTime\x12\x14\n" + "\x05count\x18\t \x01(\rR\x05count\x12)\n" + "\x10suppressed_count\x18\n" + " \x01(\rR\x0fsuppressedCount\x12\x1f\n" + @@ -14896,7 +14893,7 @@ const file_openshell_proto_rawDesc = "" + "persistent\x12!\n" + "\fdenial_stage\x18\x0f \x01(\tR\vdenialStage\x12K\n" + "\x12l7_request_samples\x18\x10 \x03(\v2\x1d.openshell.v1.L7RequestSampleR\x10l7RequestSamples\x120\n" + - "\x14l7_inspection_active\x18\x11 \x01(\bR\x12l7InspectionActive\"T\n" + + "\x14l7_inspection_active\x18\x11 \x01(\bR\x12l7InspectionActiveJ\x04\b\a\x10\bJ\x04\b\b\x10\tR\rfirst_seen_msR\flast_seen_ms\"T\n" + "\x10DenialGroupCount\x12\x1d\n" + "\n" + "deny_group\x18\x01 \x01(\tR\tdenyGroup\x12!\n" + @@ -14904,7 +14901,7 @@ const file_openshell_proto_rawDesc = "" + "\x16NetworkActivitySummary\x124\n" + "\x16network_activity_count\x18\x01 \x01(\rR\x14networkActivityCount\x12.\n" + "\x13denied_action_count\x18\x02 \x01(\rR\x11deniedActionCount\x12H\n" + - "\x10denials_by_group\x18\x03 \x03(\v2\x1e.openshell.v1.DenialGroupCountR\x0edenialsByGroup\"\xb0\b\n" + + "\x10denials_by_group\x18\x03 \x03(\v2\x1e.openshell.v1.DenialGroupCountR\x0edenialsByGroup\"\xf9\t\n" + "\vPolicyChunk\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + "\x06status\x18\x02 \x01(\tR\x06status\x12\x1b\n" + @@ -14915,16 +14912,14 @@ const file_openshell_proto_rawDesc = "" + "\n" + "confidence\x18\a \x01(\x02R\n" + "confidence\x12,\n" + - "\x12denial_summary_ids\x18\b \x03(\tR\x10denialSummaryIds\x12\"\n" + - "\rcreated_at_ms\x18\t \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rdecided_at_ms\x18\n" + - " \x01(\x03R\vdecidedAtMs\x12\x14\n" + + "\x12denial_summary_ids\x18\b \x03(\tR\x10denialSummaryIds\x12=\n" + + "\fcreated_time\x18m \x01(\v2\x1a.google.protobuf.TimestampR\vcreatedTime\x12=\n" + + "\fdecided_time\x18n \x01(\v2\x1a.google.protobuf.TimestampR\vdecidedTime\x12\x14\n" + "\x05stage\x18\v \x01(\tR\x05stage\x12.\n" + "\x13supersedes_chunk_id\x18\f \x01(\tR\x11supersedesChunkId\x12\x1b\n" + - "\thit_count\x18\r \x01(\x05R\bhitCount\x12\"\n" + - "\rfirst_seen_ms\x18\x0e \x01(\x03R\vfirstSeenMs\x12 \n" + - "\flast_seen_ms\x18\x0f \x01(\x03R\n" + - "lastSeenMs\x12\x16\n" + + "\thit_count\x18\r \x01(\x05R\bhitCount\x12B\n" + + "\x0ffirst_seen_time\x18r \x01(\v2\x1a.google.protobuf.TimestampR\rfirstSeenTime\x12@\n" + + "\x0elast_seen_time\x18s \x01(\v2\x1a.google.protobuf.TimestampR\flastSeenTime\x12\x16\n" + "\x06binary\x18\x10 \x01(\tR\x06binary\x12+\n" + "\x11validation_result\x18\x11 \x01(\tR\x10validationResult\x12)\n" + "\x10rejection_reason\x18\x12 \x01(\tR\x0frejectionReason\x12+\n" + @@ -14933,7 +14928,9 @@ const file_openshell_proto_rawDesc = "" + "\x1dcurrent_effective_policy_hash\x18\x15 \x01(\tR\x1acurrentEffectivePolicyHash\x12E\n" + "\x1fcandidate_effective_policy_hash\x18\x16 \x01(\tR\x1ccandidateEffectivePolicyHash\x12]\n" + "\x18current_effective_policy\x18\x17 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x16currentEffectivePolicy\x12a\n" + - "\x1acandidate_effective_policy\x18\x18 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x18candidateEffectivePolicy\"\x96\x01\n" + + "\x1acandidate_effective_policy\x18\x18 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x18candidateEffectivePolicyJ\x04\b\t\x10\n" + + "J\x04\b\n" + + "\x10\vJ\x04\b\x0e\x10\x0fJ\x04\b\x0f\x10\x10R\rcreated_at_msR\rdecided_at_msR\rfirst_seen_msR\flast_seen_ms\"\x96\x01\n" + "\x11DraftPolicyUpdate\x12#\n" + "\rdraft_version\x18\x01 \x01(\x04R\fdraftVersion\x12\x1d\n" + "\n" + @@ -14955,12 +14952,12 @@ const file_openshell_proto_rawDesc = "" + "\x15GetDraftPolicyRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + "\rstatus_filter\x18\x02 \x01(\tR\fstatusFilter\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"\xc8\x01\n" + + "\tworkspace\x18\x03 \x01(\tR\tworkspace\"\xfe\x01\n" + "\x16GetDraftPolicyResponse\x121\n" + "\x06chunks\x18\x01 \x03(\v2\x19.openshell.v1.PolicyChunkR\x06chunks\x12'\n" + "\x0frolling_summary\x18\x02 \x01(\tR\x0erollingSummary\x12#\n" + - "\rdraft_version\x18\x03 \x01(\x04R\fdraftVersion\x12-\n" + - "\x13last_analyzed_at_ms\x18\x04 \x01(\x03R\x10lastAnalyzedAtMs\"\x8a\x01\n" + + "\rdraft_version\x18\x03 \x01(\x04R\fdraftVersion\x12H\n" + + "\x12last_analyzed_time\x18h \x01(\v2\x1a.google.protobuf.TimestampR\x10lastAnalyzedTimeJ\x04\b\x04\x10\x05R\x13last_analyzed_at_ms\"\x8a\x01\n" + "\x18ApproveDraftChunkRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x1c\n" + @@ -15011,13 +15008,14 @@ const file_openshell_proto_rawDesc = "" + "\x0echunks_cleared\x18\x01 \x01(\rR\rchunksCleared\"J\n" + "\x16GetDraftHistoryRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + - "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\x92\x01\n" + - "\x11DraftHistoryEntry\x12!\n" + - "\ftimestamp_ms\x18\x01 \x01(\x03R\vtimestampMs\x12\x1d\n" + + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\xbe\x01\n" + + "\x11DraftHistoryEntry\x129\n" + + "\n" + + "event_time\x18e \x01(\v2\x1a.google.protobuf.TimestampR\teventTime\x12\x1d\n" + "\n" + "event_type\x18\x02 \x01(\tR\teventType\x12 \n" + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x19\n" + - "\bchunk_id\x18\x04 \x01(\tR\achunkId\"T\n" + + "\bchunk_id\x18\x04 \x01(\tR\achunkIdJ\x04\b\x01\x10\x02R\ftimestamp_ms\"T\n" + "\x17GetDraftHistoryResponse\x129\n" + "\aentries\x18\x01 \x03(\v2\x1f.openshell.v1.DraftHistoryEntryR\aentries\"\xbd\x02\n" + "\x15PolicyRevisionPayload\x12;\n" + @@ -15154,11 +15152,11 @@ const file_openshell_proto_rawDesc = "" + "\x05limit\x18\x02 \x01(\rR\x05limit\x12\x16\n" + "\x06offset\x18\x03 \x01(\rR\x06offset\"W\n" + "\x1cListWorkspaceMembersResponse\x127\n" + - "\amembers\x18\x01 \x03(\v2\x1d.openshell.v1.WorkspaceMemberR\amembers\"\x7f\n" + + "\amembers\x18\x01 \x03(\v2\x1d.openshell.v1.WorkspaceMemberR\amembers\"\xb5\x01\n" + "\x1aExtensionServiceCredential\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1a\n" + - "\x05token\x18\x02 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12\"\n" + - "\rexpires_at_ms\x18\x03 \x01(\x03R\vexpiresAtMs*\xa6\x02\n" + + "\x05token\x18\x02 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12C\n" + + "\x0fexpiration_time\x18g \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTimeJ\x04\b\x03\x10\x04R\rexpires_at_ms*\xa6\x02\n" + "\fSandboxPhase\x12\x1d\n" + "\x19SANDBOX_PHASE_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aSANDBOX_PHASE_PROVISIONING\x10\x01\x12\x17\n" + @@ -15581,14 +15579,14 @@ var file_openshell_proto_goTypes = []any{ nil, // 208: openshell.v1.CreateSandboxRequest.AnnotationsEntry nil, // 209: openshell.v1.ExecSandboxRequest.EnvironmentEntry nil, // 210: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 211: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 211: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry nil, // 212: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry nil, // 213: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry nil, // 214: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry nil, // 215: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry nil, // 216: openshell.v1.ProviderProfile.AnnotationsEntry nil, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry nil, // 219: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry nil, // 220: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry nil, // 221: openshell.v1.UpdateConfigRequest.AnnotationsEntry @@ -15597,333 +15595,368 @@ var file_openshell_proto_goTypes = []any{ nil, // 224: openshell.v1.PolicyRevisionPayload.ProvenanceEntry nil, // 225: openshell.v1.StoredPolicyRevision.ProvenanceEntry nil, // 226: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 227: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 228: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 229: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 230: openshell.datamodel.v1.Provider - (*datamodelv1.CredentialHandle)(nil), // 231: openshell.datamodel.v1.CredentialHandle - (*sandboxv1.NetworkEndpoint)(nil), // 232: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 233: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 234: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 235: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 236: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 237: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 238: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 239: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 240: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 241: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 242: openshell.sandbox.v1.GetGatewayConfigResponse + (*timestamppb.Timestamp)(nil), // 227: google.protobuf.Timestamp + (*datamodelv1.ObjectMeta)(nil), // 228: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 229: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 230: google.protobuf.Struct + (*datamodelv1.Provider)(nil), // 231: openshell.datamodel.v1.Provider + (*durationpb.Duration)(nil), // 232: google.protobuf.Duration + (*datamodelv1.CredentialHandle)(nil), // 233: openshell.datamodel.v1.CredentialHandle + (*sandboxv1.NetworkEndpoint)(nil), // 234: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 235: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 236: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 237: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 238: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 239: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 240: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 241: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 242: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 243: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 244: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 201, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 5, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus - 5, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus - 18, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 19, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 227, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 21, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 25, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 202, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 24, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 228, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 22, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 23, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 203, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 204, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 205, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 229, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 229, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 26, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition - 0, // 19: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 206, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 21, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 207, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 208, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 20, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 20, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 230, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 20, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 20, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 52, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 227, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 51, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 209, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 56, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 57, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 58, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 150, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 151, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 60, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 55, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 63, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 227, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 20, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 67, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 27, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 68, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 161, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 210, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 230, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 230, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 211, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 230, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 230, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 99, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 80, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 1, // 55: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 81, // 56: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 86, // 57: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 82, // 58: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 2, // 59: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 84, // 60: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 85, // 61: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 2, // 62: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 7, // 63: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 227, // 64: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 2, // 65: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 212, // 66: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 213, // 67: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 214, // 68: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - 90, // 69: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion - 7, // 70: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 231, // 71: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle - 87, // 72: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 73: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 215, // 74: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 87, // 75: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 87, // 76: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 3, // 77: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 83, // 78: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 232, // 79: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 233, // 80: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 88, // 81: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 216, // 82: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 227, // 83: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 99, // 84: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile - 99, // 85: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 99, // 86: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 78, // 87: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 88: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 99, // 89: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 78, // 90: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 91: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 99, // 92: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 78, // 93: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 94: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 113, // 95: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 217, // 96: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 218, // 97: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 219, // 98: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 220, // 99: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 228, // 100: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 234, // 101: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 119, // 102: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 221, // 103: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 120, // 104: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 121, // 105: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 122, // 106: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 123, // 107: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 124, // 108: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 125, // 109: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 235, // 110: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 236, // 111: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 237, // 112: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 222, // 113: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 133, // 114: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 133, // 115: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 4, // 116: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 4, // 117: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 228, // 118: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 223, // 119: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 67, // 120: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 67, // 121: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 140, // 122: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 143, // 123: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 154, // 124: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 155, // 125: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 141, // 126: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 142, // 127: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 144, // 128: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 149, // 129: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 155, // 130: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 150, // 131: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 151, // 132: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 152, // 133: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 156, // 134: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 158, // 135: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 235, // 136: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 228, // 137: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 228, // 138: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 157, // 139: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 160, // 140: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 159, // 141: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 160, // 142: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 170, // 143: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 235, // 144: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 180, // 145: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 228, // 146: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 224, // 147: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 235, // 148: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 228, // 149: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 228, // 150: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 225, // 151: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 228, // 152: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 228, // 153: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 154: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 238, // 155: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 238, // 156: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 238, // 157: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 227, // 158: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 159: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 6, // 160: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 194, // 161: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 194, // 162: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 231, // 163: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle - 83, // 164: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 114, // 165: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 12, // 166: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 14, // 167: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 16, // 168: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 28, // 169: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 29, // 170: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 30, // 171: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 31, // 172: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 32, // 173: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 33, // 174: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 34, // 175: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 35, // 176: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 36, // 177: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 43, // 178: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 45, // 179: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 46, // 180: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 47, // 181: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 49, // 182: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 53, // 183: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 55, // 184: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 61, // 185: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 62, // 186: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 69, // 187: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 70, // 188: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 71, // 189: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 76, // 190: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 77, // 191: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 103, // 192: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 105, // 193: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 107, // 194: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 72, // 195: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 91, // 196: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 93, // 197: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 95, // 198: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 97, // 199: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 73, // 200: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 110, // 201: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 239, // 202: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 240, // 203: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 118, // 204: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 127, // 205: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 129, // 206: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 131, // 207: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 112, // 208: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 116, // 209: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 134, // 210: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 135, // 211: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 138, // 212: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 145, // 213: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 147, // 214: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 153, // 215: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 65, // 216: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 162, // 217: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 164, // 218: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 166, // 219: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 168, // 220: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 171, // 221: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 173, // 222: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 175, // 223: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 177, // 224: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 179, // 225: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 8, // 226: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 10, // 227: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 186, // 228: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 188, // 229: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 190, // 230: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 192, // 231: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 195, // 232: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 197, // 233: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 199, // 234: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 13, // 235: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 15, // 236: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 17, // 237: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 37, // 238: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 239: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 38, // 240: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 39, // 241: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 40, // 242: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 41, // 243: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 42, // 244: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 37, // 245: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 246: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 44, // 247: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 52, // 248: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 52, // 249: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 48, // 250: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 50, // 251: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 54, // 252: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 59, // 253: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 61, // 254: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 59, // 255: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 74, // 256: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 74, // 257: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 75, // 258: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 102, // 259: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 101, // 260: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 104, // 261: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 106, // 262: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 108, // 263: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 74, // 264: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 92, // 265: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 94, // 266: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 96, // 267: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 98, // 268: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 109, // 269: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 111, // 270: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 241, // 271: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 242, // 272: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 126, // 273: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 128, // 274: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 130, // 275: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 132, // 276: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 115, // 277: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 117, // 278: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 137, // 279: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 136, // 280: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 139, // 281: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 146, // 282: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 148, // 283: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 153, // 284: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 66, // 285: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 163, // 286: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 165, // 287: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 167, // 288: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 169, // 289: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 172, // 290: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 174, // 291: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 176, // 292: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 178, // 293: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 181, // 294: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 9, // 295: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 11, // 296: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 187, // 297: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 189, // 298: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 191, // 299: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 193, // 300: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 196, // 301: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 198, // 302: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 200, // 303: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 235, // [235:304] is the sub-list for method output_type - 166, // [166:235] is the sub-list for method input_type - 166, // [166:166] is the sub-list for extension type_name - 166, // [166:166] is the sub-list for extension extendee - 0, // [0:166] is the sub-list for field type_name + 227, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 227, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 201, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 5, // 3: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus + 5, // 4: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus + 18, // 5: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 19, // 6: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 228, // 7: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 21, // 8: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 25, // 9: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 202, // 10: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 24, // 11: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 229, // 12: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 22, // 13: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 23, // 14: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 203, // 15: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 204, // 16: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 205, // 17: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 230, // 18: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 230, // 19: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 26, // 20: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 0, // 21: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase + 227, // 22: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp + 227, // 23: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp + 206, // 24: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 21, // 25: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 207, // 26: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 208, // 27: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 20, // 28: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 20, // 29: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 231, // 30: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 20, // 31: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 20, // 32: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 227, // 33: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 52, // 34: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 228, // 35: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 51, // 36: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 209, // 37: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 232, // 38: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 56, // 39: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 57, // 40: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 58, // 41: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 150, // 42: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 151, // 43: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 60, // 44: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 55, // 45: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 63, // 46: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 228, // 47: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 227, // 48: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 227, // 49: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 20, // 50: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 67, // 51: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 27, // 52: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 68, // 53: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 161, // 54: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 227, // 55: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 210, // 56: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 231, // 57: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 231, // 58: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 211, // 59: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 231, // 60: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 231, // 61: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 99, // 62: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 232, // 63: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 80, // 64: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 1, // 65: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 81, // 66: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 86, // 67: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 82, // 68: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 2, // 69: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 232, // 70: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 232, // 71: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 84, // 72: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 85, // 73: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 2, // 74: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 227, // 75: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 227, // 76: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 227, // 77: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 7, // 78: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 227, // 79: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 228, // 80: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 2, // 81: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 212, // 82: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 213, // 83: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 214, // 84: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + 90, // 85: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion + 7, // 86: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 233, // 87: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle + 87, // 88: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 2, // 89: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 215, // 90: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 227, // 91: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 87, // 92: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 87, // 93: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 3, // 94: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 83, // 95: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 234, // 96: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 235, // 97: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 88, // 98: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 216, // 99: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 228, // 100: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 99, // 101: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile + 99, // 102: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 99, // 103: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 78, // 104: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 79, // 105: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 99, // 106: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 78, // 107: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 79, // 108: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 99, // 109: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 78, // 110: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 79, // 111: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 113, // 112: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 217, // 113: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 218, // 114: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 219, // 115: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 220, // 116: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 232, // 117: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 229, // 118: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 236, // 119: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 119, // 120: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 221, // 121: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 120, // 122: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 121, // 123: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 122, // 124: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 123, // 125: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 124, // 126: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 125, // 127: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 237, // 128: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 238, // 129: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 239, // 130: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 222, // 131: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 133, // 132: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 133, // 133: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 4, // 134: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 4, // 135: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 227, // 136: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 227, // 137: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 229, // 138: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 223, // 139: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 227, // 140: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 67, // 141: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 67, // 142: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 140, // 143: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 143, // 144: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 154, // 145: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 155, // 146: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 141, // 147: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 142, // 148: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 144, // 149: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 149, // 150: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 155, // 151: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 232, // 152: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 150, // 153: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 151, // 154: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 152, // 155: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 227, // 156: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 227, // 157: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 156, // 158: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 158, // 159: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 237, // 160: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 227, // 161: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 227, // 162: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 227, // 163: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 227, // 164: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 229, // 165: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 229, // 166: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 157, // 167: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 160, // 168: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 159, // 169: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 160, // 170: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 227, // 171: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 170, // 172: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 237, // 173: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 227, // 174: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 180, // 175: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 229, // 176: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 224, // 177: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 237, // 178: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 229, // 179: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 229, // 180: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 225, // 181: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 229, // 182: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 229, // 183: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 226, // 184: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 240, // 185: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 240, // 186: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 240, // 187: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 228, // 188: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 189: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 6, // 190: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 194, // 191: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 194, // 192: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 227, // 193: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 227, // 194: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 233, // 195: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 227, // 196: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 83, // 197: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 114, // 198: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 12, // 199: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 14, // 200: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 16, // 201: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 28, // 202: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 29, // 203: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 30, // 204: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 31, // 205: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 32, // 206: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 33, // 207: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 34, // 208: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 35, // 209: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 36, // 210: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 43, // 211: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 45, // 212: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 46, // 213: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 47, // 214: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 49, // 215: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 53, // 216: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 55, // 217: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 61, // 218: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 62, // 219: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 69, // 220: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 70, // 221: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 71, // 222: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 76, // 223: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 77, // 224: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 103, // 225: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 105, // 226: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 107, // 227: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 72, // 228: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 91, // 229: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 93, // 230: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 95, // 231: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 97, // 232: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 73, // 233: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 110, // 234: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 241, // 235: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 242, // 236: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 118, // 237: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 127, // 238: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 129, // 239: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 131, // 240: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 112, // 241: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 116, // 242: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 134, // 243: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 135, // 244: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 138, // 245: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 145, // 246: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 147, // 247: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 153, // 248: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 65, // 249: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 162, // 250: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 164, // 251: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 166, // 252: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 168, // 253: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 171, // 254: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 173, // 255: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 175, // 256: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 177, // 257: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 179, // 258: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 8, // 259: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 10, // 260: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 186, // 261: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 188, // 262: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 190, // 263: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 192, // 264: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 195, // 265: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 197, // 266: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 199, // 267: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 13, // 268: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 15, // 269: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 17, // 270: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 37, // 271: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 37, // 272: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 38, // 273: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 39, // 274: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 40, // 275: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 41, // 276: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 42, // 277: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 37, // 278: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 37, // 279: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 44, // 280: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 52, // 281: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 52, // 282: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 48, // 283: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 50, // 284: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 54, // 285: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 59, // 286: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 61, // 287: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 59, // 288: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 74, // 289: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 74, // 290: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 75, // 291: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 102, // 292: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 101, // 293: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 104, // 294: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 106, // 295: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 108, // 296: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 74, // 297: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 92, // 298: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 94, // 299: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 96, // 300: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 98, // 301: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 109, // 302: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 111, // 303: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 243, // 304: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 244, // 305: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 126, // 306: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 128, // 307: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 130, // 308: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 132, // 309: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 115, // 310: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 117, // 311: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 137, // 312: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 136, // 313: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 139, // 314: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 146, // 315: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 148, // 316: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 153, // 317: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 66, // 318: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 163, // 319: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 165, // 320: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 167, // 321: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 169, // 322: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 172, // 323: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 174, // 324: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 176, // 325: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 178, // 326: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 181, // 327: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 9, // 328: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 11, // 329: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 187, // 330: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 189, // 331: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 191, // 332: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 193, // 333: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 196, // 334: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 198, // 335: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 200, // 336: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 268, // [268:337] is the sub-list for method output_type + 199, // [199:268] is the sub-list for method input_type + 199, // [199:199] is the sub-list for extension type_name + 199, // [199:199] is the sub-list for extension extendee + 0, // [0:199] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -15959,7 +15992,6 @@ func file_openshell_proto_init() { (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[85].OneofWrappers = []any{} file_openshell_proto_msgTypes[111].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), diff --git a/sdk/go/proto/sandboxv1/sandbox.pb.go b/sdk/go/proto/sandboxv1/sandbox.pb.go index 8da143ebaa..6f57f20f46 100644 --- a/sdk/go/proto/sandboxv1/sandbox.pb.go +++ b/sdk/go/proto/sandboxv1/sandbox.pb.go @@ -12,6 +12,7 @@ package sandboxv1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" @@ -1955,10 +1956,9 @@ type SupervisorMiddlewareService struct { // Operator-owned logical payload limit applied to every binding exposed by // the service. This caps HTTP bodies and complete WebSocket messages. MaxPayloadBytes uint64 `protobuf:"varint,3,opt,name=max_payload_bytes,json=maxPayloadBytes,proto3" json:"max_payload_bytes,omitempty"` - // Default RPC timeout for this service. Empty uses the platform default of - // 500ms. Values use an integer with an `ms` or `s` suffix and must be - // between 10ms and 30s. - Timeout string `protobuf:"bytes,4,opt,name=timeout,proto3" json:"timeout,omitempty"` + // Default RPC timeout for this service. Absence uses the platform default of + // 500ms. Values must be between 10ms and 30s. + RequestTimeout *durationpb.Duration `protobuf:"bytes,104,opt,name=request_timeout,json=requestTimeout,proto3" json:"request_timeout,omitempty"` // PEM-encoded trust roots loaded by the gateway from the operator-configured // tls_ca_cert_path. Empty uses the platform trust store. TlsCaCertPem []byte `protobuf:"bytes,5,opt,name=tls_ca_cert_pem,json=tlsCaCertPem,proto3" json:"tls_ca_cert_pem,omitempty"` @@ -2026,11 +2026,11 @@ func (x *SupervisorMiddlewareService) GetMaxPayloadBytes() uint64 { return 0 } -func (x *SupervisorMiddlewareService) GetTimeout() string { +func (x *SupervisorMiddlewareService) GetRequestTimeout() *durationpb.Duration { if x != nil { - return x.Timeout + return x.RequestTimeout } - return "" + return nil } func (x *SupervisorMiddlewareService) GetTlsCaCertPem() []byte { @@ -2058,7 +2058,7 @@ var File_sandbox_proto protoreflect.FileDescriptor const file_sandbox_proto_rawDesc = "" + "\n" + - "\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xa8\x05\n" + + "\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1egoogle/protobuf/duration.proto\"\xa8\x05\n" + "\rSandboxPolicy\x12\x18\n" + "\aversion\x18\x01 \x01(\rR\aversion\x12F\n" + "\n" + @@ -2226,15 +2226,15 @@ const file_sandbox_proto_rawDesc = "" + " extension_authentication_enabled\x18\f \x01(\bR\x1eextensionAuthenticationEnabled\x1ac\n" + "\rSettingsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12<\n" + - "\x05value\x18\x02 \x01(\v2&.openshell.sandbox.v1.EffectiveSettingR\x05value:\x028\x01\"\x99\x02\n" + + "\x05value\x18\x02 \x01(\v2&.openshell.sandbox.v1.EffectiveSettingR\x05value:\x028\x01\"\xd2\x02\n" + "\x1bSupervisorMiddlewareService\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + "\rgrpc_endpoint\x18\x02 \x01(\tR\fgrpcEndpoint\x12*\n" + - "\x11max_payload_bytes\x18\x03 \x01(\x04R\x0fmaxPayloadBytes\x12\x18\n" + - "\atimeout\x18\x04 \x01(\tR\atimeout\x12%\n" + + "\x11max_payload_bytes\x18\x03 \x01(\x04R\x0fmaxPayloadBytes\x12B\n" + + "\x0frequest_timeout\x18h \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeout\x12%\n" + "\x0ftls_ca_cert_pem\x18\x05 \x01(\fR\ftlsCaCertPem\x12\x1a\n" + "\baudience\x18\x06 \x01(\tR\baudience\x128\n" + - "\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransport*b\n" + + "\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransportJ\x04\b\x04\x10\x05R\atimeout*b\n" + "\fSettingScope\x12\x1d\n" + "\x19SETTING_SCOPE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15SETTING_SCOPE_SANDBOX\x10\x01\x12\x18\n" + @@ -2294,6 +2294,7 @@ var file_sandbox_proto_goTypes = []any{ nil, // 32: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry nil, // 33: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry (*structpb.Struct)(nil), // 34: google.protobuf.Struct + (*durationpb.Duration)(nil), // 35: google.protobuf.Duration } var file_sandbox_proto_depIdxs = []int32{ 3, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy @@ -2322,20 +2323,21 @@ var file_sandbox_proto_depIdxs = []int32{ 33, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry 1, // 24: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource 24, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService - 6, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 7, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig - 12, // 28: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation - 16, // 29: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 30: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 31: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 32: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 21, // 33: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue - 22, // 34: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting - 35, // [35:35] is the sub-list for method output_type - 35, // [35:35] is the sub-list for method input_type - 35, // [35:35] is the sub-list for extension type_name - 35, // [35:35] is the sub-list for extension extendee - 0, // [0:35] is the sub-list for field type_name + 35, // 26: openshell.sandbox.v1.SupervisorMiddlewareService.request_timeout:type_name -> google.protobuf.Duration + 6, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 7, // 28: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig + 12, // 29: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation + 16, // 30: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 16, // 31: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 16, // 32: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 16, // 33: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 21, // 34: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue + 22, // 35: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting + 36, // [36:36] is the sub-list for method output_type + 36, // [36:36] is the sub-list for method input_type + 36, // [36:36] is the sub-list for extension type_name + 36, // [36:36] is the sub-list for extension extendee + 0, // [0:36] is the sub-list for field type_name } func init() { file_sandbox_proto_init() } diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index 29a7438c69..8276fbe9c1 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -701,7 +701,7 @@ describe('ssh sessions', () => { gatewayPort: 8443, gatewayScheme: 'https', hostKeyFingerprint: 'SHA256:abc', - expiresAtMs: 1730000000000n, + expirationTime: { seconds: 1730000000n, nanos: 0 }, }), }); const session = await withExpiry.createSshSession('sb'); @@ -724,7 +724,7 @@ describe('ssh sessions', () => { gatewayPort: 80, gatewayScheme: 'http', hostKeyFingerprint: '', - expiresAtMs: 0n, + expirationTime: undefined, }), }); const bare = await noExpiry.createSshSession('sb'); @@ -745,7 +745,7 @@ describe('ssh sessions', () => { gatewayPort: 8443, gatewayScheme: 'https', hostKeyFingerprint: 'SHA256:abc', - expiresAtMs: 0n, + expirationTime: undefined, }; const cases: Array> = [ { ...base, sandboxId: 'different-sandbox' }, @@ -779,7 +779,7 @@ describe('ssh sessions', () => { gatewayPort: 443, gatewayScheme: 'https', hostKeyFingerprint: '', - expiresAtMs: 0n, + expirationTime: undefined, }), }); await expect(sandbox.createSshSession('sb')).resolves.toMatchObject({ gatewayHost }); @@ -803,7 +803,7 @@ describe('forward', () => { gatewayPort: 443, gatewayScheme: 'https', hostKeyFingerprint: '', - expiresAtMs: 0n, + expirationTime: undefined, }; }, revokeSshSession: (req) => { @@ -886,7 +886,7 @@ describe('forward', () => { gatewayPort: 443, gatewayScheme: 'https', hostKeyFingerprint: '', - expiresAtMs: 0n, + expirationTime: undefined, }), revokeSshSession: () => ({ revoked: true }), // Ignore inbound frames; just blast a large, verifiable byte stream back. @@ -947,7 +947,7 @@ describe('forward', () => { gatewayPort: 443, gatewayScheme: 'https', hostKeyFingerprint: '', - expiresAtMs: 0n, + expirationTime: undefined, }; }, // biome-ignore lint/correctness/useYield: the socket is reset before any frame is relayed @@ -1017,7 +1017,7 @@ describe('forward', () => { gatewayPort: 443, gatewayScheme: 'https', hostKeyFingerprint: '', - expiresAtMs: 0n, + expirationTime: undefined, }), forwardTcp: async function* (_requests, ctx) { streamStarted(); diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index d2bf520800..3d0ba3d5a7 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -14,6 +14,7 @@ import type { AddressInfo } from 'node:net'; import * as net from 'node:net'; import type { MessageInitShape } from '@bufbuild/protobuf'; +import { durationFromMs } from '@bufbuild/protobuf/wkt'; import { type CallOptions, type Client, createClient, type Transport } from '@connectrpc/connect'; import { errorCode, fromConnect, SdkError } from './errors.js'; import type { Provider } from './gen/datamodel_pb.js'; @@ -31,6 +32,16 @@ import { PolicySource, type SandboxPolicySchema, SettingScope, type SettingValue import { validateSshResponse } from './ssh-validate.js'; import { buildTransport, type ConnectOptions } from './transport.js'; +function durationFromSeconds(seconds: number) { + return seconds > 0 ? durationFromMs(seconds * 1000) : undefined; +} + +function timestampMillis(timestamp: { seconds: bigint; nanos: number } | undefined): string | undefined { + if (!timestamp) return undefined; + const millis = timestamp.seconds * 1000n + BigInt(Math.trunc(timestamp.nanos / 1_000_000)); + return millis === 0n ? undefined : millis.toString(); +} + // The policy and setting value shapes are the generated protobuf messages; // re-export them rather than re-curating a parallel surface. Callers round-trip // `getConfig().policy` back into `setPolicy`, and build `SettingValue`s inline. @@ -694,7 +705,7 @@ export class SandboxClient { command, workdir: options?.workdir ?? '', environment: options?.environment ?? {}, - timeoutSeconds: options?.timeoutSecs ?? 0, + executionTimeout: durationFromSeconds(options?.timeoutSecs ?? 0), stdin: options?.stdin ? new Uint8Array(options.stdin) : new Uint8Array(), tty: false, noLoginShell: options?.noLoginShell ?? false, @@ -775,7 +786,7 @@ export class SandboxClient { command, workdir: options?.workdir ?? '', environment: options?.environment ?? {}, - timeoutSeconds: options?.timeoutSecs ?? 0, + executionTimeout: durationFromSeconds(options?.timeoutSecs ?? 0), stdin: new Uint8Array(), tty: options?.tty ?? true, cols: options?.cols ?? 0, @@ -1075,7 +1086,7 @@ export class SandboxClient { gatewayPort: resp.gatewayPort, gatewayScheme: resp.gatewayScheme, ...(resp.hostKeyFingerprint ? { hostKeyFingerprint: resp.hostKeyFingerprint } : {}), - ...(resp.expiresAtMs !== 0n ? { expiresAtMs: resp.expiresAtMs.toString() } : {}), + ...(timestampMillis(resp.expirationTime) ? { expiresAtMs: timestampMillis(resp.expirationTime) } : {}), }; } catch (e) { throw e instanceof SdkError ? e : fromConnect(e);