commit b20797d61766228fcf51bb06b141231592d5369f
parent 6661f7ef67d3a97b337a038bf6b80a64b369705f
Author: triesap <tyson@radroots.org>
Date: Sat, 22 Aug 2026 06:45:19 +0000
diagnostics: freeze structured stderr and exits
Diffstat:
9 files changed, 573 insertions(+), 5 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -248,6 +248,11 @@
- Keep logs as safe structured stderr output. Keep result data on stdout and
diagnostics on stderr. Use stable bounded public codes and messages, bounded
metric labels, and explicit redaction.
+- Emit only the sealed `MycLogRecord` vocabulary. Do not log caller text, raw
+ errors or sources, paths, SQL, relay URLs, identifiers, credentials,
+ protected content, or decrypted payloads. Keep the exact 0-6 process result
+ mapping synchronized with the operator contract; do not call process exit
+ from library code or add file logging.
- Backup and restore must preserve lock, manifest, integrity, schema, service,
instance, identity, permission, fsync, atomic-rename, and protected-material
exclusion invariants.
diff --git a/README b/README
@@ -108,6 +108,17 @@ disabled unless the validated configuration explicitly enables and binds it,
and its parser floor, headers, response, concurrency, deadline, and idle bounds
remain enforced by the shared server.
+Process outcomes use one exact seven-value vocabulary with exit codes 0
+through 6. Diagnostics are compact single-line JSON on stderr; result data is
+reserved for stdout, and Myc never writes log files. The structured record
+admits only the closed level, event, result, lifecycle, task-failure, and signal
+codes. It accepts no caller text, path, SQL, raw cause, identity, relay URL,
+credential, secret, or decrypted content. Every public Myc error remains a
+crate-owned source-free classification, so ordinary `Display`, `Debug`, and
+whole-chain traversal cannot bypass redaction. Current binary dispatch reports
+invalid input as exit 2 and the intentionally unavailable later executor as
+exit 3; Steps 158 and 159 own real daemon/task/signal execution.
+
`parse_myc_config_v1` caps original bytes before decoding, checks the schema
header before closed contract admission, rejects duplicate, null, unknown, and
semantically inconsistent input, and returns an immutable document plus a
diff --git a/contracts/api_baselines/myc.txt b/contracts/api_baselines/myc.txt
@@ -395,6 +395,22 @@ pub myc::MycLocalSignerTransportErrorKind::Transport
pub myc::MycLocalSignerTransportErrorKind::UnsupportedPlatform
impl myc::MycLocalSignerTransportErrorKind
pub const fn myc::MycLocalSignerTransportErrorKind::code(self) -> &'static str
+pub enum myc::MycLogEvent
+pub myc::MycLogEvent::CriticalTaskFailed
+pub myc::MycLogEvent::Lifecycle
+pub myc::MycLogEvent::ProcessResult
+pub myc::MycLogEvent::ShutdownForced
+pub myc::MycLogEvent::ShutdownRequested
+impl myc::MycLogEvent
+pub const fn myc::MycLogEvent::as_str(self) -> &'static str
+pub enum myc::MycLogLevel
+pub myc::MycLogLevel::Debug
+pub myc::MycLogLevel::Error
+pub myc::MycLogLevel::Info
+pub myc::MycLogLevel::Trace
+pub myc::MycLogLevel::Warn
+impl myc::MycLogLevel
+pub const fn myc::MycLogLevel::as_str(self) -> &'static str
pub enum myc::MycNip05ExportSelection
pub myc::MycNip05ExportSelection::Current
pub myc::MycNip05ExportSelection::Desired
@@ -499,6 +515,18 @@ pub myc::MycPersistenceHealthV1::ReadOnly
pub myc::MycPersistenceHealthV1::Ready
pub myc::MycPersistenceHealthV1::RepairRequired
pub myc::MycPersistenceHealthV1::Unavailable
+pub enum myc::MycProcessResult
+pub myc::MycProcessResult::DoctorRequiredCheckFailed
+pub myc::MycProcessResult::InputOrConfiguration
+pub myc::MycProcessResult::OperationRejectedOrConflict
+pub myc::MycProcessResult::ServiceOrDependencyUnavailable
+pub myc::MycProcessResult::StateOrIdentityUnavailable
+pub myc::MycProcessResult::Success
+pub myc::MycProcessResult::UnexpectedInternal
+impl myc::MycProcessResult
+pub const fn myc::MycProcessResult::code(self) -> &'static str
+pub fn myc::MycProcessResult::exit_code(self) -> std::process::ExitCode
+pub const fn myc::MycProcessResult::exit_code_u8(self) -> u8
pub enum myc::MycProviderCapability
pub myc::MycProviderCapability::Describe
pub myc::MycProviderCapability::Nip04Decrypt
@@ -1249,6 +1277,21 @@ impl myc::MycLocalSignerUntrustedResponse
pub fn myc::MycLocalSignerUntrustedResponse::verify(self, &myc::MycProviderBinding, &myc::MycProviderOperation, myc::MycProviderResponseObservedAtUnixMs) -> core::result::Result<myc::MycVerifiedProviderResponse, myc::MycProviderVerificationError>
impl core::fmt::Debug for myc::MycLocalSignerUntrustedResponse
pub fn myc::MycLocalSignerUntrustedResponse::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycLogRecord
+impl myc::MycLogRecord
+pub const fn myc::MycLogRecord::code(&self) -> &'static str
+pub const fn myc::MycLogRecord::critical_task_failed() -> Self
+pub const fn myc::MycLogRecord::event(&self) -> myc::MycLogEvent
+pub const fn myc::MycLogRecord::level(&self) -> myc::MycLogLevel
+pub const fn myc::MycLogRecord::lifecycle(myc::MycServicePhase) -> Self
+pub const fn myc::MycLogRecord::process_exit(&self) -> core::option::Option<myc::MycProcessResult>
+pub const fn myc::MycLogRecord::process_result(myc::MycProcessResult) -> Self
+pub const fn myc::MycLogRecord::shutdown_forced() -> Self
+pub const fn myc::MycLogRecord::shutdown_requested() -> Self
+impl core::fmt::Debug for myc::MycLogRecord
+pub fn myc::MycLogRecord::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for myc::MycLogRecord
+pub fn myc::MycLogRecord::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct myc::MycNip05Document
impl myc::MycNip05Document
pub fn myc::MycNip05Document::bytes(&self) -> &[u8]
@@ -1926,6 +1969,7 @@ pub const myc::MYC_DELIVERY_RELAY_ID_MAX_BYTES: usize
pub const myc::MYC_DELIVERY_RETRY_JITTER_MAX_MS: u64
pub const myc::MYC_DELIVERY_TARGET_MAX_COUNT: usize
pub const myc::MYC_DETAILED_STATUS_MAX_UTF8_BYTES: usize
+pub const myc::MYC_DIAGNOSTICS_CONTRACT_VERSION: u32
pub const myc::MYC_DISCOVERY_DOCUMENT_MAX_BYTES: usize
pub const myc::MYC_DOCTOR_CHECK_COUNT: usize
pub const myc::MYC_DOCTOR_CONTRACT_VERSION: u32
@@ -1937,6 +1981,7 @@ pub const myc::MYC_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES: usize
pub const myc::MYC_LIVEZ_PATH: &str
pub const myc::MYC_LOCAL_SIGNER_ENDPOINT: &str
pub const myc::MYC_LOCAL_SIGNER_TRANSPORT_CONTRACT_VERSION: u32
+pub const myc::MYC_LOG_RECORD_MAX_UTF8_BYTES: usize
pub const myc::MYC_METRICS_PATH: &str
pub const myc::MYC_MIGRATION_CATALOG_SHA256: [u8; 32]
pub const myc::MYC_NIP05_DOCUMENT_MAX_BYTES: usize
diff --git a/contracts/services_hardening/diagnostics.v1.json b/contracts/services_hardening/diagnostics.v1.json
@@ -0,0 +1,61 @@
+{
+ "schema": "radroots.myc.diagnostics.v1",
+ "contract_version": 1,
+ "step": 156,
+ "stream_policy": {
+ "result_data": "stdout",
+ "logs_and_diagnostics": "stderr",
+ "file_logging": false
+ },
+ "log_record": {
+ "schema": "radroots.myc.log.v1",
+ "maximum_utf8_bytes_excluding_newline": 512,
+ "field_order": ["schema", "contract_version", "service", "level", "event", "code", "exit_code"],
+ "service": "myc",
+ "levels": ["trace", "debug", "info", "warn", "error"],
+ "events": ["process_result", "lifecycle", "critical_task_failed", "shutdown_requested", "shutdown_forced"],
+ "caller_controlled_text": false,
+ "timestamp": "not_owned_by_this_record",
+ "newline": "stderr_writer_owned"
+ },
+ "exit_codes": [
+ { "code": 0, "name": "success" },
+ { "code": 1, "name": "unexpected_internal" },
+ { "code": 2, "name": "input_or_configuration" },
+ { "code": 3, "name": "service_or_dependency_unavailable" },
+ { "code": 4, "name": "state_or_identity_unavailable" },
+ { "code": 5, "name": "operation_rejected_or_conflict" },
+ { "code": 6, "name": "doctor_required_check_failed" }
+ ],
+ "public_error_policy": {
+ "crate_owned_classification": true,
+ "error_source": "none",
+ "display_and_debug": "path_secret_content_and_raw_cause_free"
+ },
+ "forbidden_fields": [
+ "path",
+ "sql",
+ "raw_error",
+ "source",
+ "credential",
+ "secret",
+ "private_key",
+ "decrypted_payload",
+ "relay_url",
+ "public_key",
+ "event_id",
+ "request_id",
+ "connection_id"
+ ],
+ "deferrals": [
+ "command_execution",
+ "task_graph",
+ "signal_installation",
+ "shutdown_execution",
+ "integration_wave",
+ "rcld_promotion",
+ "nix",
+ "oci",
+ "terminal_consumer_convergence"
+ ]
+}
diff --git a/src/diagnostics_v1.rs b/src/diagnostics_v1.rs
@@ -0,0 +1,236 @@
+//! Closed process-result and structured stderr diagnostic contract.
+
+use core::fmt;
+use std::process::ExitCode;
+
+use crate::MycServicePhase;
+
+/// Exact Myc diagnostics contract version.
+pub const MYC_DIAGNOSTICS_CONTRACT_VERSION: u32 = 1;
+
+/// Hard maximum for one canonical Myc structured log record, excluding newline.
+pub const MYC_LOG_RECORD_MAX_UTF8_BYTES: usize = 512;
+
+const LOG_SCHEMA: &str = "radroots.myc.log.v1";
+
+/// Exact stable Myc process result and exit-code inventory.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycProcessResult {
+ Success,
+ UnexpectedInternal,
+ InputOrConfiguration,
+ ServiceOrDependencyUnavailable,
+ StateOrIdentityUnavailable,
+ OperationRejectedOrConflict,
+ DoctorRequiredCheckFailed,
+}
+
+impl MycProcessResult {
+ #[must_use]
+ pub const fn exit_code_u8(self) -> u8 {
+ match self {
+ Self::Success => 0,
+ Self::UnexpectedInternal => 1,
+ Self::InputOrConfiguration => 2,
+ Self::ServiceOrDependencyUnavailable => 3,
+ Self::StateOrIdentityUnavailable => 4,
+ Self::OperationRejectedOrConflict => 5,
+ Self::DoctorRequiredCheckFailed => 6,
+ }
+ }
+
+ #[must_use]
+ pub fn exit_code(self) -> ExitCode {
+ ExitCode::from(self.exit_code_u8())
+ }
+
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::Success => "success",
+ Self::UnexpectedInternal => "unexpected_internal",
+ Self::InputOrConfiguration => "input_or_configuration",
+ Self::ServiceOrDependencyUnavailable => "service_or_dependency_unavailable",
+ Self::StateOrIdentityUnavailable => "state_or_identity_unavailable",
+ Self::OperationRejectedOrConflict => "operation_rejected_or_conflict",
+ Self::DoctorRequiredCheckFailed => "doctor_required_check_failed",
+ }
+ }
+}
+
+/// Closed structured-log severity vocabulary admitted by Myc.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycLogLevel {
+ Trace,
+ Debug,
+ Info,
+ Warn,
+ Error,
+}
+
+impl MycLogLevel {
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Trace => "trace",
+ Self::Debug => "debug",
+ Self::Info => "info",
+ Self::Warn => "warn",
+ Self::Error => "error",
+ }
+ }
+}
+
+/// Closed structured-log event vocabulary required by the current runtime plan.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycLogEvent {
+ ProcessResult,
+ Lifecycle,
+ CriticalTaskFailed,
+ ShutdownRequested,
+ ShutdownForced,
+}
+
+impl MycLogEvent {
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::ProcessResult => "process_result",
+ Self::Lifecycle => "lifecycle",
+ Self::CriticalTaskFailed => "critical_task_failed",
+ Self::ShutdownRequested => "shutdown_requested",
+ Self::ShutdownForced => "shutdown_forced",
+ }
+ }
+}
+
+/// One sealed canonical structured log record containing only governed values.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct MycLogRecord {
+ level: MycLogLevel,
+ event: MycLogEvent,
+ code: &'static str,
+ process_result: Option<MycProcessResult>,
+}
+
+impl MycLogRecord {
+ /// Builds the exact terminal record for one governed process result.
+ #[must_use]
+ pub const fn process_result(result: MycProcessResult) -> Self {
+ let level = match result {
+ MycProcessResult::Success => MycLogLevel::Info,
+ MycProcessResult::OperationRejectedOrConflict => MycLogLevel::Warn,
+ MycProcessResult::UnexpectedInternal
+ | MycProcessResult::InputOrConfiguration
+ | MycProcessResult::ServiceOrDependencyUnavailable
+ | MycProcessResult::StateOrIdentityUnavailable
+ | MycProcessResult::DoctorRequiredCheckFailed => MycLogLevel::Error,
+ };
+ Self {
+ level,
+ event: MycLogEvent::ProcessResult,
+ code: result.code(),
+ process_result: Some(result),
+ }
+ }
+
+ /// Builds the exact phase record from an already-published lifecycle value.
+ #[must_use]
+ pub const fn lifecycle(phase: MycServicePhase) -> Self {
+ let (level, code) = match phase {
+ MycServicePhase::Starting => (MycLogLevel::Info, "starting"),
+ MycServicePhase::Ready => (MycLogLevel::Info, "ready"),
+ MycServicePhase::Degraded => (MycLogLevel::Warn, "degraded"),
+ MycServicePhase::Unready => (MycLogLevel::Warn, "unready"),
+ MycServicePhase::Stopping => (MycLogLevel::Info, "stopping"),
+ MycServicePhase::Failed => (MycLogLevel::Error, "failed"),
+ };
+ Self {
+ level,
+ event: MycLogEvent::Lifecycle,
+ code,
+ process_result: None,
+ }
+ }
+
+ #[must_use]
+ pub const fn critical_task_failed() -> Self {
+ Self {
+ level: MycLogLevel::Error,
+ event: MycLogEvent::CriticalTaskFailed,
+ code: "critical_task_failed",
+ process_result: None,
+ }
+ }
+
+ #[must_use]
+ pub const fn shutdown_requested() -> Self {
+ Self {
+ level: MycLogLevel::Info,
+ event: MycLogEvent::ShutdownRequested,
+ code: "first_signal",
+ process_result: None,
+ }
+ }
+
+ #[must_use]
+ pub const fn shutdown_forced() -> Self {
+ Self {
+ level: MycLogLevel::Error,
+ event: MycLogEvent::ShutdownForced,
+ code: "second_signal",
+ process_result: None,
+ }
+ }
+
+ #[must_use]
+ pub const fn level(&self) -> MycLogLevel {
+ self.level
+ }
+
+ #[must_use]
+ pub const fn event(&self) -> MycLogEvent {
+ self.event
+ }
+
+ #[must_use]
+ pub const fn code(&self) -> &'static str {
+ self.code
+ }
+
+ #[must_use]
+ pub const fn process_exit(&self) -> Option<MycProcessResult> {
+ self.process_result
+ }
+}
+
+impl fmt::Debug for MycLogRecord {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("MycLogRecord")
+ .field("level", &self.level)
+ .field("event", &self.event)
+ .field("code", &self.code)
+ .field(
+ "exit_code",
+ &self.process_result.map(MycProcessResult::exit_code_u8),
+ )
+ .finish()
+ }
+}
+
+impl fmt::Display for MycLogRecord {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ formatter,
+ "{{\"schema\":\"{LOG_SCHEMA}\",\"contract_version\":{MYC_DIAGNOSTICS_CONTRACT_VERSION},\"service\":\"myc\",\"level\":\"{}\",\"event\":\"{}\",\"code\":\"{}\"",
+ self.level.as_str(),
+ self.event.as_str(),
+ self.code,
+ )?;
+ if let Some(result) = self.process_result {
+ write!(formatter, ",\"exit_code\":{}", result.exit_code_u8())?;
+ }
+ formatter.write_str("}")
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -5,6 +5,7 @@
mod admin_v1;
mod cli_v1;
mod config_v1;
+mod diagnostics_v1;
mod doctor_v1;
mod nip46_admission;
mod nip46_authorization;
@@ -56,6 +57,10 @@ pub use config_v1::{
MycConfigDocumentV1, MycConfigProfile, MycConfigV1Error, MycConfigV1ErrorKind,
MycConfigValueSource, MycEffectiveConfigV1, parse_myc_config_v1,
};
+pub use diagnostics_v1::{
+ MYC_DIAGNOSTICS_CONTRACT_VERSION, MYC_LOG_RECORD_MAX_UTF8_BYTES, MycLogEvent, MycLogLevel,
+ MycLogRecord, MycProcessResult,
+};
pub use doctor_v1::{
MYC_DOCTOR_CHECK_COUNT, MYC_DOCTOR_CONTRACT_VERSION, MYC_DOCTOR_REPORT_MAX_UTF8_BYTES,
MYC_DOCTOR_SUMMARY_MAX_UTF8_BYTES, MycDoctorAggregateStatus, MycDoctorCheckDefinition,
diff --git a/src/main.rs b/src/main.rs
@@ -2,16 +2,20 @@
use std::process::ExitCode;
+use myc::{MycLogRecord, MycProcessResult};
+
fn main() -> ExitCode {
match myc::parse_myc_cli_v1_from(std::env::args_os()) {
Ok(invocation) => {
let _plan = myc::plan_myc_cli_v1(&invocation);
- eprintln!("myc: command execution is unavailable");
- ExitCode::FAILURE
+ let result = MycProcessResult::ServiceOrDependencyUnavailable;
+ eprintln!("{}", MycLogRecord::process_result(result));
+ result.exit_code()
}
- Err(error) => {
- eprintln!("myc: {error}");
- ExitCode::from(2)
+ Err(_) => {
+ let result = MycProcessResult::InputOrConfiguration;
+ eprintln!("{}", MycLogRecord::process_result(result));
+ result.exit_code()
}
}
}
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -15,6 +15,10 @@ const NIP46_COMPLETION: &str = include_str!("../src/state_completion.rs");
const NIP46_RESPONSE: &str = include_str!("../src/state_response.rs");
const DELIVERY_RECOVERY: &str = include_str!("../src/state_recovery.rs");
const DOCTOR_V1: &str = include_str!("../src/doctor_v1.rs");
+const DIAGNOSTICS_V1: &str = include_str!("../src/diagnostics_v1.rs");
+const DIAGNOSTICS_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/diagnostics.v1.json");
+const MAIN: &str = include_str!("../src/main.rs");
const STATUS_V1: &str = include_str!("../src/status_v1.rs");
const STATUS_CONTRACT: &str = include_str!("../contracts/services_hardening/status_cache.v1.json");
const OPERATIONS_V1: &str = include_str!("../src/operations_v1.rs");
@@ -42,6 +46,7 @@ const SOURCES: &[&str] = &[
include_str!("../src/cli_v1.rs"),
include_str!("../src/config_v1.rs"),
include_str!("../src/doctor_v1.rs"),
+ include_str!("../src/diagnostics_v1.rs"),
include_str!("../src/nip46_admission.rs"),
include_str!("../src/nip46_authorization.rs"),
include_str!("../src/nip46_replay.rs"),
@@ -83,6 +88,7 @@ fn implementation_modules_are_private_and_rustdoc_uses_the_reviewed_readme() {
"cli_v1",
"config_v1",
"doctor_v1",
+ "diagnostics_v1",
"nip46_admission",
"nip46_authorization",
"nip46_replay",
@@ -138,6 +144,10 @@ fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() {
"pub enum myc::MycCliAdminOperationV1",
"pub const fn myc::plan_myc_cli_v1",
"pub struct myc::MycDoctorReport",
+ "pub struct myc::MycLogRecord",
+ "pub enum myc::MycLogEvent",
+ "pub enum myc::MycLogLevel",
+ "pub enum myc::MycProcessResult",
"pub struct myc::MycDoctorCheckDefinition",
"pub struct myc::MycDoctorCheckResult",
"pub enum myc::MycDoctorCheckId",
@@ -231,6 +241,7 @@ fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() {
"cli_v1",
"config_v1",
"doctor_v1",
+ "diagnostics_v1",
"nip46_admission",
"nip46_authorization",
"nip46_replay",
@@ -421,6 +432,52 @@ fn doctor_boundary_is_closed_bounded_and_dependency_neutral() {
}
#[test]
+fn step156_diagnostics_are_closed_stderr_only_and_whole_chain_redacted() {
+ let contract: serde_json::Value =
+ serde_json::from_str(DIAGNOSTICS_CONTRACT).expect("Step 156 diagnostics contract");
+ assert_eq!(contract["schema"], "radroots.myc.diagnostics.v1");
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(contract["step"], 156);
+ assert_eq!(contract["stream_policy"]["result_data"], "stdout");
+ assert_eq!(contract["stream_policy"]["logs_and_diagnostics"], "stderr");
+ assert_eq!(contract["public_error_policy"]["error_source"], "none");
+ for required in [
+ "MYC_LOG_RECORD_MAX_UTF8_BYTES: usize = 512",
+ "Self::Success => 0",
+ "Self::DoctorRequiredCheckFailed => 6",
+ "formatter.write_str(\"}\")",
+ "MycLogRecord::process_result(result)",
+ ] {
+ assert!(
+ DIAGNOSTICS_V1.contains(required) || MAIN.contains(required),
+ "Step 156 implementation is missing `{required}`"
+ );
+ }
+ assert!(MAIN.contains("eprintln!(\"{}\", MycLogRecord::process_result(result))"));
+ for forbidden in [
+ "process::exit",
+ "{error}",
+ "source()",
+ "raw_error",
+ "std::fs::",
+ "sqlx::",
+ "SystemTime",
+ ] {
+ assert!(
+ !DIAGNOSTICS_V1.contains(forbidden) && !MAIN.contains(forbidden),
+ "Step 156 diagnostic boundary gained `{forbidden}`"
+ );
+ }
+ assert!(
+ !MAIN
+ .lines()
+ .any(|line| line.trim_start().starts_with("println!("))
+ );
+ assert!(!SOURCES.join("\n").contains("fn source("));
+ assert!(!PUBLIC_API.contains("std::io::Error"));
+}
+
+#[test]
fn step148_response_commit_is_one_atomic_exact_byte_authority() {
let contract: serde_json::Value =
serde_json::from_str(NIP46_RESPONSE_CONTRACT).expect("Step 148 contract");
diff --git a/tests/services_hardening_diagnostics.rs b/tests/services_hardening_diagnostics.rs
@@ -0,0 +1,144 @@
+#![forbid(unsafe_code)]
+
+use std::process::Command;
+
+use myc::{
+ MYC_DIAGNOSTICS_CONTRACT_VERSION, MYC_LOG_RECORD_MAX_UTF8_BYTES, MycLogEvent, MycLogLevel,
+ MycLogRecord, MycProcessResult, MycServicePhase,
+};
+
+const CONTRACT: &str = include_str!("../contracts/services_hardening/diagnostics.v1.json");
+const OPERATOR_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/operator_contract.v1.json");
+
+fn process_results() -> [MycProcessResult; 7] {
+ [
+ MycProcessResult::Success,
+ MycProcessResult::UnexpectedInternal,
+ MycProcessResult::InputOrConfiguration,
+ MycProcessResult::ServiceOrDependencyUnavailable,
+ MycProcessResult::StateOrIdentityUnavailable,
+ MycProcessResult::OperationRejectedOrConflict,
+ MycProcessResult::DoctorRequiredCheckFailed,
+ ]
+}
+
+#[test]
+fn machine_contract_exit_inventory_matches_the_operator_contract_exactly() {
+ let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract");
+ let operator: serde_json::Value =
+ serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract");
+ assert_eq!(contract["schema"], "radroots.myc.diagnostics.v1");
+ assert_eq!(
+ contract["contract_version"],
+ MYC_DIAGNOSTICS_CONTRACT_VERSION
+ );
+ assert_eq!(contract["step"], 156);
+ assert_eq!(
+ contract["log_record"]["maximum_utf8_bytes_excluding_newline"],
+ MYC_LOG_RECORD_MAX_UTF8_BYTES
+ );
+
+ let diagnostics = contract["exit_codes"].as_array().expect("exit codes");
+ let operator = operator["exit_codes"].as_array().expect("operator exits");
+ assert_eq!(diagnostics.len(), operator.len());
+ for ((result, diagnostic), operator) in
+ process_results().into_iter().zip(diagnostics).zip(operator)
+ {
+ assert_eq!(diagnostic["code"], result.exit_code_u8());
+ assert_eq!(diagnostic["name"], result.code());
+ assert_eq!(diagnostic["code"], operator["code"]);
+ assert_eq!(diagnostic["name"], operator["name"]);
+ }
+ assert_eq!(contract["public_error_policy"]["error_source"], "none");
+ assert_eq!(contract["stream_policy"]["logs_and_diagnostics"], "stderr");
+ assert_eq!(contract["stream_policy"]["file_logging"], false);
+}
+
+#[test]
+fn process_lifecycle_task_and_signal_records_are_exact_and_bounded() {
+ for result in process_results() {
+ let record = MycLogRecord::process_result(result);
+ let rendered = record.to_string();
+ assert_eq!(record.event(), MycLogEvent::ProcessResult);
+ assert_eq!(record.code(), result.code());
+ assert_eq!(record.process_exit(), Some(result));
+ assert!(rendered.len() <= MYC_LOG_RECORD_MAX_UTF8_BYTES);
+ assert_eq!(
+ serde_json::from_str::<serde_json::Value>(&rendered).expect("record")["exit_code"],
+ result.exit_code_u8()
+ );
+ }
+
+ for (phase, level, code) in [
+ (MycServicePhase::Starting, MycLogLevel::Info, "starting"),
+ (MycServicePhase::Ready, MycLogLevel::Info, "ready"),
+ (MycServicePhase::Degraded, MycLogLevel::Warn, "degraded"),
+ (MycServicePhase::Unready, MycLogLevel::Warn, "unready"),
+ (MycServicePhase::Stopping, MycLogLevel::Info, "stopping"),
+ (MycServicePhase::Failed, MycLogLevel::Error, "failed"),
+ ] {
+ let record = MycLogRecord::lifecycle(phase);
+ assert_eq!(record.level(), level);
+ assert_eq!(record.event(), MycLogEvent::Lifecycle);
+ assert_eq!(record.code(), code);
+ assert_eq!(record.process_exit(), None);
+ assert!(!record.to_string().contains("exit_code"));
+ }
+
+ for (record, event, code) in [
+ (
+ MycLogRecord::critical_task_failed(),
+ MycLogEvent::CriticalTaskFailed,
+ "critical_task_failed",
+ ),
+ (
+ MycLogRecord::shutdown_requested(),
+ MycLogEvent::ShutdownRequested,
+ "first_signal",
+ ),
+ (
+ MycLogRecord::shutdown_forced(),
+ MycLogEvent::ShutdownForced,
+ "second_signal",
+ ),
+ ] {
+ assert_eq!(record.event(), event);
+ assert_eq!(record.code(), code);
+ assert!(record.to_string().len() <= MYC_LOG_RECORD_MAX_UTF8_BYTES);
+ }
+}
+
+#[test]
+fn binary_writes_only_fixed_json_diagnostics_to_stderr() {
+ let canary = "secret-canary-private-key-path-sql-relay-url";
+ let invalid = Command::new(env!("CARGO_BIN_EXE_myc"))
+ .arg(format!("--credential={canary}"))
+ .output()
+ .expect("invalid invocation");
+ assert_eq!(invalid.status.code(), Some(2));
+ assert!(invalid.stdout.is_empty());
+ let invalid_stderr = String::from_utf8(invalid.stderr).expect("invalid stderr");
+ assert_eq!(
+ invalid_stderr,
+ format!(
+ "{}\n",
+ MycLogRecord::process_result(MycProcessResult::InputOrConfiguration)
+ )
+ );
+ assert!(!invalid_stderr.contains(canary));
+
+ let unavailable = Command::new(env!("CARGO_BIN_EXE_myc"))
+ .args(["--profile", "service-host", "--instance", "primary", "run"])
+ .output()
+ .expect("admitted invocation");
+ assert_eq!(unavailable.status.code(), Some(3));
+ assert!(unavailable.stdout.is_empty());
+ assert_eq!(
+ String::from_utf8(unavailable.stderr).expect("unavailable stderr"),
+ format!(
+ "{}\n",
+ MycLogRecord::process_result(MycProcessResult::ServiceOrDependencyUnavailable)
+ )
+ );
+}