blob: f51f2cb3fd907c68e303dd631cd3c806686b32bf [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Safe evidence at the boundary where private errors become public rejection codes.
use maka_agent_ports::provider::Error as ProviderError;
use maka_config::ConfigError;
use maka_model::ModelError;
pub(super) fn provider(
stage: &'static str,
error: &ProviderError,
connection_id: &str,
provider: &str,
) {
let (cause, reason) = match error {
ProviderError::Cancelled => return,
ProviderError::Unavailable => ("provider_unavailable", None),
ProviderError::AuthenticationRequired => ("provider_authentication_required", None),
ProviderError::Rejected(_) => ("provider_rejected", None),
ProviderError::Invalid(_) => ("provider_invalid", None),
ProviderError::OutcomeUnknown => ("provider_outcome_unknown", None),
ProviderError::Transport(_) => ("provider_transport", None),
ProviderError::Http(_) => ("provider_http", None),
ProviderError::InvalidConfiguration => ("provider_invalid", Some("invalid_configuration")),
ProviderError::InvalidModel => ("provider_invalid", Some("invalid_model")),
ProviderError::InvalidPreparedModel => ("provider_invalid", Some("invalid_prepared_model")),
ProviderError::SelectedModelChanged => ("provider_invalid", Some("selected_model_changed")),
ProviderError::UnsupportedThinkingLevel => {
("provider_invalid", Some("unsupported_thinking_level"))
}
ProviderError::UnsupportedProtocol => ("provider_invalid", Some("unsupported_protocol")),
ProviderError::UnsupportedExecutionProfile => {
("provider_invalid", Some("unsupported_execution_profile"))
}
ProviderError::InvalidEndpoint => ("provider_invalid", Some("invalid_endpoint")),
ProviderError::MissingResponsesContract => {
("provider_invalid", Some("missing_responses_contract"))
}
ProviderError::UnsupportedParallelPolicy => {
("provider_invalid", Some("unsupported_parallel_policy"))
}
ProviderError::InsufficientOutputBudget => {
("provider_invalid", Some("insufficient_output_budget"))
}
ProviderError::ProviderFactsUnavailable => {
("provider_invalid", Some("provider_facts_unavailable"))
}
ProviderError::UnsupportedProbeAuthentication => {
("provider_invalid", Some("unsupported_probe_authentication"))
}
};
let status = match error {
ProviderError::Http(status) => Some(*status),
_ => None,
};
tracing::warn!(
stage,
cause,
reason,
connection_id,
provider,
status,
"Model preparation failed"
);
}
pub(super) fn configuration(stage: &'static str, error: &ConfigError, connection_id: Option<&str>) {
let cause = crate::diagnostics::config_cause(error);
match error {
ConfigError::Io(error) => tracing::warn!(
stage, cause, connection_id, error_kind = ?error.kind(), os_code = error.raw_os_error(),
"Model preparation failed"
),
ConfigError::Sqlx(error) => {
// Only a numeric SQLite code, never the database's free-form message.
let sql_code = error
.as_database_error()
.and_then(|error| error.code())
.and_then(|code| code.parse::<i32>().ok());
tracing::warn!(
stage,
cause,
connection_id,
sql_code,
"Model preparation failed"
);
}
ConfigError::Json(error) => tracing::warn!(
stage, cause, connection_id, category = ?error.classify(),
line = error.line(), column = error.column(), "Model preparation failed"
),
_ => tracing::warn!(stage, cause, connection_id, "Model preparation failed"),
}
}
pub(super) fn model(stage: &'static str, error: &ModelError, connection_id: &str) {
match error {
ModelError::Cancelled => {}
ModelError::Http { status } => tracing::warn!(
stage,
cause = "model_http",
connection_id,
status,
"Model preparation failed"
),
ModelError::TimedOut => tracing::warn!(
stage,
cause = "model_timed_out",
connection_id,
"Model preparation failed"
),
ModelError::ContextOverflow { observed_output } => tracing::warn!(
stage,
cause = "model_context_overflow",
connection_id,
observed_output,
"Model preparation failed"
),
ModelError::Provider(error) => {
tracing::warn!(stage, cause = "model_provider", connection_id, reason = ?error.reason(), "Model preparation failed")
}
ModelError::Adapter(_) => tracing::warn!(
stage,
cause = "model_adapter",
connection_id,
"Model preparation failed"
),
}
}
pub(super) fn network(stage: &'static str, error: &maka_network::Error, connection_id: &str) {
let cause = match error {
maka_network::Error::InvalidProxy => "proxy_invalid",
maka_network::Error::MissingCredentials => "proxy_credentials_missing",
maka_network::Error::InvalidUpgrade => "network_invalid_upgrade",
maka_network::Error::InvalidHeaders => "network_invalid_headers",
maka_network::Error::Transport(_) => "network_transport",
};
tracing::warn!(stage, cause, connection_id, "Model preparation failed");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arbitrary_provider_error_strings_stay_private_at_diagnostic_boundary() {
use std::io::{Read, Seek};
let mut file = tempfile::tempfile().unwrap();
let subscriber = tracing_subscriber::fmt()
.with_writer(file.try_clone().unwrap())
.with_max_level(tracing::Level::WARN)
.with_ansi(false)
.without_time()
.finish();
let _guard = tracing::subscriber::set_default(subscriber);
for (stage, error) in [
(
"boundary_invalid",
ProviderError::Invalid("PRIVATE_INVALID_VALIDATION_SENTINEL".into()),
),
(
"boundary_rejected",
ProviderError::Rejected("PRIVATE_REJECTED_REMOTE_SENTINEL".into()),
),
(
"boundary_transport",
ProviderError::Transport("PRIVATE_TRANSPORT_URL_KEY_SENTINEL".into()),
),
("boundary_thinking", ProviderError::UnsupportedThinkingLevel),
(
"boundary_profile",
ProviderError::UnsupportedExecutionProfile,
),
] {
provider(stage, &error, "original-connection", "kimi-coding-plan");
}
file.rewind().unwrap();
let mut log = String::new();
file.read_to_string(&mut log).unwrap();
for (stage, cause) in [
("boundary_invalid", "provider_invalid"),
("boundary_rejected", "provider_rejected"),
("boundary_transport", "provider_transport"),
] {
let line = log.lines().find(|line| line.contains(stage)).unwrap();
assert!(line.contains(&format!("cause=\"{cause}\"")), "{line}");
assert!(line.contains("original-connection"));
assert!(line.contains("kimi-coding-plan"));
assert!(!line.contains("reason="));
}
for (stage, reason) in [
("boundary_thinking", "unsupported_thinking_level"),
("boundary_profile", "unsupported_execution_profile"),
] {
let line = log.lines().find(|line| line.contains(stage)).unwrap();
assert!(line.contains("cause=\"provider_invalid\""), "{line}");
assert!(line.contains(&format!("reason=\"{reason}\"")), "{line}");
}
for secret in [
"PRIVATE_INVALID_VALIDATION_SENTINEL",
"PRIVATE_REJECTED_REMOTE_SENTINEL",
"PRIVATE_TRANSPORT_URL_KEY_SENTINEL",
] {
assert!(
!log.contains(secret),
"arbitrary provider error payload escaped sanitization"
);
}
}
}