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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract
pipeline replay tests, reference trusted host loop test, and serious external
consumer fixture without requiring developers to run the full DIND suite for
normal local iteration.
- `xtask test-slice` now includes `runtime-wal-ack`, a narrow runtime WAL
ACK witness. The slice runs WAL-backed app-facing submission acceptance,
scheduler tick receipt commit-before-publish, recovered runtime indexes,
CLI submission-posture JSON, stale-claim guard, and generated man-page
checks.
- The docs now include an executable local contract-host quickstart and a
v0.1.0 authority-boundary audit. The quickstart points developers at
`cargo xtask test-slice contract-path-release`, names the app-facing and
Expand Down Expand Up @@ -528,6 +533,15 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract

### Changed

- `echo-cli wal submission-posture` now exposes generic read-only recovery JSON
for one submission id and canonical envelope digest. The output reports retry
posture, recovered submission posture, receipt digest, and ticket digest
without importing any application nouns into Echo.
- Runtime WAL-backed scheduler ticks now roll back all tick WAL evidence from a
failed multi-head scheduler pass, treat missing or mismatched receipt
correlation evidence as an invariant error instead of a normal obstruction,
and bind runtime recovery certificates to rebuilt submission and receipt
indexes.
Comment thread
flyingrobots marked this conversation as resolved.
- `warp-wasm` no longer contains the legacy Stack Witness 0001
`createBuffer`/`replaceRange`/`textWindow` shortcut. QueryView requests now
route through the generic installed contract observer boundary only; without
Expand Down
47 changes: 47 additions & 0 deletions crates/warp-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ pub enum WalCommands {
#[arg(default_value = ".")]
root: PathBuf,
},
/// Report recovered posture for one submission id/envelope pair.
SubmissionPosture {
/// Filesystem WAL root to inspect.
root: PathBuf,
/// 64-character hex submission id.
#[arg(long)]
submission_id: String,
/// 64-character hex canonical envelope digest.
#[arg(long)]
canonical_envelope_digest: String,
},
}

/// Output format selector.
Expand Down Expand Up @@ -239,6 +250,42 @@ mod tests {
}
}

