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
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ boundaries above remain the target modular MSA architecture.
| `tepp_simulation` | known-truth temporal/event data generation |
| `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics |
| `tepp_api` | versioned DTO, schema, and export contracts |
| `payload_bound` | untrusted documents, records, checkpoints, and LLM outputs fail closed without identity, provenance, size, and depth |
| `inferred_status` | inferred relations cannot be promoted to observed evidence or transitions |
| `support_edge` | support, contradiction, summary, and outcome_of edges are not state transitions |
| `system_clock` | system time cannot be replaced by event, assertion, document, available, or cutoff time |
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `payload_bound` identity gate: documents, serialized records, model checkpoints, and LLM outputs stay untrusted until identity, provenance, size, and depth validate; recovered accept/reject flags match known truth at a higher computed rate than accepting every payload (ADR 0008/0013).
- `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Duplicate retention changelog bullet

The new payload_bound bullet at CHANGELOG.md:9 is accompanied by a second identical persistence_postgres migration 0007 bullet at CHANGELOG.md:10, which already appears several times in the file. Likely a merge artifact, though it matches the file's existing duplication pattern.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- `inferred_status` identity gate: inferred relations cannot be promoted to observed evidence or to state transitions; recovered observed/inferred labels match known truth at a higher computed rate than treating every status as observed (ADR 0003).
- `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose.
- `support_edge` identity gate: support, contradiction, summary, and `outcome_of` edges cannot become state transitions; recovered evidential kinds match known truth at a higher computed rate than collapsing every kind to support (ADR 0002/0003).
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/payload_bound",
"crates/inferred_status",
"crates/support_edge",
"crates/system_clock",
Expand Down Expand Up @@ -47,6 +48,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/payload_bound",
"crates/inferred_status",
"crates/support_edge",
"crates/system_clock",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ crates/corpus_split
crates/tepp_simulation
crates/validation_core
crates/tepp_api
crates/payload_bound
crates/inferred_status
crates/support_edge
crates/system_clock
Expand Down
17 changes: 17 additions & 0 deletions crates/payload_bound/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "payload_bound"
description = "Untrusted payloads fail closed without identity, provenance, size, and depth."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
publish = false

[lints]
workspace = true
218 changes: 218 additions & 0 deletions crates/payload_bound/src/bound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
//! Size, depth, identity, and provenance gates for untrusted payloads.

use crate::PayloadBoundError;

/// Closed vocabulary of untrusted inbound payload kinds.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PayloadKind {
/// External document bytes.
Document,
/// Serialized domain or wire record.
SerializedRecord,
/// Model checkpoint or artifact bytes.
ModelCheckpoint,
/// LLM or agent output.
LlmOutput,
}

impl PayloadKind {
/// Return the stable wire payload-kind name.
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::Document => "document",
Self::SerializedRecord => "serialized_record",
Self::ModelCheckpoint => "model_checkpoint",
Self::LlmOutput => "llm_output",
}
}

/// Parse a stable wire payload-kind name.
///
/// # Errors
///
/// Returns [`PayloadBoundError::InvalidPayloadDecision`] for unrecognized
/// names.
pub fn from_wire_name(name: &str) -> Result<Self, PayloadBoundError> {
match name {
"document" => Ok(Self::Document),
"serialized_record" => Ok(Self::SerializedRecord),
"model_checkpoint" => Ok(Self::ModelCheckpoint),
"llm_output" => Ok(Self::LlmOutput),
_ => Err(PayloadBoundError::InvalidPayloadDecision),
}
}
}

/// Positive byte and nesting-depth limits for one untrusted payload.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PayloadBound {
max_bytes: usize,
max_depth: usize,
}

impl PayloadBound {
/// Construct a positive payload bound.
///
/// # Errors
///
/// Returns [`PayloadBoundError::InvalidBound`] when either maximum is zero.
pub const fn new(max_bytes: usize, max_depth: usize) -> Result<Self, PayloadBoundError> {
if max_bytes == 0 || max_depth == 0 {
return Err(PayloadBoundError::InvalidBound);
}
Ok(Self {
max_bytes,
max_depth,
})
}

/// Return the maximum accepted payload size in bytes.
#[must_use]
pub const fn max_bytes(self) -> usize {
self.max_bytes
}

/// Return the maximum accepted nesting depth.
#[must_use]
pub const fn max_depth(self) -> usize {
self.max_depth
}
}

/// Refuse an untrusted payload that lacks identity or provenance or exceeds
/// the configured size or depth bound.
///
/// # Errors
///
/// Returns a missing-identity, missing-provenance, size, or depth error.
pub fn refuse_untrusted_payload(
kind: PayloadKind,
identity: Option<&str>,
provenance: Option<&str>,
byte_len: usize,
depth: usize,
bound: PayloadBound,
) -> Result<(), PayloadBoundError> {
let _ = kind.wire_name();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: kind parameter is validated only cosmetically

refuse_untrusted_payload accepts a PayloadKind but only calls let _ = kind.wire_name(); (bound.rs) and never uses the kind to influence any decision. All four payload kinds are gated identically. This is presumably intentional for this slice (the discard exists to force the wire_name match arms to be exercised for coverage), but any future expectation that different kinds get different identity/size/depth policies is not supported here.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

match identity {
Some(value) if !value.is_empty() => {}
_ => return Err(PayloadBoundError::MissingIdentity),
}
match provenance {
Some(value) if !value.is_empty() => {}
_ => return Err(PayloadBoundError::MissingProvenance),
}
if byte_len > bound.max_bytes() {
return Err(PayloadBoundError::PayloadTooLarge);
}
if depth > bound.max_depth() {
return Err(PayloadBoundError::PayloadTooDeep);
}
Ok(())
}

/// Fraction of recovered accept/reject flags that match known truth.
///
/// # Errors
///
/// Returns [`PayloadBoundError::InvalidPayloadDecision`] when either slice is
/// empty or the lengths differ.
pub fn identity_recovery_rate(truth: &[bool], decided: &[bool]) -> Result<f64, PayloadBoundError> {
if truth.is_empty() || truth.len() != decided.len() {
return Err(PayloadBoundError::InvalidPayloadDecision);
}
let mut matches = 0_u32;
for (truth_flag, decided_flag) in truth.iter().zip(decided) {
if truth_flag == decided_flag {
matches += 1;
}
}
Ok(f64::from(matches) / truth.len() as f64)
}

#[cfg(test)]
mod tests {
use super::{PayloadBound, PayloadKind, identity_recovery_rate, refuse_untrusted_payload};
use crate::PayloadBoundError;

#[test]
fn local_branches_cover_kinds_bounds_and_payloads() {
assert_eq!(
PayloadBound::new(0, 1),
Err(PayloadBoundError::InvalidBound)
);
assert_eq!(
PayloadBound::new(1, 0),
Err(PayloadBoundError::InvalidBound)
);
let bound = PayloadBound::new(8, 2).expect("bound");
assert_eq!(bound.max_bytes(), 8);
assert_eq!(bound.max_depth(), 2);
assert_eq!(
refuse_untrusted_payload(PayloadKind::LlmOutput, None, Some("p"), 1, 1, bound),
Err(PayloadBoundError::MissingIdentity)
);
assert_eq!(
refuse_untrusted_payload(PayloadKind::Document, Some(""), Some("p"), 1, 1, bound),
Err(PayloadBoundError::MissingIdentity)
);
assert_eq!(
refuse_untrusted_payload(PayloadKind::SerializedRecord, Some("id"), None, 1, 1, bound),
Err(PayloadBoundError::MissingProvenance)
);
assert_eq!(
refuse_untrusted_payload(
PayloadKind::ModelCheckpoint,
Some("id"),
Some(""),
1,
1,
bound
),
Err(PayloadBoundError::MissingProvenance)
);
assert_eq!(
refuse_untrusted_payload(
PayloadKind::ModelCheckpoint,
Some("id"),
Some("p"),
9,
1,
bound
),
Err(PayloadBoundError::PayloadTooLarge)
);
assert_eq!(
refuse_untrusted_payload(PayloadKind::Document, Some("id"), Some("p"), 1, 3, bound),
Err(PayloadBoundError::PayloadTooDeep)
);
refuse_untrusted_payload(PayloadKind::Document, Some("id"), Some("p"), 8, 2, bound)
.expect("ok");
for kind in [
PayloadKind::Document,
PayloadKind::SerializedRecord,
PayloadKind::ModelCheckpoint,
PayloadKind::LlmOutput,
] {
assert_eq!(
PayloadKind::from_wire_name(kind.wire_name()).expect("round-trip"),
kind
);
}
assert_eq!(
PayloadKind::from_wire_name("trusted"),
Err(PayloadBoundError::InvalidPayloadDecision)
);
let matched = identity_recovery_rate(&[true], &[true]).expect("rate");
assert!((matched - 1.0).abs() < f64::EPSILON);
assert_eq!(
identity_recovery_rate(&[], &[]),
Err(PayloadBoundError::InvalidPayloadDecision)
);
assert_eq!(
identity_recovery_rate(&[true], &[]),
Err(PayloadBoundError::InvalidPayloadDecision)
);
}
}
74 changes: 74 additions & 0 deletions crates/payload_bound/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Fail-closed payload-bound errors.

use std::fmt;

/// A fail-closed payload-bound error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PayloadBoundError {
/// A maximum size or depth of zero was configured.
InvalidBound,
/// Identity was missing or empty.
MissingIdentity,
/// Provenance was missing or empty.
MissingProvenance,
/// The payload exceeded the configured byte bound.
PayloadTooLarge,
/// The payload exceeded the configured nesting-depth bound.
PayloadTooDeep,
/// A recovery slice was empty or length-mismatched.
InvalidPayloadDecision,
}

impl fmt::Display for PayloadBoundError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::InvalidBound => "payload bound must be positive",
Self::MissingIdentity => "untrusted payload is missing identity",
Self::MissingProvenance => "untrusted payload is missing provenance",
Self::PayloadTooLarge => "untrusted payload exceeds the byte bound",
Self::PayloadTooDeep => "untrusted payload exceeds the depth bound",
Self::InvalidPayloadDecision => "invalid payload-bound decision payload",
};
formatter.write_str(message)
}
}

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

#[cfg(test)]
mod tests {
use super::PayloadBoundError;

#[test]
fn error_messages_are_stable() {
for (error, message) in [
(
PayloadBoundError::InvalidBound,
"payload bound must be positive",
),
(
PayloadBoundError::MissingIdentity,
"untrusted payload is missing identity",
),
(
PayloadBoundError::MissingProvenance,
"untrusted payload is missing provenance",
),
(
PayloadBoundError::PayloadTooLarge,
"untrusted payload exceeds the byte bound",
),
(
PayloadBoundError::PayloadTooDeep,
"untrusted payload exceeds the depth bound",
),
(
PayloadBoundError::InvalidPayloadDecision,
"invalid payload-bound decision payload",
),
] {
assert_eq!(error.to_string(), message);
}
}
}
22 changes: 22 additions & 0 deletions crates/payload_bound/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::cast_precision_loss)]
//! Untrusted payloads fail closed without identity, provenance, size, and depth.
//!
//! Documents, serialized records, model checkpoints, and LLM outputs stay
//! untrusted until the owning boundary validates those four gates
//! (AGENTS.md; ADR 0008/0013).

mod bound;
mod error;

/// Positive byte and nesting-depth limits for one untrusted payload.
pub use bound::PayloadBound;
/// Closed vocabulary of untrusted inbound payload kinds.
pub use bound::PayloadKind;
/// Fraction of recovered accept/reject flags that match known truth.
pub use bound::identity_recovery_rate;
/// Refuse an untrusted payload that fails identity, provenance, size, or depth.
pub use bound::refuse_untrusted_payload;
/// Fail-closed payload-bound errors.
pub use error::PayloadBoundError;
7 changes: 7 additions & 0 deletions crates/payload_bound/tests/crate_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Integration contract for the `payload_bound` package identity.

#[test]
fn package_identity_is_stable() {
let observed = std::hint::black_box(env!("CARGO_PKG_NAME"));
assert_eq!(observed, "payload_bound");
}
Loading
Loading