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 |
| `intake_authorization` | untrusted intake fails closed without a grant; bounds are not authorization |
| `summarizes_edge` | a summary is not a state transition and not the source document |
| `outcome_order` | input-process-outcome edges cannot move backward in event time |
| `retrospective_edge` | retrospective reporting cannot become a transition or a translation |
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `provider_receipt` disclosure receipt: records provider field codes and
purpose-bound receipt metadata without persisting source text or source
identity (ADR 0009).
- `intake_authorization` identity gate: documents, serialized records, checkpoints, and LLM outputs cannot be accepted without a purpose-bound grant; size/identity/provenance bounds are not that grant; recovered grant-presence flags match known truth at a higher computed rate than accepting every intake (ADR 0009).
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
- `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.
- `summarizes_edge` identity gate: a summary may point to earlier event time but cannot become a state transition or reuse the source document identity; recovered summary kinds match known truth at a higher computed rate than collapsing every summary to the source (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.
- `outcome_order` identity gate: `input_to` and `process_to` cannot move backward or stay contemporaneous in event-time rank; `outcome_of` may point at an earlier producer and cannot become a state transition; recovered kinds match known truth at a higher computed rate than collapsing every kind to `input_to` (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.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/provider_receipt",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 provider_receipt duplicated as workspace member

provider_receipt was already a workspace member, and it is now listed a second time in both members and default-members (default-members line 56), alongside the new intake_authorization. The EXPECTED_CRATES contract in check_workspace_contract.py and README.md were duplicated to match, so the approved-crate-list check enforces the redundant entry instead of rejecting it.

Prompt for agents
provider_receipt was already a workspace member before this PR (it still appears at Cargo.toml members line 34 and default-members line 76). This PR mistakenly re-adds it at members line 14 and default-members line 56 while adding the genuinely new intake_authorization. Remove the duplicate provider_receipt entries so it appears once in each list. The same duplicate was introduced into EXPECTED_CRATES in scripts/check_workspace_contract.py (the new entry near line 26 duplicates the existing one near line 46) and into the crate listing in README.md (line 46 duplicates line 66); remove those duplicates too. Keep intake_authorization as the only newly added crate. Verify member ordering stays consistent across Cargo.toml, the contract script, and the README.
Open in Devin Review

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

"crates/intake_authorization",
"crates/summarizes_edge",
"crates/outcome_order",
"crates/retrospective_edge",
Expand Down Expand Up @@ -51,6 +53,8 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/provider_receipt",
"crates/intake_authorization",
"crates/summarizes_edge",
"crates/outcome_order",
"crates/retrospective_edge",
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and component RMSE APIs; the remaining crates expose no placeholder production
APIs, and domain behavior for them begins in Task 2 with immutable evidence
identifiers and source records.
This branch establishes the Task 1 Rust workspace and quality-gate foundation.
The twelve bounded crates compile independently but intentionally expose no
The eleven bounded crates compile independently; Task 1 includes the
implemented `encrypted_mapping` crate with AES-256-GCM sealing and
purpose-bound opening, while the remaining domain behavior begins in Task 2
Expand All @@ -42,6 +43,8 @@ crates/corpus_split
crates/tepp_simulation
crates/validation_core
crates/tepp_api
crates/provider_receipt
crates/intake_authorization
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
crates/summarizes_edge
crates/outcome_order
crates/retrospective_edge
Expand Down
17 changes: 17 additions & 0 deletions crates/intake_authorization/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "intake_authorization"
description = "Untrusted intake fails closed without a grant; bounds are not authorization."
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
55 changes: 55 additions & 0 deletions crates/intake_authorization/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! Fail-closed intake-authorization errors.

use std::fmt;

/// A fail-closed intake-authorization error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum IntakeAuthorizationError {
/// Intake was attempted without a purpose-bound grant.
MissingGrant,
/// Size, identity, or provenance bounds were treated as authorization.
BoundsAreNotAuthorization,
/// A recovery slice was empty or length-mismatched.
InvalidIntakePayload,
}

impl fmt::Display for IntakeAuthorizationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::MissingGrant => "untrusted intake requires a purpose-bound grant",
Self::BoundsAreNotAuthorization => {
"identity, provenance, size, and depth bounds are not authorization"
}
Self::InvalidIntakePayload => "invalid intake-authorization payload",
};
formatter.write_str(message)
}
}

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

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

#[test]
fn error_messages_are_stable() {
for (error, message) in [
(
IntakeAuthorizationError::MissingGrant,
"untrusted intake requires a purpose-bound grant",
),
(
IntakeAuthorizationError::BoundsAreNotAuthorization,
"identity, provenance, size, and depth bounds are not authorization",
),
(
IntakeAuthorizationError::InvalidIntakePayload,
"invalid intake-authorization payload",
),
] {
assert_eq!(error.to_string(), message);
}
}
}
152 changes: 152 additions & 0 deletions crates/intake_authorization/src/intake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! Grant presence required at untrusted intake.

use crate::IntakeAuthorizationError;

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

impl IntakeKind {
/// Return the stable wire intake-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 intake-kind name.
///
/// # Errors
///
/// Returns [`IntakeAuthorizationError::InvalidIntakePayload`] for
/// unrecognized names.
pub fn from_wire_name(name: &str) -> Result<Self, IntakeAuthorizationError> {
match name {
"document" => Ok(Self::Document),
"serialized_record" => Ok(Self::SerializedRecord),
"model_checkpoint" => Ok(Self::ModelCheckpoint),
"llm_output" => Ok(Self::LlmOutput),
_ => Err(IntakeAuthorizationError::InvalidIntakePayload),
}
}
}

/// Whether a purpose-bound grant is present at intake.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GrantPresence {
/// A grant exists for this intake.
Present,
/// No grant exists for this intake.
Absent,
}

/// Refuse untrusted intake that has no purpose-bound grant.
///
/// Cross-purpose reuse of a present grant is owned by `purpose_authorization`.
/// Identity, provenance, size, and depth are owned by `payload_bound`.
///
/// # Errors
///
/// Returns [`IntakeAuthorizationError::MissingGrant`] when `grant` is
/// [`GrantPresence::Absent`].
pub fn refuse_intake_without_grant(
kind: IntakeKind,
grant: GrantPresence,
) -> Result<(), IntakeAuthorizationError> {
let _ = kind.wire_name();
match grant {
GrantPresence::Absent => Err(IntakeAuthorizationError::MissingGrant),
GrantPresence::Present => Ok(()),
}
}

/// Refuse to treat size, identity, or provenance bounds as authorization.
///
/// # Errors
///
/// Always returns [`IntakeAuthorizationError::BoundsAreNotAuthorization`].
pub fn refuse_bounds_as_authorization() -> Result<(), IntakeAuthorizationError> {
Err(IntakeAuthorizationError::BoundsAreNotAuthorization)
}

/// Fraction of recovered grant-presence flags that match known truth.
///
/// # Errors
///
/// Returns [`IntakeAuthorizationError::InvalidIntakePayload`] when either
/// slice is empty or the lengths differ.
pub fn identity_recovery_rate(
truth: &[bool],
decided: &[bool],
) -> Result<f64, IntakeAuthorizationError> {
if truth.is_empty() || truth.len() != decided.len() {
return Err(IntakeAuthorizationError::InvalidIntakePayload);
}
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)
Comment on lines +98 to +104

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: identity_recovery_rate match counter is u32 while inputs are unbounded slices

In intake.rs, matches is a u32 accumulated over truth.len() iterations while truth is an unbounded &[bool]. If a caller ever passed more than u32::MAX (~4.29e9) elements, the counter could overflow (panic in debug, wraparound in release). This is not realistic for practical inputs (would require multi-GB slices) so it is not a real-world bug, but using usize/u64 for the counter would be more robust and consistent with the usize denominator. Noting rather than flagging since it is not reachable in practice.

Open in Devin Review

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

}

#[cfg(test)]
mod tests {
use super::{
GrantPresence, IntakeKind, identity_recovery_rate, refuse_bounds_as_authorization,
refuse_intake_without_grant,
};
use crate::IntakeAuthorizationError;

#[test]
fn local_branches_cover_kinds_grants_and_payloads() {
for kind in [
IntakeKind::Document,
IntakeKind::SerializedRecord,
IntakeKind::ModelCheckpoint,
IntakeKind::LlmOutput,
] {
assert_eq!(
refuse_intake_without_grant(kind, GrantPresence::Absent),
Err(IntakeAuthorizationError::MissingGrant)
);
refuse_intake_without_grant(kind, GrantPresence::Present).expect("present");
assert_eq!(
IntakeKind::from_wire_name(kind.wire_name()).expect("round-trip"),
kind
);
}
assert_eq!(
refuse_bounds_as_authorization(),
Err(IntakeAuthorizationError::BoundsAreNotAuthorization)
);
assert_eq!(
IntakeKind::from_wire_name("trusted"),
Err(IntakeAuthorizationError::InvalidIntakePayload)
);
let matched = identity_recovery_rate(&[true], &[true]).expect("rate");
assert!((matched - 1.0).abs() < f64::EPSILON);
assert_eq!(
identity_recovery_rate(&[], &[]),
Err(IntakeAuthorizationError::InvalidIntakePayload)
);
assert_eq!(
identity_recovery_rate(&[true], &[]),
Err(IntakeAuthorizationError::InvalidIntakePayload)
);
}
}
24 changes: 24 additions & 0 deletions crates/intake_authorization/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::cast_precision_loss)]
//! Untrusted intake fails closed without a grant; bounds are not authorization.
//!
//! Documents, serialized records, checkpoints, and LLM outputs require a
//! purpose-bound grant at the intake boundary. Passing size or identity
//! bounds is not that grant (ADR 0009; AGENTS.md).

mod error;
mod intake;

/// Fail-closed intake-authorization errors.
pub use error::IntakeAuthorizationError;
/// Whether a purpose-bound grant is present at intake.
pub use intake::GrantPresence;
/// Closed vocabulary of untrusted inbound kinds that require a grant.
pub use intake::IntakeKind;
/// Fraction of recovered grant-presence flags that match known truth.
pub use intake::identity_recovery_rate;
/// Refuse to treat size, identity, or provenance bounds as authorization.
pub use intake::refuse_bounds_as_authorization;
/// Refuse untrusted intake that has no purpose-bound grant.
pub use intake::refuse_intake_without_grant;
7 changes: 7 additions & 0 deletions crates/intake_authorization/tests/crate_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Integration contract for the `intake_authorization` package identity.

#[test]
fn package_identity_is_stable() {
let observed = std::hint::black_box(env!("CARGO_PKG_NAME"));
assert_eq!(observed, "intake_authorization");
}
62 changes: 62 additions & 0 deletions crates/intake_authorization/tests/intake_authorization_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Untrusted intake fails closed without a grant; bounds are not authorization.

use intake_authorization::{
GrantPresence, IntakeAuthorizationError, IntakeKind, identity_recovery_rate,
refuse_bounds_as_authorization, refuse_intake_without_grant,
};

#[test]
fn untrusted_intake_fails_closed_without_a_grant() {
for kind in [
IntakeKind::Document,
IntakeKind::SerializedRecord,
IntakeKind::ModelCheckpoint,
IntakeKind::LlmOutput,
] {
assert_eq!(
refuse_intake_without_grant(kind, GrantPresence::Absent),
Err(IntakeAuthorizationError::MissingGrant)
);
refuse_intake_without_grant(kind, GrantPresence::Present).expect("grant present");
}
assert_eq!(
refuse_bounds_as_authorization(),
Err(IntakeAuthorizationError::BoundsAreNotAuthorization)
);
}

#[test]
fn recovered_grant_flags_match_known_truth_better_than_accepting_every_intake() {
let truth = [true, false, false];
let recovered = [true, false, false];
let collapsed = [true, true, true];
let recovered_rate = identity_recovery_rate(&truth, &recovered).expect("recovered");
let collapsed_rate = identity_recovery_rate(&truth, &collapsed).expect("collapsed");
let expected = {
let mut matches = 0_u32;
for (truth_flag, decided_flag) in truth.iter().zip(recovered.iter()) {
if truth_flag == decided_flag {
matches += 1;
}
}
f64::from(matches) / f64::from(u32::try_from(truth.len()).expect("len"))
};
assert!((recovered_rate - expected).abs() < f64::EPSILON);
assert!(recovered_rate > collapsed_rate);
}

#[test]
fn empty_or_mismatched_grant_flags_fail_closed() {
assert_eq!(
identity_recovery_rate(&[], &[]),
Err(IntakeAuthorizationError::InvalidIntakePayload)
);
assert_eq!(
identity_recovery_rate(&[true], &[]),
Err(IntakeAuthorizationError::InvalidIntakePayload)
);
assert_eq!(
identity_recovery_rate(&[true, false], &[true]),
Err(IntakeAuthorizationError::InvalidIntakePayload)
);
}
1 change: 1 addition & 0 deletions docs/PRIVACY_DATA_GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Ordinary logs contain identifiers/digests sufficient for diagnosis without copyi

## 10. Privacy validation

Required tests include cross-tenant denial, expired-purpose denial, re-identification-boundary checks, export authorization, provider payload minimization, raw-source log absence, deletion/retention behavior, audit replay, and derived-sensitive-data classification. Privacy controls must be tested with realistic author/customer/project/multiple-membership cases rather than only anonymous fixtures. The in-memory `provider_receipt` crate is the current disclosure-audit gate; persistence of receipts remains accepted-target.
Required tests include cross-tenant denial, expired-purpose denial, re-identification-boundary checks, export authorization, provider payload minimization, raw-source log absence, deletion/retention behavior, audit replay, and derived-sensitive-data classification. Privacy controls must be tested with realistic author/customer/project/multiple-membership cases rather than only anonymous fixtures. The in-memory `provider_receipt` crate is the current disclosure-audit gate; persistence of receipts remains accepted-target.
Required tests include cross-tenant denial, expired-purpose denial, re-identification-boundary checks, export authorization, provider payload minimization, raw-source log absence, deletion/retention behavior, audit replay, and derived-sensitive-data classification. Privacy controls must be tested with realistic author/customer/project/multiple-membership cases rather than only anonymous fixtures. The in-memory `operational_log` crate is the current source-separation gate: `try_record` is the only recording API and inspects source text, source identity, and blanket-mask intent; a source-identity `&str` cannot become an analytical subject. `persistence_postgres` `audit_event` inserts call the same gate before SQL is rendered. Live HTTP and provider adapters remain accepted-target.
Required tests include cross-tenant denial, expired-purpose denial, re-identification-boundary checks, export authorization, provider payload minimization, raw-source log absence, deletion/retention behavior, audit replay, and derived-sensitive-data classification. Privacy controls must be tested with realistic author/customer/project/multiple-membership cases rather than only anonymous fixtures. The in-memory `derived_sensitivity` crate is the current inheritance gate; persistence of classifications remains accepted-target.
Loading
Loading