Source commit: 6756e5208c8b79e83d55610251430bc1baef53a3 Public tree identity: sha256:2f58837c3759b4f466cbc274571ee71bc0d24c7552dc009c186394e99123da87
666 lines
26 KiB
Rust
666 lines
26 KiB
Rust
use std::path::Path;
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::errors::{
|
|
cli_error_summary, cli_error_summary_for_category, cli_error_summary_with_default,
|
|
message_mentions_locality_failure,
|
|
};
|
|
|
|
fn task_event_values(task_events: Option<&Value>) -> Vec<&Value> {
|
|
task_events
|
|
.and_then(|task_events| task_events.pointer("/response/events"))
|
|
.and_then(Value::as_array)
|
|
.map(|events| events.iter().collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn event_string(event: &Value, field: &str) -> Option<String> {
|
|
event.get(field).and_then(Value::as_str).map(str::to_owned)
|
|
}
|
|
|
|
fn event_u64(event: &Value, field: &str) -> Option<u64> {
|
|
event.get(field).and_then(Value::as_u64)
|
|
}
|
|
|
|
pub(crate) fn task_summaries(task_events: Option<&Value>) -> Value {
|
|
Value::Array(
|
|
task_event_values(task_events)
|
|
.into_iter()
|
|
.map(|event| {
|
|
let task = event_string(event, "task").unwrap_or_else(|| "unknown".to_owned());
|
|
let terminal_state =
|
|
event_string(event, "terminal_state").unwrap_or_else(|| "unknown".to_owned());
|
|
let node = event_string(event, "node");
|
|
let placement = event.get("placement");
|
|
let placement_reasons = placement
|
|
.and_then(|placement| placement.get("reasons"))
|
|
.cloned()
|
|
.unwrap_or_else(|| json!([]));
|
|
let placement_score = placement
|
|
.and_then(|placement| placement.get("score"))
|
|
.cloned()
|
|
.unwrap_or(Value::Null);
|
|
let failure_reason = task_failure_reason(event);
|
|
let locality_failure = task_locality_failure_from_reason(&failure_reason);
|
|
let machine_error = task_failure_machine_error_from_reason(event, &failure_reason);
|
|
json!({
|
|
"process": event_string(event, "process"),
|
|
"task": task,
|
|
"state": terminal_state,
|
|
"environment": event.get("environment").cloned().unwrap_or_else(|| json!("unknown_from_task_event")),
|
|
"environment_digest": event.get("environment_digest").cloned().unwrap_or(Value::Null),
|
|
"node_placement": {
|
|
"node": node,
|
|
"source": "coordinator_task_event",
|
|
"score": placement_score,
|
|
"reasons": placement_reasons,
|
|
"explanation_available": placement.is_some(),
|
|
},
|
|
"failure_reason": failure_reason,
|
|
"locality_failure": locality_failure,
|
|
"machine_error": machine_error,
|
|
"stdout_bytes": event_u64(event, "stdout_bytes").unwrap_or(0),
|
|
"stderr_bytes": event_u64(event, "stderr_bytes").unwrap_or(0),
|
|
})
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn task_failure_reason(event: &Value) -> Value {
|
|
match event.get("terminal_state").and_then(Value::as_str) {
|
|
Some("failed") => {
|
|
if let Some(stderr) = event.get("stderr_tail").and_then(Value::as_str) {
|
|
if !stderr.is_empty() {
|
|
return json!(redact_secret_like_text(stderr).0);
|
|
}
|
|
}
|
|
if let Some(status_code) = event.get("status_code").and_then(Value::as_i64) {
|
|
return json!(format!("task exited with status {status_code}"));
|
|
}
|
|
json!("task failed")
|
|
}
|
|
Some("cancelled") => json!("task cancelled"),
|
|
_ => Value::Null,
|
|
}
|
|
}
|
|
|
|
fn task_failure_machine_error_from_reason(event: &Value, reason: &Value) -> Value {
|
|
match event.get("terminal_state").and_then(Value::as_str) {
|
|
Some("failed") => {
|
|
let reason = reason.as_str().unwrap_or("task failed").to_owned();
|
|
let mut summary = cli_error_summary_with_default(&reason, "program");
|
|
if let Some(object) = summary.as_object_mut() {
|
|
if message_mentions_locality_failure(&reason.to_ascii_lowercase()) {
|
|
object.insert("locality_failure".to_owned(), json!(true));
|
|
object.insert(
|
|
"next_actions".to_owned(),
|
|
json!(locality_failure_next_actions(&reason)),
|
|
);
|
|
}
|
|
}
|
|
summary
|
|
}
|
|
Some("cancelled") => cli_error_summary_for_category("program", "task cancelled"),
|
|
_ => Value::Null,
|
|
}
|
|
}
|
|
|
|
fn task_locality_failure_from_reason(reason: &Value) -> Value {
|
|
let Some(reason) = reason.as_str() else {
|
|
return Value::Null;
|
|
};
|
|
let lower = reason.to_ascii_lowercase();
|
|
if !message_mentions_locality_failure(&lower) {
|
|
return Value::Null;
|
|
}
|
|
let affected_data = if lower.contains("source snapshot") {
|
|
"source_snapshot"
|
|
} else if lower.contains("artifact") {
|
|
"artifact"
|
|
} else {
|
|
"direct_transfer"
|
|
};
|
|
json!({
|
|
"category": "connectivity",
|
|
"affected_data": affected_data,
|
|
"reason": reason,
|
|
"coordinator_bulk_relay_used": false,
|
|
"safe_failure": true,
|
|
"safe_next_actions": locality_failure_next_actions(reason),
|
|
})
|
|
}
|
|
|
|
fn locality_failure_next_actions(reason: &str) -> Vec<&'static str> {
|
|
let lower = reason.to_ascii_lowercase();
|
|
if lower.contains("source snapshot") {
|
|
return vec![
|
|
"attach or select a node that already has the required source snapshot",
|
|
"rerun source preparation on an attached node",
|
|
"restore direct node-to-node connectivity and retry",
|
|
"do not rely on coordinator bulk source relay",
|
|
];
|
|
}
|
|
if lower.contains("artifact") {
|
|
return vec![
|
|
"attach or select a node that already has the required artifact",
|
|
"explicitly export or download the artifact before retrying",
|
|
"restore direct node-to-node connectivity and retry",
|
|
"do not rely on coordinator bulk artifact relay",
|
|
];
|
|
}
|
|
vec![
|
|
"check node direct connectivity and NAT traversal",
|
|
"attach a node with the needed source/artifact locality",
|
|
"retry after node connectivity is restored",
|
|
"do not rely on coordinator bulk relay",
|
|
]
|
|
}
|
|
|
|
pub(crate) fn process_state_from_tasks(task_events: Option<&Value>) -> &'static str {
|
|
let events = task_event_values(task_events);
|
|
if events.is_empty() {
|
|
return "no_tasks_observed";
|
|
}
|
|
if events.iter().any(|event| {
|
|
event
|
|
.get("terminal_state")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|state| state == "failed")
|
|
}) {
|
|
return "has_failed_tasks";
|
|
}
|
|
if events.iter().any(|event| {
|
|
event
|
|
.get("terminal_state")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|state| state == "cancelled")
|
|
}) {
|
|
return "has_cancelled_tasks";
|
|
}
|
|
if events.iter().all(|event| {
|
|
event
|
|
.get("terminal_state")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|state| state == "completed")
|
|
}) {
|
|
return "completed_tasks_observed";
|
|
}
|
|
"tasks_observed"
|
|
}
|
|
|
|
pub(crate) fn log_entries(task_events: Option<&Value>, task_filter: Option<&str>) -> Value {
|
|
Value::Array(
|
|
task_event_values(task_events)
|
|
.into_iter()
|
|
.filter(|event| {
|
|
task_filter.is_none_or( |task_filter| {
|
|
event
|
|
.get("task")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|task| task == task_filter)
|
|
})
|
|
})
|
|
.map(|event| {
|
|
let stdout_tail = event
|
|
.get("stdout_tail")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("");
|
|
let stderr_tail = event
|
|
.get("stderr_tail")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("");
|
|
let (stdout_tail, stdout_tail_redacted) = redact_secret_like_text(stdout_tail);
|
|
let (stderr_tail, stderr_tail_redacted) = redact_secret_like_text(stderr_tail);
|
|
json!({
|
|
"process": event_string(event, "process"),
|
|
"task": event_string(event, "task"),
|
|
"node": event_string(event, "node"),
|
|
"stdout_bytes": event_u64(event, "stdout_bytes").unwrap_or(0),
|
|
"stderr_bytes": event_u64(event, "stderr_bytes").unwrap_or(0),
|
|
"stdout_tail": stdout_tail,
|
|
"stderr_tail": stderr_tail,
|
|
"stdout_truncated": event.get("stdout_truncated").and_then(Value::as_bool).unwrap_or(false),
|
|
"stderr_truncated": event.get("stderr_truncated").and_then(Value::as_bool).unwrap_or(false),
|
|
"capped": true,
|
|
"secret_like_values_redacted": stdout_tail_redacted || stderr_tail_redacted,
|
|
"redacted_fields": redacted_log_fields(stdout_tail_redacted, stderr_tail_redacted),
|
|
})
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn redacted_log_fields(
|
|
stdout_tail_redacted: bool,
|
|
stderr_tail_redacted: bool,
|
|
) -> Vec<&'static str> {
|
|
let mut fields = Vec::new();
|
|
if stdout_tail_redacted {
|
|
fields.push("stdout_tail");
|
|
}
|
|
if stderr_tail_redacted {
|
|
fields.push("stderr_tail");
|
|
}
|
|
fields
|
|
}
|
|
|
|
fn redact_secret_like_text(text: &str) -> (String, bool) {
|
|
let markers = [
|
|
"access_token=",
|
|
"access_token:",
|
|
"refresh_token=",
|
|
"refresh_token:",
|
|
"id_token=",
|
|
"id_token:",
|
|
"api_key=",
|
|
"api_key:",
|
|
"api-key=",
|
|
"api-key:",
|
|
"token=",
|
|
"token:",
|
|
"secret=",
|
|
"secret:",
|
|
"password=",
|
|
"password:",
|
|
"passwd=",
|
|
"passwd:",
|
|
"bearer ",
|
|
];
|
|
let mut output = text.to_owned();
|
|
let mut redacted = false;
|
|
for marker in markers {
|
|
let (updated, changed) = redact_marker_values(output, marker);
|
|
output = updated;
|
|
redacted |= changed;
|
|
}
|
|
(output, redacted)
|
|
}
|
|
|
|
fn redact_marker_values(mut text: String, marker: &str) -> (String, bool) {
|
|
let mut changed = false;
|
|
let mut search_start = 0;
|
|
loop {
|
|
let lower = text.to_ascii_lowercase();
|
|
let Some(relative) = lower[search_start..].find(marker) else {
|
|
break;
|
|
};
|
|
let value_start = search_start + relative + marker.len();
|
|
let value_end = text[value_start..]
|
|
.char_indices()
|
|
.find_map(|(offset, character)| {
|
|
(character.is_whitespace()
|
|
|| matches!(
|
|
character,
|
|
'&' | '"' | '\'' | '`' | '<' | '>' | ',' | ';' | ')' | ']'
|
|
))
|
|
.then_some(value_start + offset)
|
|
})
|
|
.unwrap_or(text.len());
|
|
if value_start == value_end {
|
|
search_start = value_end;
|
|
if search_start >= text.len() {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if text[value_start..value_end].starts_with("[redacted") {
|
|
search_start = value_end;
|
|
if search_start >= text.len() {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
text.replace_range(value_start..value_end, "[redacted]");
|
|
changed = true;
|
|
search_start = value_start + "[redacted]".len();
|
|
if search_start >= text.len() {
|
|
break;
|
|
}
|
|
}
|
|
(text, changed)
|
|
}
|
|
|
|
pub(crate) fn artifact_summaries(task_events: Option<&Value>) -> Value {
|
|
Value::Array(
|
|
task_event_values(task_events)
|
|
.into_iter()
|
|
.filter_map(|event| {
|
|
let path = event.get("artifact_path").and_then(Value::as_str)?;
|
|
let node = event_string(event, "node");
|
|
Some(json!({
|
|
"artifact": artifact_name_from_path(path),
|
|
"path": path,
|
|
"producer_task": event_string(event, "task"),
|
|
"producer_node": node,
|
|
"process": event_string(event, "process"),
|
|
"digest": event.get("artifact_digest").cloned().unwrap_or(Value::Null),
|
|
"size_bytes": event.get("artifact_size_bytes").cloned().unwrap_or(Value::Null),
|
|
"state": if event.get("artifact_digest").is_some() { "metadata_flushed" } else { "metadata_without_digest" },
|
|
"known_locations": node.into_iter().collect::<Vec<_>>(),
|
|
"durable_storage": false,
|
|
}))
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn artifact_name_from_path(path: &str) -> String {
|
|
path.rsplit('/').next().unwrap_or(path).to_owned()
|
|
}
|
|
|
|
fn response_error_message(response: &Value, fallback: &str) -> String {
|
|
response
|
|
.get("message")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned)
|
|
.unwrap_or_else(|| fallback.to_owned())
|
|
}
|
|
|
|
pub(crate) fn artifact_response_machine_error(
|
|
response: &Value,
|
|
fallback: &str,
|
|
default_category: &'static str,
|
|
) -> Value {
|
|
let message = response_error_message(response, fallback);
|
|
cli_error_summary_with_default(&message, default_category)
|
|
}
|
|
|
|
pub(crate) fn process_restart_request_summary(
|
|
response: &Value,
|
|
requires_confirmation: bool,
|
|
) -> Value {
|
|
if response.get("type").and_then(Value::as_str) != Some("process_started") {
|
|
let message = response_error_message(response, "coordinator rejected process restart");
|
|
return json!({
|
|
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
|
|
"operation": "restart_virtual_process",
|
|
"accepted": false,
|
|
"requires_confirmation": requires_confirmation,
|
|
"explicit_user_action": true,
|
|
"error": message,
|
|
"machine_error": cli_error_summary(&message),
|
|
});
|
|
}
|
|
json!({
|
|
"status": "process_started",
|
|
"operation": "restart_virtual_process",
|
|
"accepted": true,
|
|
"process": response.get("process").cloned().unwrap_or(Value::Null),
|
|
"coordinator_epoch": response.get("epoch").cloned().unwrap_or(Value::Null),
|
|
"requires_confirmation": requires_confirmation,
|
|
"explicit_user_action": true,
|
|
"website_required": false,
|
|
"single_active_process_boundary": true,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn process_cancel_request_summary(
|
|
response: &Value,
|
|
requires_confirmation: bool,
|
|
) -> Value {
|
|
if response.get("type").and_then(Value::as_str) != Some("process_cancellation_requested") {
|
|
let message = response_error_message(response, "coordinator rejected process cancel");
|
|
return json!({
|
|
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
|
|
"operation": "cancel_virtual_process",
|
|
"accepted": false,
|
|
"requires_confirmation": requires_confirmation,
|
|
"explicit_user_action": true,
|
|
"whole_process_cancel_available": true,
|
|
"error": message,
|
|
"machine_error": cli_error_summary(&message),
|
|
});
|
|
}
|
|
let cancelled_tasks = response
|
|
.get("cancelled_tasks")
|
|
.and_then(Value::as_array)
|
|
.map(Vec::len)
|
|
.unwrap_or(0);
|
|
json!({
|
|
"status": "process_cancellation_requested",
|
|
"operation": "cancel_virtual_process",
|
|
"accepted": true,
|
|
"process": response.get("process").cloned().unwrap_or(Value::Null),
|
|
"cancelled_task_count": cancelled_tasks,
|
|
"cancelled_tasks": response.get("cancelled_tasks").cloned().unwrap_or_else(|| json!([])),
|
|
"affected_nodes": response.get("affected_nodes").cloned().unwrap_or_else(|| json!([])),
|
|
"requires_confirmation": requires_confirmation,
|
|
"explicit_user_action": true,
|
|
"website_required": false,
|
|
"whole_process_cancel_available": true,
|
|
"node_must_poll_task_control": true,
|
|
"new_task_launches_blocked": true,
|
|
"surviving_state_visibility": "task and artifact state remains visible after terminal task events are reported",
|
|
})
|
|
}
|
|
|
|
pub(crate) fn task_restart_request_summary(response: &Value, requires_confirmation: bool) -> Value {
|
|
if response.get("type").and_then(Value::as_str) != Some("task_restart") {
|
|
let message = response_error_message(response, "coordinator rejected task restart");
|
|
return json!({
|
|
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
|
|
"operation": "restart_selected_task",
|
|
"accepted": false,
|
|
"requires_confirmation": requires_confirmation,
|
|
"explicit_user_action": true,
|
|
"clean_boundary_required": true,
|
|
"error": message,
|
|
"machine_error": cli_error_summary(&message),
|
|
});
|
|
}
|
|
json!({
|
|
"status": "task_restart",
|
|
"operation": "restart_selected_task",
|
|
"accepted": response.get("accepted").cloned().unwrap_or(json!(false)),
|
|
"process": response.get("process").cloned().unwrap_or(Value::Null),
|
|
"task": response.get("task").cloned().unwrap_or(Value::Null),
|
|
"requires_confirmation": requires_confirmation,
|
|
"explicit_user_action": true,
|
|
"clean_boundary_required": true,
|
|
"clean_boundary_available": response
|
|
.get("clean_boundary_available")
|
|
.cloned()
|
|
.unwrap_or(json!(false)),
|
|
"active_task": response
|
|
.get("active_task")
|
|
.cloned()
|
|
.unwrap_or(json!(false)),
|
|
"completed_event_observed": response
|
|
.get("completed_event_observed")
|
|
.cloned()
|
|
.unwrap_or(json!(false)),
|
|
"requires_whole_process_restart": response
|
|
.get("requires_whole_process_restart")
|
|
.cloned()
|
|
.unwrap_or(json!(true)),
|
|
"message": response.get("message").cloned().unwrap_or(Value::Null),
|
|
"audit_event": response.get("audit_event").cloned().unwrap_or(Value::Null),
|
|
"charged_debug_read_bytes": response
|
|
.get("charged_debug_read_bytes")
|
|
.cloned()
|
|
.unwrap_or_else(|| json!(0)),
|
|
"used_debug_read_bytes": response
|
|
.get("used_debug_read_bytes")
|
|
.cloned()
|
|
.unwrap_or_else(|| json!(0)),
|
|
"debug_reads_quota_limited": true,
|
|
"website_required": false,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn artifact_download_session_summary(response: &Value) -> Value {
|
|
if response.get("type").and_then(Value::as_str) != Some("artifact_download_link") {
|
|
let message = response_error_message(response, "coordinator rejected artifact download");
|
|
return json!({
|
|
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
|
|
"link_issued": false,
|
|
"explicit_user_action_required": true,
|
|
"error": message.clone(),
|
|
"machine_error": cli_error_summary_with_default(&message, "connectivity"),
|
|
});
|
|
}
|
|
|
|
let link = response.get("link").unwrap_or(&Value::Null);
|
|
json!({
|
|
"status": "download_link_issued",
|
|
"link_issued": true,
|
|
"explicit_user_action_required": true,
|
|
"coordinator_preflight": "completed_before_link_issued",
|
|
"tenant": link.get("tenant").cloned().unwrap_or(Value::Null),
|
|
"project": link.get("project").cloned().unwrap_or(Value::Null),
|
|
"process": link.get("process").cloned().unwrap_or(Value::Null),
|
|
"artifact": link.get("artifact").cloned().unwrap_or(Value::Null),
|
|
"actor": link.get("actor").cloned().unwrap_or(Value::Null),
|
|
"source": link.get("source").cloned().unwrap_or(Value::Null),
|
|
"url_path": link.get("url_path").cloned().unwrap_or(Value::Null),
|
|
"expires_at_epoch_seconds": link.get("expires_at_epoch_seconds").cloned().unwrap_or(Value::Null),
|
|
"max_bytes": link.get("max_bytes").cloned().unwrap_or(Value::Null),
|
|
"token_material_returned": false,
|
|
"scoped_token_digest_present": link.get("scoped_token_digest").is_some(),
|
|
"policy_context_digest_present": link.get("policy_context_digest").is_some(),
|
|
"authorization_required": true,
|
|
"short_lived": link.get("expires_at_epoch_seconds").is_some(),
|
|
"guessable_public_url": false,
|
|
"cross_tenant_usable": false,
|
|
"unauthorized_project_usable": false,
|
|
"default_durable_store_assumed": false,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn artifact_download_grant_disclosures(response: &Value) -> Value {
|
|
if response.get("type").and_then(Value::as_str) != Some("artifact_download_link") {
|
|
return json!([]);
|
|
}
|
|
|
|
let link = response.get("link").unwrap_or(&Value::Null);
|
|
json!([{
|
|
"grant": "artifact_download",
|
|
"description": "download scoped artifact bytes to the requesting machine",
|
|
"risk": "authorized artifact bytes leave the retaining node or explicit storage through an explicit download/export operation",
|
|
"coordinator_policy_limited": true,
|
|
"authorization_required": true,
|
|
"explicit_user_action_required": true,
|
|
"tenant": link.get("tenant").cloned().unwrap_or(Value::Null),
|
|
"project": link.get("project").cloned().unwrap_or(Value::Null),
|
|
"process": link.get("process").cloned().unwrap_or(Value::Null),
|
|
"artifact": link.get("artifact").cloned().unwrap_or(Value::Null),
|
|
"actor": link.get("actor").cloned().unwrap_or(Value::Null),
|
|
"source": link.get("source").cloned().unwrap_or(Value::Null),
|
|
"max_bytes": link.get("max_bytes").cloned().unwrap_or(Value::Null),
|
|
"expires_at_epoch_seconds": link.get("expires_at_epoch_seconds").cloned().unwrap_or(Value::Null),
|
|
"short_lived": link.get("expires_at_epoch_seconds").is_some(),
|
|
"scoped_token_digest_present": link.get("scoped_token_digest").is_some(),
|
|
"token_material_returned": false,
|
|
"guessable_public_url": false,
|
|
"cross_tenant_reuse_allowed": false,
|
|
"unauthorized_project_reuse_allowed": false,
|
|
"default_durable_store_assumed": false,
|
|
"private_website_required": false,
|
|
}])
|
|
}
|
|
|
|
pub(crate) fn artifact_export_plan_summary(response: &Value, to: &Path) -> Value {
|
|
if response.get("type").and_then(Value::as_str) != Some("artifact_export_plan") {
|
|
let message = response_error_message(response, "coordinator rejected artifact export");
|
|
return json!({
|
|
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
|
|
"explicit_user_action": true,
|
|
"local_path": to,
|
|
"local_bytes_written_by_cli": false,
|
|
"default_durable_store_assumed": false,
|
|
"error": message.clone(),
|
|
"machine_error": cli_error_summary_with_default(&message, "connectivity"),
|
|
});
|
|
}
|
|
|
|
let plan = response.get("plan").unwrap_or(&Value::Null);
|
|
json!({
|
|
"status": "transfer_plan_created",
|
|
"explicit_user_action": true,
|
|
"local_path": to,
|
|
"local_bytes_written_by_cli": false,
|
|
"writes_require_data_plane_followup": true,
|
|
"default_durable_store_assumed": false,
|
|
"artifact_size_bytes": response.get("artifact_size_bytes").cloned().unwrap_or(Value::Null),
|
|
"source_node": response.get("source_node").cloned().unwrap_or(Value::Null),
|
|
"receiver_node": response.get("receiver_node").cloned().unwrap_or(Value::Null),
|
|
"transport": plan.get("transport").cloned().unwrap_or(Value::Null),
|
|
"artifact": plan.pointer("/scope/object/Artifact").cloned().unwrap_or(Value::Null),
|
|
"tenant": plan.pointer("/scope/tenant").cloned().unwrap_or(Value::Null),
|
|
"project": plan.pointer("/scope/project").cloned().unwrap_or(Value::Null),
|
|
"process": plan.pointer("/scope/process").cloned().unwrap_or(Value::Null),
|
|
"coordinator_assisted_rendezvous": plan.get("coordinator_assisted_rendezvous").cloned().unwrap_or(Value::Null),
|
|
"coordinator_bulk_relay_allowed": plan.get("coordinator_bulk_relay_allowed").cloned().unwrap_or(Value::Null),
|
|
"authorization_digest_present": plan.get("authorization_digest").is_some(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn task_event_count(task_events: Option<&Value>) -> usize {
|
|
task_events
|
|
.and_then(|task_events| task_events.pointer("/response/events"))
|
|
.and_then(Value::as_array)
|
|
.map(Vec::len)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
pub(crate) fn project_quota_posture(attached_nodes: &Value, task_events: Option<&Value>) -> Value {
|
|
let current_usage = quota_current_usage(attached_nodes, task_events);
|
|
let next_blocked_action = quota_next_blocked_action(¤t_usage);
|
|
json!({
|
|
"source": "cli_project_status_summary",
|
|
"current_usage": current_usage,
|
|
"limits": quota_limits_value(),
|
|
"next_blocked_action": next_blocked_action,
|
|
"private_abuse_heuristics_exposed": false,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn quota_current_usage(attached_nodes: &Value, task_events: Option<&Value>) -> Value {
|
|
let attached_node_count = attached_nodes
|
|
.get("count")
|
|
.and_then(Value::as_u64)
|
|
.unwrap_or(0);
|
|
let online_node_count = attached_nodes
|
|
.get("online")
|
|
.and_then(Value::as_u64)
|
|
.unwrap_or(0);
|
|
json!({
|
|
"attached_nodes": attached_node_count,
|
|
"online_nodes": online_node_count,
|
|
"observed_task_events": task_event_count(task_events),
|
|
"artifact_download_bytes": 0,
|
|
"rendezvous_attempts": 0,
|
|
"hosted_wasm_processes": 0,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn quota_limits_value() -> Value {
|
|
json!({
|
|
"source": "coordinator",
|
|
"configured": false,
|
|
"message": "connect to the selected coordinator to read its scoped quota configuration",
|
|
})
|
|
}
|
|
|
|
pub(crate) fn quota_next_blocked_action(current_usage: &Value) -> Value {
|
|
if current_usage
|
|
.get("online_nodes")
|
|
.and_then(Value::as_u64)
|
|
.unwrap_or(0)
|
|
== 0
|
|
{
|
|
return json!({
|
|
"action": "node_work_requires_online_attached_node",
|
|
"category": "capability",
|
|
"quota_related": false,
|
|
"message": "no online attached node is visible for work that requires a user node",
|
|
"machine_error": cli_error_summary_for_category(
|
|
"capability",
|
|
"no online attached node is visible for work that requires a user node"
|
|
)
|
|
});
|
|
}
|
|
Value::Null
|
|
}
|