Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
73253d4
test(bap): require fail-closed external outcome recovery
seonghobae Aug 22, 2026
ce485c7
feat(bap): classify external crash-recovery outcomes
seonghobae Aug 22, 2026
60729ab
feat(bap): expose recovery facade as library root
seonghobae Aug 22, 2026
9a9cd81
test(bap): align recovery outcome regression formatting
seonghobae Aug 22, 2026
0c4191b
test(bap): reject stale recovery redispatch signal
seonghobae Aug 22, 2026
a6e5a09
test(bap): format stale recovery regression
seonghobae Aug 22, 2026
700a372
fix(bap): validate recovery receipt before redispatch signal
seonghobae Aug 22, 2026
e15b3f7
test(bap): satisfy strict panic lint in recovery regression
seonghobae Aug 22, 2026
4e72b6f
docs(changelog): record fail-closed recovery receipt validation
seonghobae Aug 22, 2026
c8f2dfc
merge(bap): integrate reconciliation receipt parent
seonghobae Aug 22, 2026
f9c14b3
merge(bap): integrate read-only replay validation
seonghobae Aug 22, 2026
cf245ff
test(bap): require read-only recovery validation
seonghobae Aug 22, 2026
8940841
fix(bap): keep recovery validation read-only
seonghobae Aug 22, 2026
2eb1e12
test(bap): use read-only recovery validation
seonghobae Aug 22, 2026
8523642
Merge current BAP idempotency parent into recovery stack
seonghobae Aug 22, 2026
babe479
test(bap): deny crash redispatch for terminal tasks
seonghobae Aug 22, 2026
573718b
style(bap): format terminal recovery regression
seonghobae Aug 22, 2026
db9b3bb
fix(bap): forbid terminal crash redispatch signals
seonghobae Aug 22, 2026
a03cb16
Merge current BAP idempotency parent into recovery stack
seonghobae Aug 23, 2026
c5fb384
Merge current BAP idempotency parent into recovery stack
seonghobae Aug 23, 2026
0280cfe
test(bap): require recovery evidence digest binding
seonghobae Aug 24, 2026
db2127a
fix(bap): bind recovery classification to evidence digest
seonghobae Aug 24, 2026
f799996
docs(bap): record recovery evidence identity binding
seonghobae Aug 24, 2026
6918ce0
Merge current idempotency parent into recovery stack
seonghobae Aug 25, 2026
be3186e
test(bap): reject redispatch while reconciliation-held
seonghobae Aug 25, 2026
8a3463b
fix(bap): keep reconciliation state non-replayable
seonghobae Aug 25, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

- Added an active BAP in-memory command receipt that binds bounded tenant namespaces, idempotency keys, and task identities to accepted lifecycle transitions without claiming authenticated tenant authority, durable deduplication, or side-effect suppression.
- Receipt replay now additionally requires the lifecycle's actual most recently accepted transition to equal the retained receipt transition; same-state/same-sequence divergent histories and state-only restored snapshots fail closed instead of replaying ambiguous command evidence.
- Crash-recovery redispatch classification now binds the exact accepted BAP command receipt and a canonical lowercase SHA-256 recovery-evidence identity, validates the receipt against the lifecycle's exact most recently accepted transition, and keeps the digest as identity rather than authentication or retry authority; malformed evidence identities and stale or divergent receipt state fail closed.
- Aligned the hourly product-development branch-coverage toolchain and its one-shot materializer with the reviewed `nightly-2026-08-18` pin, and corrected the official Dependabot Rust-toolchain reference.
- Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result.
- Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port.
Expand Down
3 changes: 3 additions & 0 deletions crates/originweave-bap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ authors.workspace = true
repository.workspace = true
homepage.workspace = true

[lib]
path = "src/public_api.rs"

[lints]
workspace = true
205 changes: 205 additions & 0 deletions crates/originweave-bap/src/public_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//! Stable internal Browser Agent Protocol lifecycle and crash-recovery contracts.
//!
//! The public recovery types deliberately separate caller-supplied external
//! side-effect classification from task success or authority. Durable runtimes
//! remain responsible for authenticating recovery evidence and for revalidating
//! tenant, policy, destination, secret, and browser authority before any retry.

#![forbid(unsafe_code)]
#![deny(missing_docs)]

#[path = "lib.rs"]
mod lifecycle;

pub use lifecycle::*;

/// Caller-supplied classification of an external side effect during crash recovery.
///
/// This value is not proof that the classified outcome occurred. A durable
/// runtime or reconciler must authenticate and persist the evidence that
/// supports the classification. Unknown or explicitly unreconciled outcomes
/// fail closed and cannot authorize redispatch.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BapExternalSideEffectOutcome {
/// The recovery authority confirmed that the interrupted command caused no external side effect.
ConfirmedNoSideEffect,
/// The recovery authority confirmed that the interrupted command caused its external side effect.
ConfirmedSideEffect,
/// The recovery authority cannot determine whether the external side effect occurred.
UnknownOutcome,
/// Recovery evidence explicitly requires reconciliation before further action.
ReconciliationRequired,
}

/// Required fail-closed handling for one classified external recovery outcome.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BapRecoveryAction {
/// Revalidate normal authority and policy before considering command redispatch.
RevalidateBeforeRedispatch,
/// Verify the confirmed external side effect and its post-condition without redispatching it.
VerifyConfirmedSideEffect,
/// Reconcile external state before any retry, success, or terminal decision.
ReconcileBeforeFurtherAction,
}

