Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 9 additions & 23 deletions src/handlers/agent/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ use std::{
};

use anyhow::{Context, Result};
use chrono::{DateTime, SecondsFormat, Utc};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use tabled::Tabled;

use crate::utils::{interaction, spinner::request_spinner};
use crate::utils::{
human_datetime::{format_utc, format_utc_timestamp},
interaction,
spinner::request_spinner,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalAgentSession {
Expand All @@ -36,7 +40,7 @@ impl LocalAgentSessionGuard {

let session = LocalAgentSession {
session_id: session_id.clone(),
started_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
started_at: format_utc(Utc::now()),
process_id: std::process::id(),
process_started_at: process_start_time(std::process::id())?,
revoked: false,
Expand Down Expand Up @@ -196,12 +200,6 @@ impl From<LocalAgentSession> for AgentSessionRow {
}
}

fn format_utc_timestamp(value: &str) -> Result<String> {
Ok(DateTime::parse_from_rfc3339(value)?
.with_timezone(&Utc)
.to_rfc3339_opts(SecondsFormat::Secs, true))
}

pub fn format_sessions(rows: &Vec<AgentSessionRow>, json: bool) -> Result<()> {
if json {
println!(
Expand All @@ -219,8 +217,8 @@ pub fn format_sessions(rows: &Vec<AgentSessionRow>, json: bool) -> Result<()> {
#[cfg(test)]
mod tests {
use super::{
format_utc_timestamp, is_valid_agent_session_id, process_start_time,
should_confirm_bulk_revoke, validate_bulk_scope, AgentSessionRow,
is_valid_agent_session_id, process_start_time, should_confirm_bulk_revoke,
validate_bulk_scope, AgentSessionRow,
};

#[test]
Expand All @@ -231,18 +229,6 @@ mod tests {
assert_eq!(process_start_time(pid).unwrap(), first);
}

#[test]
fn session_timestamps_are_utc_seconds() {
assert_eq!(
format_utc_timestamp("2026-09-13T11:53:59.237435+00:00").unwrap(),
"2026-09-13T11:53:59Z"
);
assert_eq!(
format_utc_timestamp("2026-07-03T16:10:07+02:00").unwrap(),
"2026-07-03T14:10:07Z"
);
}

#[test]
fn session_rows_do_not_include_expiration() {
let row = AgentSessionRow {
Expand Down
26 changes: 24 additions & 2 deletions src/handlers/run/docker_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,8 @@ pub(crate) struct ExistingRunNetwork {
/// `None` for the pre-session-naming fallback (a bare UUID) — see
/// `generate_run_network_name`.
pub session_id: Option<String>,
/// RFC 3339 creation timestamp, straight from `docker network inspect`.
/// Creation timestamp as UTC RFC 3339 (see `format_utc`), or empty when
/// `docker network inspect` couldn't report it.
pub created_at: String,
}

Expand Down Expand Up @@ -453,7 +454,7 @@ pub(crate) fn list_run_networks() -> Result<Vec<ExistingRunNetwork>, String> {
.output()
.map_err(|error| format!("failed to run `docker network inspect`: {error}"))?;
let created_at = if inspect.status.success() {
String::from_utf8_lossy(&inspect.stdout).trim().to_owned()
normalize_network_created_at(&String::from_utf8_lossy(&inspect.stdout))
} else {
// The network could have been removed between the `ls` and
// this `inspect` (e.g. a concurrent run finishing normally)
Expand Down Expand Up @@ -647,6 +648,13 @@ pub(crate) const STALE_IMAGE_AFTER_DAYS: i64 = 14;
/// When the local image for `source` was built, or `None` if it isn't
/// built (or `docker` can't say). Not meaningful for a plain image
/// reference, which `docker run` pulls on demand.
/// Turns `docker network inspect --format '{{.Created}}'` output into a UTC
/// RFC 3339 timestamp, keeping the raw value if it doesn't parse.
fn normalize_network_created_at(output: &str) -> String {
let raw = output.trim().trim_matches('"');
crate::utils::human_datetime::display_utc_timestamp(raw)
}

pub(crate) fn sandbox_image_created_at(
source: &AgentImageSource,
) -> Option<chrono::DateTime<chrono::Utc>> {
Expand Down Expand Up @@ -1831,6 +1839,20 @@ mod tests {
assert!(arg_line < install_line);
}

#[test]
fn network_created_at_is_normalized_to_utc() {
// Docker Engine exposes `.Created` as an RFC 3339 string.
assert_eq!(
normalize_network_created_at("2026-10-09T09:57:15.791491833+02:00\n"),
"2026-10-09T07:57:15Z"
);
assert_eq!(
normalize_network_created_at("2026-10-09T07:57:15.791491833Z"),
"2026-10-09T07:57:15Z"
);
assert_eq!(normalize_network_created_at("not-a-date"), "not-a-date");
}

#[test]
fn format_image_age_reads_naturally() {
let now = chrono::Utc::now();
Expand Down
15 changes: 9 additions & 6 deletions src/handlers/secrets/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
validation::InputValidationError,
},
utils::{
human_datetime::display_utc_timestamp,
output::{get_formatted_json_string, ColorizeIfColoredOutput},
spinner::request_spinner,
tables::build::build_table,
Expand Down Expand Up @@ -257,7 +258,8 @@ fn print_secret_metadata_list(secret_metadata: Vec<SecretMetadata>, format: &Sec
.iter()
.map(|s| {
let comment = s.comment.clone().unwrap_or_default();
let last_accessed_at = s.last_accessed_at.clone().unwrap_or_default();
let last_accessed_at =
display_utc_timestamp(&s.last_accessed_at.clone().unwrap_or_default());

format!(
"{}\n{} {}\n{} {}\n{} {}\n{} {}\n{} {}\n{} {}",
Expand All @@ -269,9 +271,9 @@ fn print_secret_metadata_list(secret_metadata: Vec<SecretMetadata>, format: &Sec
"has_value:".blue_if_tty(),
s.has_value,
"created_at:".blue_if_tty(),
s.created_at,
display_utc_timestamp(&s.created_at),
"updated_at:".blue_if_tty(),
s.updated_at,
display_utc_timestamp(&s.updated_at),
"last_accessed_at:".blue_if_tty(),
last_accessed_at,
)
Expand Down Expand Up @@ -317,7 +319,8 @@ fn print_secret_metadata(secret_metadata: SecretMetadata, format: &SecretsOutput
}
SecretsOutputFormat::Plain | SecretsOutputFormat::Dotenv => {
let comment = secret_metadata.comment.unwrap_or_default();
let last_accessed_at = secret_metadata.last_accessed_at.unwrap_or_default();
let last_accessed_at =
display_utc_timestamp(&secret_metadata.last_accessed_at.unwrap_or_default());

println!(
"{}\n{} {}\n{} {}\n{} {}\n{} {}\n{} {}\n{} {}",
Expand All @@ -329,9 +332,9 @@ fn print_secret_metadata(secret_metadata: SecretMetadata, format: &SecretsOutput
"has_value:".blue_if_tty(),
secret_metadata.has_value,
"created_at:".blue_if_tty(),
secret_metadata.created_at,
display_utc_timestamp(&secret_metadata.created_at),
"updated_at:".blue_if_tty(),
secret_metadata.updated_at,
display_utc_timestamp(&secret_metadata.updated_at),
"last_accessed_at:".blue_if_tty(),
last_accessed_at,
);
Expand Down
13 changes: 7 additions & 6 deletions src/models/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
cmd::config::SecretsOutputFormat,
utils::{
self,
human_datetime::display_utc_timestamp,
output::{is_color_enabled, write_indented, ColorizeIfColoredOutput},
},
};
Expand Down Expand Up @@ -187,9 +188,9 @@ impl From<SecretMetadata> for SecretMetadataTable {
comment: value.comment.unwrap_or_default(),
version: value.version,
has_value: value.has_value,
created_at: value.created_at,
updated_at: value.updated_at,
last_accessed_at: value.last_accessed_at.unwrap_or_default(),
created_at: display_utc_timestamp(&value.created_at),
updated_at: display_utc_timestamp(&value.updated_at),
last_accessed_at: display_utc_timestamp(&value.last_accessed_at.unwrap_or_default()),
}
}
}
Expand All @@ -200,9 +201,9 @@ impl From<SecretMetadata> for SecretMetadataTableWithoutComment {
name: value.name,
version: value.version,
has_value: value.has_value,
created_at: value.created_at,
updated_at: value.updated_at,
last_accessed_at: value.last_accessed_at.unwrap_or_default(),
created_at: display_utc_timestamp(&value.created_at),
updated_at: display_utc_timestamp(&value.updated_at),
last_accessed_at: display_utc_timestamp(&value.last_accessed_at.unwrap_or_default()),
}
}
}
Expand Down
66 changes: 57 additions & 9 deletions src/utils/human_datetime.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
use chrono::prelude::DateTime;
use chrono::Local;
use anyhow::Result;
use chrono::{DateTime, SecondsFormat, Utc};
use chrono_humanize::HumanTime;
use log::debug;

/// Formats an instant the way every absolute date in the CLI is shown:
/// UTC, whole seconds, RFC 3339 with a `Z` (e.g. `2026-09-13T11:53:59Z`).
pub fn format_utc(date_time: DateTime<Utc>) -> String {
date_time.to_rfc3339_opts(SecondsFormat::Secs, true)
}

/// Parses an RFC 3339 timestamp in any offset and re-formats it with
/// [`format_utc`].
pub fn format_utc_timestamp(value: &str) -> Result<String> {
Ok(format_utc(
DateTime::parse_from_rfc3339(value)?.with_timezone(&Utc),
))
}

/// [`format_utc_timestamp`] for display: anything that doesn't parse
/// (including an empty "never" value) is shown unchanged.
pub fn display_utc_timestamp(value: &str) -> String {
format_utc_timestamp(value).unwrap_or_else(|_| value.to_owned())
}

pub fn get_human_datetime(date_time_str: &str) -> (String, HumanTime) {
match DateTime::parse_from_rfc3339(date_time_str) {
Ok(d) => {
let datetime = DateTime::<Local>::from(d);

let date_time = datetime.format("%Y-%m-%d %H:%M").to_string();
let dt = datetime - chrono::Local::now();

let ht = HumanTime::from(dt);
return (date_time, ht);
let datetime = d.with_timezone(&Utc);
let ht = HumanTime::from(datetime - Utc::now());
(format_utc(datetime), ht)
}
Err(e) => {
debug!("{:#?}", &e);
Expand All @@ -23,3 +39,35 @@ pub fn get_human_datetime(date_time_str: &str) -> (String, HumanTime) {
}
}
}

#[cfg(test)]
mod tests {
use super::{display_utc_timestamp, format_utc_timestamp, get_human_datetime};

#[test]
fn timestamps_are_utc_seconds() {
assert_eq!(
format_utc_timestamp("2026-09-13T11:53:59.237435+00:00").unwrap(),
"2026-09-13T11:53:59Z"
);
assert_eq!(
format_utc_timestamp("2026-07-03T16:10:07+02:00").unwrap(),
"2026-07-03T14:10:07Z"
);
}

#[test]
fn human_datetime_is_utc_regardless_of_local_timezone() {
let (formatted, _) = get_human_datetime("2026-07-03T16:10:07.5+02:00");
assert_eq!(formatted, "2026-07-03T14:10:07Z");
}

#[test]
fn display_keeps_unparseable_values() {
assert_eq!(
display_utc_timestamp("2026-07-03T16:10:07+02:00"),
"2026-07-03T14:10:07Z"
);
assert_eq!(display_utc_timestamp(""), "");
}
}
Loading