Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 |

No crate exposes placeholder production behavior in Task 1. This prevents an
empty façade from becoming a de facto public API before its invariants and tests
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

- `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.
- `tepp_api` adaptive orchestration router (ADR 0010): versioned `direct`/`verify`/`committee`/`conductor`/`abstain` selection from CPU `f64` risk, ambiguity, evidence, and token-budget inputs; recorded stages, recursion, decomposition, access lists, and role-specific reasoning effort; fail-closed document-controlled policy/access/credentials; LLM plans remain proposals under deterministic statistical authority; comparable-budget ablation requires a direct baseline; credential-free contextual-orchestrator binding. Live NIM HTTP remains accepted-target.
- `tepp_api` purpose-bound provider-payload minimization: time-bounded `PurposeGrant` evaluation, fail-closed expired/not-yet-valid/inverted/cross-tenant/impossible-calendar denial, semantic UTC calendar validation, refusal to copy identity mappings into model-provider payloads or ordinary logs, preservation of opaque analytical identifiers and membership roles (no blanket PII mask), a separately authorized scientific re-identification path, and an internally bound FIPS 180-4 SHA-256 audit digest appended through `ReidentificationAuditSink` before disclosure.
- `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013).
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/intake_authorization",
]
default-members = [
"crates/evidence_core",
Expand All @@ -23,6 +24,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/intake_authorization",
]

[workspace.package]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ implemented in Rust.
## Current implementation state

This branch establishes the Task 1 Rust workspace and quality-gate foundation.
The ten bounded crates compile independently but intentionally expose no
The eleven bounded crates compile independently but intentionally expose no
placeholder production APIs. Domain behavior begins in Task 2 with immutable
evidence identifiers and source records.

Expand All @@ -22,6 +22,7 @@ crates/corpus_split
crates/tepp_simulation
crates/validation_core
crates/tepp_api
crates/intake_authorization
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
```

## Local verification
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)
);
}
6 changes: 3 additions & 3 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ The full APA 7th standards/literature register remains `docs/research/standards-
| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | future `psychometric_core` | accepted-target |
| CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target |
| TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target |
| evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial |
| evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Traceability entry for evidence-bounded LLM interpretation quietly dropped tepp_api router claim

TRACEABILITY.md changed the "evidence-bounded LLM interpretation" row from partial (tepp_api router plus future interpretation_gateway) to accepted-target (future interpretation_gateway only), removing the reference to the existing tepp_api router. The very next row (TRACEABILITY.md) still lists tepp_api::route_orchestration as an active-PR/partial capability, so the router itself was not removed. This appears to be a deliberate re-scoping of what counts as "evidence-bounded interpretation" rather than a code change, but the downgrade of maturity is unrelated to this PR's stated intent (untrusted-intake grant presence). Worth confirming the maturity change is intended and not an accidental regression in the traceability ledger.

Open in Devin Review

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

| adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | `tepp_api::route_orchestration` + ablation record on the active PR; live contextual-orchestrator execution remaining | partial |
| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization plus provider-payload minimization / elevated re-identification implemented-main; persistence retention/deletion remaining | partial |
| tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | `tepp_api` time-bounded `PurposeGrant` + cross-tenant denial implemented-main; persistent `access_grant` storage remaining | partial |
| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization plus provider-payload minimization / elevated re-identification implemented-main; `intake_authorization` grant-presence gate on the active PR; persistence retention/deletion remaining | partial |
| tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | `tepp_api` time-bounded `PurposeGrant` + cross-tenant denial implemented-main; `intake_authorization` grant-presence gate on the active PR; persistent `access_grant` storage remaining | partial |
| standalone + modular CWL MSA / no cross-service DB coupling | ADR 0011; `docs/API_CONTRACT.md` | current standalone crates; future service ports | partial |
| naruon modular artifact consumer boundary | ADR 0011/0012; API contract | `docs/connectors/naruon-artifact-consumer.md` + PR #22 versioned consumer contract on protected main; `tepp_api` HTTP interchange (active PR); live HTTP service remaining | partial |
| contextual-orchestrator interpretation port boundary | ADR 0010/0011; LLM orchestration | `docs/connectors/contextual-orchestrator-interpretation-port.md`; live port remaining | partial |
Expand Down
Loading
Loading