impl BapExternalSideEffectOutcome {
/// Map the classification to the minimum required recovery action.
#[must_use]
pub const fn required_action(self) -> BapRecoveryAction {
match self {
Self::ConfirmedNoSideEffect => BapRecoveryAction::RevalidateBeforeRedispatch,
Self::ConfirmedSideEffect => BapRecoveryAction::VerifyConfirmedSideEffect,
Self::UnknownOutcome | Self::ReconciliationRequired => {
BapRecoveryAction::ReconcileBeforeFurtherAction
}
}
}
}

/// Validation failure for one crash-recovery evidence digest identity.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BapRecoveryEvidenceDigestError {
/// The digest was not canonical lowercase SHA-256 identity evidence.
InvalidFormat,
}

impl std::fmt::Display for BapRecoveryEvidenceDigestError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidFormat => formatter.write_str(
"recovery evidence digest must be sha256: followed by 64 lowercase hexadecimal digits",
),
}
}
}

impl std::error::Error for BapRecoveryEvidenceDigestError {}

/// Canonical SHA-256 identity for durable crash-recovery evidence.
///
/// The digest identifies the exact evidence object a durable recovery boundary must authenticate
/// before relying on an external side-effect classification. Possession of this identity does not
/// authenticate the evidence, prove the classified outcome, or grant retry, browser, network,
/// secret, approval, or storage authority.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BapRecoveryEvidenceDigest(String);

impl BapRecoveryEvidenceDigest {
/// Parse one exact `sha256:` identity with 64 lowercase hexadecimal digits.
pub fn parse(value: &str) -> Result<Self, BapRecoveryEvidenceDigestError> {
let Some(hex_digest) = value.strip_prefix("sha256:") else {
return Err(BapRecoveryEvidenceDigestError::InvalidFormat);
};
if hex_digest.len() != 64 {
return Err(BapRecoveryEvidenceDigestError::InvalidFormat);
}
if hex_digest
.bytes()
.any(|byte| !matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
{
return Err(BapRecoveryEvidenceDigestError::InvalidFormat);
}
Ok(Self(value.to_owned()))
}

/// Return the canonical lowercase SHA-256 identity.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}

/// Receipt- and evidence-bound crash-recovery classification for one accepted BAP command.
///
/// Binding the external outcome to both the immutable command receipt and exact recovery-evidence
/// digest prevents a recovery classification from floating free of the retry namespace, task,
/// lifecycle event, accepted transition, or the durable evidence object that supports the outcome.
/// Construction does not authenticate the classification or evidence and grants no authority;
/// callers must validate the evidence at their durable trust boundary.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BapCommandRecovery {
receipt: BapCommandReceipt,
external_outcome: BapExternalSideEffectOutcome,
evidence_digest: BapRecoveryEvidenceDigest,
}

impl BapCommandRecovery {
/// Bind one external outcome classification and evidence identity to an accepted command receipt.
#[must_use]
pub const fn new(
receipt: BapCommandReceipt,
external_outcome: BapExternalSideEffectOutcome,
evidence_digest: BapRecoveryEvidenceDigest,
) -> Self {
Self {
receipt,
external_outcome,
evidence_digest,
}
}

/// Return the immutable command receipt whose interrupted side effect is being classified.
#[must_use]
pub const fn receipt(&self) -> &BapCommandReceipt {
&self.receipt
}

/// Return the caller-supplied external side-effect classification.
#[must_use]
pub const fn external_outcome(&self) -> BapExternalSideEffectOutcome {
self.external_outcome
}

/// Return the exact recovery-evidence digest bound to this classification.
#[must_use]
pub const fn evidence_digest(&self) -> &BapRecoveryEvidenceDigest {
&self.evidence_digest
}

/// Return the minimum fail-closed handling required by the external outcome.
#[must_use]
pub const fn required_action(&self) -> BapRecoveryAction {
self.external_outcome.required_action()
}

/// Return whether redispatch may be considered for the current exact lifecycle state.
///
/// The retained receipt must still match the lifecycle's exact most recently accepted
/// transition before a confirmed absence of the external side effect can produce `true`.
/// Stale, foreign, state-only restored, or divergent lifecycle history therefore fails
/// closed with the underlying typed receipt error instead of emitting a redispatch signal.
/// An exact receipt for a terminal lifecycle also returns `Ok(false)` because a completed,
/// failed, cancelled, expired, or dead-lettered task cannot resume command dispatch. An exact
/// receipt for `ReconciliationRequired` likewise returns `Ok(false)`: an explicit reconciliation
/// hold cannot be bypassed merely because later recovery evidence classifies the interrupted
/// external operation as having caused no side effect. Resolving that hold is a separate
/// lifecycle transition, which also makes this retained receipt stale for subsequent replay.
/// Validation requires only read access to the lifecycle and cannot mutate an already accepted
/// transition or consume mutable execution authority.
///
/// `Ok(true)` is still not authorization to redispatch. The caller must separately
/// authenticate the exact recovery evidence identified by [`Self::evidence_digest`] and
/// revalidate tenant, policy, destination, secret, browser, and any other current authority
/// before dispatching the command again.
pub fn permits_redispatch(
&self,
lifecycle: &BapTaskLifecycle,
) -> Result<bool, BapCommandReceiptError> {
lifecycle.validate_replay(
&self.receipt,
self.receipt.idempotency_key(),
self.receipt.tenant_id(),
self.receipt.task_id(),
self.receipt.event(),
)?;
if lifecycle.state().is_terminal()
|| lifecycle.state() == BapTaskState::ReconciliationRequired
{
return Ok(false);
}
Ok(matches!(
self.required_action(),
BapRecoveryAction::RevalidateBeforeRedispatch
))
}
}
Loading
Loading