#[test]
fn parse_wal_submission_posture() {
let cli = Cli::try_parse_from([
"echo-cli",
"wal",
"submission-posture",
"runtime.wal",
"--submission-id",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"--canonical-envelope-digest",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
])
.unwrap();
match cli.command {
Commands::Wal {
command:
WalCommands::SubmissionPosture {
ref root,
ref submission_id,
ref canonical_envelope_digest,
},
} => {
assert_eq!(root, &PathBuf::from("runtime.wal"));
assert_eq!(
submission_id,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
assert_eq!(
canonical_envelope_digest,
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
);
}
_ => panic!("expected Wal submission-posture command"),
}
}

#[test]
fn unknown_subcommand_is_error() {
let result = Cli::try_parse_from(["echo-cli", "bogus"]);
Expand Down
8 changes: 8 additions & 0 deletions crates/warp-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,13 @@ fn main() -> Result<()> {
Commands::Wal {
command: WalCommands::Doctor { ref root },
} => wal::doctor(root, &cli.format),
Commands::Wal {
command:
WalCommands::SubmissionPosture {
ref root,
ref submission_id,
ref canonical_envelope_digest,
},
} => wal::submission_posture(root, submission_id, canonical_envelope_digest, &cli.format),
}
}
104 changes: 101 additions & 3 deletions crates/warp-cli/src/wal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

use std::path::Path;

use anyhow::Result;
use anyhow::{bail, Result};
use serde::Serialize;
use warp_core::causal_wal::{doctor_filesystem_store, RecoveryTailPosture};
use warp_core::causal_wal::{
doctor_filesystem_store, recover_filesystem_store, recover_receipt_index,
recover_submission_index, RecoveredSubmissionPosture, RecoveryAccessMode, RecoveryTailPosture,
SubmissionRetryPosture,
};

use crate::cli::OutputFormat;
use crate::output::emit;
use crate::output::{emit, hex_hash};

/// Read-only WAL doctor JSON/text report.
#[derive(Debug, Serialize)]
Expand All @@ -20,6 +24,18 @@ pub(crate) struct WalDoctorOutput {
pub(crate) obstruction_count: u64,
}

/// Read-only recovered posture for one submission id/envelope pair.
#[derive(Debug, Serialize)]
pub(crate) struct WalSubmissionPostureOutput {
pub(crate) root: String,
pub(crate) submission_id: String,
pub(crate) canonical_envelope_digest: String,
pub(crate) retry_posture: &'static str,
pub(crate) recovered_posture: Option<&'static str>,
pub(crate) receipt_digest: Option<String>,
pub(crate) ticket_digest: Option<String>,
}

/// Runs `echo-cli wal doctor`.
pub(crate) fn doctor(root: &Path, format: &OutputFormat) -> Result<()> {
let report = doctor_filesystem_store(root)?;
Expand All @@ -43,6 +59,49 @@ pub(crate) fn doctor(root: &Path, format: &OutputFormat) -> Result<()> {
emit(format, &text, &json)
}

/// Runs `echo-cli wal submission-posture`.
pub(crate) fn submission_posture(
root: &Path,
submission_id: &str,
canonical_envelope_digest: &str,
format: &OutputFormat,
) -> Result<()> {
let submission_id = parse_hash_hex(submission_id)?;
let canonical_envelope_digest = parse_hash_hex(canonical_envelope_digest)?;
let recovery = recover_filesystem_store(root, RecoveryAccessMode::ReadOnly)?;
let submissions = recover_submission_index(&recovery)?;
let receipts = recover_receipt_index(&recovery)?;
let matching_entry = submissions
.get(&submission_id)
.filter(|entry| entry.acceptance.canonical_envelope_digest == canonical_envelope_digest);
let output = WalSubmissionPostureOutput {
root: root.display().to_string(),
submission_id: hex_hash(&submission_id),
canonical_envelope_digest: hex_hash(&canonical_envelope_digest),
retry_posture: retry_posture_label(
submissions.retry_posture(submission_id, canonical_envelope_digest),
),
recovered_posture: matching_entry.map(|entry| recovered_posture_label(entry.posture)),
receipt_digest: matching_entry
.and_then(|entry| entry.receipt_digest.map(|digest| hex_hash(&digest))),
ticket_digest: matching_entry
.and_then(|_| receipts.ticket_by_submission.get(&submission_id))
.map(hex_hash),
};
let text = format!(
"echo-cli wal submission-posture\nRoot: {}\nSubmission: {}\nCanonical envelope: {}\nRetry posture: {}\nRecovered posture: {}\nReceipt: {}\nTicket: {}\n",
output.root,
output.submission_id,
output.canonical_envelope_digest,
output.retry_posture,
output.recovered_posture.unwrap_or("None"),
output.receipt_digest.as_deref().unwrap_or("None"),
output.ticket_digest.as_deref().unwrap_or("None")
);
Comment thread
flyingrobots marked this conversation as resolved.
let json = serde_json::to_value(&output)?;
emit(format, &text, &json)
}

fn tail_posture_label(posture: RecoveryTailPosture) -> &'static str {
match posture {
RecoveryTailPosture::Clean => "Clean",
Expand All @@ -52,3 +111,42 @@ fn tail_posture_label(posture: RecoveryTailPosture) -> &'static str {
RecoveryTailPosture::WouldTruncateAfter(_) => "WouldTruncateAfter",
}
}

fn recovered_posture_label(posture: RecoveredSubmissionPosture) -> &'static str {
match posture {
RecoveredSubmissionPosture::AcceptedPending => "AcceptedPending",
RecoveredSubmissionPosture::DecidedApplied => "DecidedApplied",
RecoveredSubmissionPosture::DecidedRejected => "DecidedRejected",
RecoveredSubmissionPosture::Obstructed => "Obstructed",
RecoveredSubmissionPosture::RecoveryFaulted => "RecoveryFaulted",
}
}

fn retry_posture_label(posture: SubmissionRetryPosture) -> &'static str {
match posture {
SubmissionRetryPosture::NotAccepted => "NotAccepted",
SubmissionRetryPosture::AlreadyAcceptedPending => "AlreadyAcceptedPending",
SubmissionRetryPosture::AlreadyDecidedApplied => "AlreadyDecidedApplied",
SubmissionRetryPosture::AlreadyDecidedRejected => "AlreadyDecidedRejected",
SubmissionRetryPosture::AlreadyObstructed => "AlreadyObstructed",
SubmissionRetryPosture::ConflictSameIdDifferentEnvelope => {
"ConflictSameIdDifferentEnvelope"
}
SubmissionRetryPosture::NewSubmissionWithoutPolicyDedupe => {
"NewSubmissionWithoutPolicyDedupe"
}
}
}

fn parse_hash_hex(input: &str) -> Result<warp_core::Hash> {
let bytes = hex::decode(input)?;
if bytes.len() != 32 {
bail!(
"expected a 64-character hex hash, got {} bytes",
bytes.len()
);
}
let mut hash = [0_u8; 32];
hash.copy_from_slice(&bytes);
Ok(hash)
}
Loading
Loading