From f96cbffb462b50efc29d211633871ee3c97d050b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 01:43:16 +0900 Subject: [PATCH 1/7] feat(privacy): record provider field codes without source text A disclosure receipt binds a purpose to field codes sent to a model provider. Source text, source identity, and blanket masking fail closed (ADR 0009). --- ARCHITECTURE.md | 1 + CHANGELOG.md | 1 + Cargo.lock | 4 + Cargo.toml | 2 + README.md | 3 +- crates/provider_receipt/Cargo.toml | 17 +++ crates/provider_receipt/src/error.rs | 64 +++++++++ crates/provider_receipt/src/lib.rs | 24 ++++ crates/provider_receipt/src/receipt.rs | 128 ++++++++++++++++++ .../provider_receipt/tests/crate_contract.rs | 7 + .../tests/receipt_contract.rs | 62 +++++++++ docs/PRIVACY_DATA_GOVERNANCE.md | 2 +- docs/TRACEABILITY.md | 2 +- docs/adr/0009-purpose-bound-pii-governance.md | 2 +- docs/adr/README.md | 2 +- docs/research/provider-disclosure-receipt.md | 30 ++++ docs/research/standards-and-literature.md | 2 + docs/validation/temporal-event-foundation.md | 1 + scripts/check_workspace_contract.py | 1 + 19 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 crates/provider_receipt/Cargo.toml create mode 100644 crates/provider_receipt/src/error.rs create mode 100644 crates/provider_receipt/src/lib.rs create mode 100644 crates/provider_receipt/src/receipt.rs create mode 100644 crates/provider_receipt/tests/crate_contract.rs create mode 100644 crates/provider_receipt/tests/receipt_contract.rs create mode 100644 docs/research/provider-disclosure-receipt.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db..55fca31a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 | +| `provider_receipt` | provider-disclosure field-code receipts; source text and identity are not disclosable | 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c1cc6e87..cdd99ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `provider_receipt` disclosure audit: a receipt records purpose and field codes sent to a model provider; source text, source identity, and blanket PII masking fail closed; recovered field codes match known truth at a higher computed rate than a collapsed set (ADR 0009). - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/Cargo.lock b/Cargo.lock index 372a55f4..d784835b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,6 +856,10 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "provider_receipt" +version = "0.1.0" + [[package]] name = "quote" version = "1.0.47" diff --git a/Cargo.toml b/Cargo.toml index 92565940..7a461f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/provider_receipt", ] default-members = [ "crates/evidence_core", @@ -23,6 +24,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/provider_receipt", ] [workspace.package] diff --git a/README.md b/README.md index ae74015d..3df4f5f9 100644 --- a/README.md +++ b/README.md @@ -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. @@ -22,6 +22,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api +crates/provider_receipt ``` ## Local verification diff --git a/crates/provider_receipt/Cargo.toml b/crates/provider_receipt/Cargo.toml new file mode 100644 index 00000000..cecc91c3 --- /dev/null +++ b/crates/provider_receipt/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "provider_receipt" +description = "Provider-disclosure receipts that refuse source text and identity." +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 diff --git a/crates/provider_receipt/src/error.rs b/crates/provider_receipt/src/error.rs new file mode 100644 index 00000000..805b5e8e --- /dev/null +++ b/crates/provider_receipt/src/error.rs @@ -0,0 +1,64 @@ +//! Fail-closed provider-receipt errors. + +use std::fmt; + +/// A fail-closed provider-receipt error. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ProviderReceiptError { + /// Raw source text was supplied to a provider receipt. + SourceTextNotDisclosable, + /// Source identity was supplied to a provider receipt. + SourceIdentityNotDisclosable, + /// Blanket PII masking was treated as a disclosure grant. + BlanketMaskIsNotAuthorization, + /// A receipt or recovery slice was empty or length-mismatched. + InvalidReceiptPayload, +} + +impl fmt::Display for ProviderReceiptError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::SourceTextNotDisclosable => "source text cannot appear in a provider receipt", + Self::SourceIdentityNotDisclosable => { + "source identity cannot appear in a provider receipt" + } + Self::BlanketMaskIsNotAuthorization => { + "blanket PII masking is not provider-disclosure authorization" + } + Self::InvalidReceiptPayload => "invalid provider-receipt payload", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for ProviderReceiptError {} + +#[cfg(test)] +mod tests { + use super::ProviderReceiptError; + + #[test] + fn error_messages_are_stable() { + for (error, message) in [ + ( + ProviderReceiptError::SourceTextNotDisclosable, + "source text cannot appear in a provider receipt", + ), + ( + ProviderReceiptError::SourceIdentityNotDisclosable, + "source identity cannot appear in a provider receipt", + ), + ( + ProviderReceiptError::BlanketMaskIsNotAuthorization, + "blanket PII masking is not provider-disclosure authorization", + ), + ( + ProviderReceiptError::InvalidReceiptPayload, + "invalid provider-receipt payload", + ), + ] { + assert_eq!(error.to_string(), message); + } + } +} diff --git a/crates/provider_receipt/src/lib.rs b/crates/provider_receipt/src/lib.rs new file mode 100644 index 00000000..376aea95 --- /dev/null +++ b/crates/provider_receipt/src/lib.rs @@ -0,0 +1,24 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +#![allow(clippy::cast_precision_loss)] +//! Provider-disclosure receipts that refuse source text and identity. +//! +//! A receipt records which field codes were sent to a model provider under one +//! purpose. It cannot carry source text or source identity, and blanket PII +//! masking is not a disclosure grant (ADR 0009). + +mod error; +mod receipt; + +/// Fail-closed provider-receipt errors. +pub use error::ProviderReceiptError; +/// One provider-disclosure receipt of field codes under a purpose. +pub use receipt::ProviderReceipt; +/// Fraction of recovered field codes that match known truth. +pub use receipt::receipt_recovery_rate; +/// Refuse to treat a blanket PII mask as provider-disclosure authorization. +pub use receipt::refuse_blanket_mask_as_disclosure; +/// Refuse to place source identity in a provider receipt. +pub use receipt::refuse_source_identity_in_receipt; +/// Refuse to place raw source text in a provider receipt. +pub use receipt::refuse_source_text_in_receipt; diff --git a/crates/provider_receipt/src/receipt.rs b/crates/provider_receipt/src/receipt.rs new file mode 100644 index 00000000..af3d98e7 --- /dev/null +++ b/crates/provider_receipt/src/receipt.rs @@ -0,0 +1,128 @@ +//! Purpose-bound field-code receipts for provider disclosure. + +use crate::ProviderReceiptError; + +/// One provider-disclosure receipt of field codes under a purpose. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderReceipt { + purpose_code: u16, + field_codes: Vec, +} + +impl ProviderReceipt { + /// Record the field codes sent to a provider under one purpose. + /// + /// # Errors + /// + /// Returns [`ProviderReceiptError::InvalidReceiptPayload`] when no field + /// codes are supplied. + pub fn new(purpose_code: u16, field_codes: &[u16]) -> Result { + if field_codes.is_empty() { + return Err(ProviderReceiptError::InvalidReceiptPayload); + } + Ok(Self { + purpose_code, + field_codes: field_codes.to_vec(), + }) + } + + /// Purpose bound to the disclosure. + #[must_use] + pub const fn purpose_code(&self) -> u16 { + self.purpose_code + } + + /// Field codes sent, never source text. + #[must_use] + pub fn field_codes(&self) -> &[u16] { + &self.field_codes + } +} + +/// Refuse to place raw source text in a provider receipt. +/// +/// # Errors +/// +/// Always returns [`ProviderReceiptError::SourceTextNotDisclosable`]. +pub fn refuse_source_text_in_receipt() -> Result<(), ProviderReceiptError> { + Err(ProviderReceiptError::SourceTextNotDisclosable) +} + +/// Refuse to place source identity in a provider receipt. +/// +/// # Errors +/// +/// Always returns [`ProviderReceiptError::SourceIdentityNotDisclosable`]. +pub fn refuse_source_identity_in_receipt() -> Result<(), ProviderReceiptError> { + Err(ProviderReceiptError::SourceIdentityNotDisclosable) +} + +/// Refuse to treat a blanket PII mask as provider-disclosure authorization. +/// +/// # Errors +/// +/// Always returns [`ProviderReceiptError::BlanketMaskIsNotAuthorization`]. +pub fn refuse_blanket_mask_as_disclosure() -> Result<(), ProviderReceiptError> { + Err(ProviderReceiptError::BlanketMaskIsNotAuthorization) +} + +/// Fraction of recovered field codes that match known truth. +/// +/// # Errors +/// +/// Returns [`ProviderReceiptError::InvalidReceiptPayload`] when the field-code +/// lengths differ. +pub fn receipt_recovery_rate( + truth: &ProviderReceipt, + decided: &ProviderReceipt, +) -> Result { + if truth.field_codes.len() != decided.field_codes.len() { + return Err(ProviderReceiptError::InvalidReceiptPayload); + } + let mut matches = 0_u32; + for (truth_field, decided_field) in truth.field_codes.iter().zip(&decided.field_codes) { + if truth_field == decided_field { + matches += 1; + } + } + Ok(f64::from(matches) / truth.field_codes.len() as f64) +} + +#[cfg(test)] +mod tests { + use super::{ + ProviderReceipt, receipt_recovery_rate, refuse_blanket_mask_as_disclosure, + refuse_source_identity_in_receipt, refuse_source_text_in_receipt, + }; + use crate::ProviderReceiptError; + + #[test] + fn local_branches_cover_construct_and_fail_closed_paths() { + let receipt = ProviderReceipt::new(7, &[1, 2]).expect("receipt"); + assert_eq!(receipt.purpose_code(), 7); + assert_eq!(receipt.field_codes(), &[1, 2]); + let matched = receipt_recovery_rate(&receipt, &receipt).expect("rate"); + assert!((matched - 1.0).abs() < f64::EPSILON); + assert_eq!( + ProviderReceipt::new(7, &[]), + Err(ProviderReceiptError::InvalidReceiptPayload) + ); + let short = ProviderReceipt::new(7, &[1]).expect("short"); + assert_eq!( + receipt_recovery_rate(&receipt, &short), + Err(ProviderReceiptError::InvalidReceiptPayload) + ); + assert_eq!( + refuse_source_text_in_receipt(), + Err(ProviderReceiptError::SourceTextNotDisclosable) + ); + assert_eq!( + refuse_source_identity_in_receipt(), + Err(ProviderReceiptError::SourceIdentityNotDisclosable) + ); + assert_eq!( + refuse_blanket_mask_as_disclosure(), + Err(ProviderReceiptError::BlanketMaskIsNotAuthorization) + ); + } +} diff --git a/crates/provider_receipt/tests/crate_contract.rs b/crates/provider_receipt/tests/crate_contract.rs new file mode 100644 index 00000000..4292829c --- /dev/null +++ b/crates/provider_receipt/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `provider_receipt` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "provider_receipt"); +} diff --git a/crates/provider_receipt/tests/receipt_contract.rs b/crates/provider_receipt/tests/receipt_contract.rs new file mode 100644 index 00000000..44af33d6 --- /dev/null +++ b/crates/provider_receipt/tests/receipt_contract.rs @@ -0,0 +1,62 @@ +//! Provider receipts cannot carry source text, identity, or a blanket mask. + +use provider_receipt::{ + ProviderReceipt, ProviderReceiptError, receipt_recovery_rate, + refuse_blanket_mask_as_disclosure, refuse_source_identity_in_receipt, + refuse_source_text_in_receipt, +}; + +fn receipt(purpose: u16, fields: &[u16]) -> ProviderReceipt { + ProviderReceipt::new(purpose, fields).expect("receipt") +} + +#[test] +fn source_text_identity_and_blanket_mask_cannot_enter_a_receipt() { + assert_eq!( + refuse_source_text_in_receipt(), + Err(ProviderReceiptError::SourceTextNotDisclosable) + ); + assert_eq!( + refuse_source_identity_in_receipt(), + Err(ProviderReceiptError::SourceIdentityNotDisclosable) + ); + assert_eq!( + refuse_blanket_mask_as_disclosure(), + Err(ProviderReceiptError::BlanketMaskIsNotAuthorization) + ); +} + +#[test] +fn recovered_field_codes_match_known_truth_better_than_a_collapsed_set() { + let truth = receipt(7, &[1, 2, 3]); + let recovered = receipt(7, &[1, 2, 3]); + let collapsed = receipt(7, &[1, 1, 1]); + let recovered_rate = receipt_recovery_rate(&truth, &recovered).expect("recovered"); + let collapsed_rate = receipt_recovery_rate(&truth, &collapsed).expect("collapsed"); + let expected = { + let mut matches = 0_u32; + for (truth_field, decided_field) in truth.field_codes().iter().zip(recovered.field_codes()) + { + if truth_field == decided_field { + matches += 1; + } + } + f64::from(matches) / f64::from(u32::try_from(truth.field_codes().len()).expect("len")) + }; + assert!((recovered_rate - expected).abs() < f64::EPSILON); + assert!(recovered_rate > collapsed_rate); +} + +#[test] +fn empty_or_mismatched_receipt_payloads_fail_closed() { + assert_eq!( + ProviderReceipt::new(7, &[]), + Err(ProviderReceiptError::InvalidReceiptPayload) + ); + let truth = receipt(7, &[1, 2]); + let short = receipt(7, &[1]); + assert_eq!( + receipt_recovery_rate(&truth, &short), + Err(ProviderReceiptError::InvalidReceiptPayload) + ); +} diff --git a/docs/PRIVACY_DATA_GOVERNANCE.md b/docs/PRIVACY_DATA_GOVERNANCE.md index 429c7687..d27f72c6 100644 --- a/docs/PRIVACY_DATA_GOVERNANCE.md +++ b/docs/PRIVACY_DATA_GOVERNANCE.md @@ -80,4 +80,4 @@ 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index c29d9743..6d42e4ba 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -33,7 +33,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | 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 | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | -| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | +| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `provider_receipt` field-code disclosure audit on the active PR; persistence/live HTTP remaining | active-PR | | tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | future service/persistence boundaries | accepted-target | | 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 | diff --git a/docs/adr/0009-purpose-bound-pii-governance.md b/docs/adr/0009-purpose-bound-pii-governance.md index 26fa3ad0..b5c85e44 100644 --- a/docs/adr/0009-purpose-bound-pii-governance.md +++ b/docs/adr/0009-purpose-bound-pii-governance.md @@ -1,7 +1,7 @@ # ADR 0009 — Purpose-bound PII governance without blanket masking **Decision status:** Accepted -**Implementation maturity:** accepted-target +**Implementation maturity:** active-PR **Date:** 2026-08-10 **Supersedes:** None. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1a9a7b31..76bbe971 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -14,7 +14,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | -| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | accepted-target | Controls are normative architecture; deployment/control evidence is not yet a certification claim. | +| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | active-PR | Provider-disclosure receipts in `provider_receipt` on the active PR; persistence, live HTTP, and certification evidence remain accepted-target. | | [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | accepted-target | Owns direct/verify/committee/conductor selection, budget, role/topology, and ablation policy. | | [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. | | [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. | diff --git a/docs/research/provider-disclosure-receipt.md b/docs/research/provider-disclosure-receipt.md new file mode 100644 index 00000000..32031f8f --- /dev/null +++ b/docs/research/provider-disclosure-receipt.md @@ -0,0 +1,30 @@ +# Provider-disclosure receipts (doctoring) + +## Scope + +`provider_receipt` records the purpose and field codes sent to a model +provider. Source text and source identity cannot enter the receipt. +Blanket PII masking is not a disclosure grant. Recovery is the computed +share of field codes that match known truth. + +This slice does not send HTTP, persist receipts, or claim CSAP, SOC 2, or +legal sufficiency. + +## Authority + +### Normative TEPP contract + +- `docs/adr/0009-purpose-bound-pii-governance.md` — model/provider payloads + are evidence-minimized and version/audit bound. +- `docs/PRIVACY_DATA_GOVERNANCE.md` — provider payload minimization and + raw-source log absence are required tests. + +### Supporting literature + +ISO/IEC 29100 treats data minimization and purpose specification as +distinct controls. They do **not** authorize copying source text into a +provider audit artifact, and they do not certify TEPP. + +International Organization for Standardization and International +Electrotechnical Commission. (2011). *Information technology—Security +techniques—Privacy framework* (ISO/IEC Standard No. 29100:2011). diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index b4b14468..c0641139 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -96,6 +96,8 @@ Moreau, L., & Missier, P. (Eds.). (2013). *PROV-DM: The PROV data model*. World TEPP separates stable record identity, content equality, exact text location, wire representation, authorization, and provenance. JSON wire records are explicit versioned DTOs with unknown-field rejection and reconstruct through domain validation. `SHA-256` detects content substitution but is not treated as proof of origin, authority, or chain of custody. +International Organization for Standardization and International Electrotechnical Commission. (2011). *Information technology—Security techniques—Privacy framework* (ISO/IEC Standard No. 29100:2011). Data minimization informs `provider_receipt`; it is not a certification claim. + ## AI risk, management systems, and assurance readiness International Organization for Standardization. (2023a). *Information technology—Artificial intelligence—Guidance on risk management* (ISO/IEC Standard No. 23894:2023). https://www.iso.org/standard/77304.html diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 295fbae0..d8fb1df9 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,6 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | +| Provider-disclosure receipts | `provider_receipt` | active-PR | this PR | recovered field-code rate vs collapsed set | ADR 0009 | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index c7b1ecf5..67070de0 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "provider_receipt", ) REQUIRED_CI_SNIPPETS: tuple[str, ...] = ( From 97e603b3691783cb9e9312d0acc8447ddea01052 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 15:47:12 +0900 Subject: [PATCH 2/7] feat(privacy): refuse untrusted intake without a grant Documents, serialized records, checkpoints, and LLM outputs stay outside the analysis boundary until a purpose-bound grant is present (ADR 0009). Size, identity, and provenance bounds are not that grant. Recovery is the computed share of grant-presence flags that match known truth versus accepting every intake. --- ARCHITECTURE.md | 1 + CHANGELOG.md | 1 + Cargo.lock | 4 + Cargo.toml | 2 + README.md | 3 +- crates/intake_authorization/Cargo.toml | 17 ++ crates/intake_authorization/src/error.rs | 55 +++++++ crates/intake_authorization/src/intake.rs | 152 ++++++++++++++++++ crates/intake_authorization/src/lib.rs | 24 +++ .../tests/crate_contract.rs | 7 + .../tests/intake_authorization_contract.rs | 62 +++++++ docs/TRACEABILITY.md | 2 +- docs/adr/0009-purpose-bound-pii-governance.md | 2 +- docs/adr/README.md | 2 +- .../research/intake-authorization-identity.md | 33 ++++ docs/research/standards-and-literature.md | 4 +- docs/validation/temporal-event-foundation.md | 1 + scripts/check_workspace_contract.py | 1 + 18 files changed, 368 insertions(+), 5 deletions(-) create mode 100644 crates/intake_authorization/Cargo.toml create mode 100644 crates/intake_authorization/src/error.rs create mode 100644 crates/intake_authorization/src/intake.rs create mode 100644 crates/intake_authorization/src/lib.rs create mode 100644 crates/intake_authorization/tests/crate_contract.rs create mode 100644 crates/intake_authorization/tests/intake_authorization_contract.rs create mode 100644 docs/research/intake-authorization-identity.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db..471be389 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index f3764d25..938578e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ 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). - `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. - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. diff --git a/Cargo.lock b/Cargo.lock index 372a55f4..b618e370 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,6 +584,10 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "intake_authorization" +version = "0.1.0" + [[package]] name = "io-uring" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index 92565940..f15023a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/intake_authorization", ] default-members = [ "crates/evidence_core", @@ -23,6 +24,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/intake_authorization", ] [workspace.package] diff --git a/README.md b/README.md index ae74015d..84bc31d4 100644 --- a/README.md +++ b/README.md @@ -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. @@ -22,6 +22,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api +crates/intake_authorization ``` ## Local verification diff --git a/crates/intake_authorization/Cargo.toml b/crates/intake_authorization/Cargo.toml new file mode 100644 index 00000000..15c7fb39 --- /dev/null +++ b/crates/intake_authorization/Cargo.toml @@ -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 diff --git a/crates/intake_authorization/src/error.rs b/crates/intake_authorization/src/error.rs new file mode 100644 index 00000000..5436108d --- /dev/null +++ b/crates/intake_authorization/src/error.rs @@ -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); + } + } +} diff --git a/crates/intake_authorization/src/intake.rs b/crates/intake_authorization/src/intake.rs new file mode 100644 index 00000000..1ac19b76 --- /dev/null +++ b/crates/intake_authorization/src/intake.rs @@ -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 { + 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 { + 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) +} + +#[cfg(test)] +mod tests { + use super::{ + identity_recovery_rate, refuse_bounds_as_authorization, refuse_intake_without_grant, + GrantPresence, IntakeKind, + }; + 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) + ); + } +} diff --git a/crates/intake_authorization/src/lib.rs b/crates/intake_authorization/src/lib.rs new file mode 100644 index 00000000..e69358e2 --- /dev/null +++ b/crates/intake_authorization/src/lib.rs @@ -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; +/// 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; +/// 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; diff --git a/crates/intake_authorization/tests/crate_contract.rs b/crates/intake_authorization/tests/crate_contract.rs new file mode 100644 index 00000000..9422e5dc --- /dev/null +++ b/crates/intake_authorization/tests/crate_contract.rs @@ -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"); +} diff --git a/crates/intake_authorization/tests/intake_authorization_contract.rs b/crates/intake_authorization/tests/intake_authorization_contract.rs new file mode 100644 index 00000000..4d64bd44 --- /dev/null +++ b/crates/intake_authorization/tests/intake_authorization_contract.rs @@ -0,0 +1,62 @@ +//! Untrusted intake fails closed without a grant; bounds are not authorization. + +use intake_authorization::{ + identity_recovery_rate, refuse_bounds_as_authorization, refuse_intake_without_grant, + GrantPresence, IntakeAuthorizationError, IntakeKind, +}; + +#[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) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index c29d9743..01997f26 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -33,7 +33,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | 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 | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | -| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | +| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `intake_authorization` grant-presence gate on the active PR; export/provider adapters remaining | partial | | tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | future service/persistence boundaries | accepted-target | | 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 | diff --git a/docs/adr/0009-purpose-bound-pii-governance.md b/docs/adr/0009-purpose-bound-pii-governance.md index 229733fd..838d8c91 100644 --- a/docs/adr/0009-purpose-bound-pii-governance.md +++ b/docs/adr/0009-purpose-bound-pii-governance.md @@ -1,7 +1,7 @@ # ADR 0009 — Purpose-bound PII governance without blanket masking **Decision status:** Accepted -**Implementation maturity:** partial — persistence retention/deletion/legal-hold (migration `0007`) is on the active PR and is not implemented-main until exact-head checks, review, and protected-main integration complete; authorization/export/provider adapters remain accepted-target +**Implementation maturity:** partial — persistence retention/deletion/legal-hold (migration `0007`) is implemented-main; untrusted-intake grant presence in `intake_authorization` is on the active PR; export/provider adapters remain accepted-target **Date:** 2026-08-10 **Supersedes:** None. diff --git a/docs/adr/README.md b/docs/adr/README.md index 5d3aa546..b5ff78ba 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -14,7 +14,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | -| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) is on the active PR; authorization/export/provider adapters and deployment evidence remain accepted-target. | +| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) is implemented-main; untrusted-intake grant presence is `intake_authorization` on the active PR; export/provider adapters remain accepted-target. | | [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | accepted-target | Owns direct/verify/committee/conductor selection, budget, role/topology, and ablation policy. | | [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. | | [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. | diff --git a/docs/research/intake-authorization-identity.md b/docs/research/intake-authorization-identity.md new file mode 100644 index 00000000..40519394 --- /dev/null +++ b/docs/research/intake-authorization-identity.md @@ -0,0 +1,33 @@ +# Untrusted intake requires a grant (doctoring) + +## Scope + +`intake_authorization` keeps documents, serialized records, checkpoints, +and LLM outputs out of the analysis boundary until a purpose-bound grant +is present. Size, identity, and provenance bounds are not that grant. +Recovery is the computed share of grant-presence flags that match known +truth. + +This slice does not persist grants, allocate migration `0008`, or replace +`purpose_authorization` (one grant, one purpose) or `payload_bound` +(identity/provenance/size/depth). + +## Authority + +### Normative TEPP contract + +- `docs/adr/0009-purpose-bound-pii-governance.md` — processing is + purpose-bound; blanket masking is not authorization. +- `AGENTS.md` — documents, serialized payloads, checkpoints, and LLM + outputs are untrusted until identity, provenance, size/depth, + authorization, and scientific semantics validate. + +### Supporting literature + +Voigt and Von dem Bussche (2017) treat purpose limitation as a +processing precondition, not a post-hoc filter. A size bound is not a +purpose. + +Voigt, P., & Von dem Bussche, A. (2017). *The EU General Data Protection +Regulation (GDPR): A practical guide*. Springer. +https://doi.org/10.1007/978-3-319-57959-7 diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 75710ed3..bffc05a2 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -100,9 +100,11 @@ TEPP separates stable record identity, content equality, exact text location, wi European Union. (2016). *Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data (General Data Protection Regulation)*. Official Journal of the European Union, L 119, 1–88. https://eur-lex.europa.eu/eli/reg/2016/679/oj +Voigt, P., & Von dem Bussche, A. (2017). *The EU General Data Protection Regulation (GDPR): A practical guide*. Springer. https://doi.org/10.1007/978-3-319-57959-7 + National Institute of Standards and Technology. (2020). *NIST privacy framework: A tool for improving privacy through enterprise risk management, version 1.0*. https://doi.org/10.6028/NIST.CSWP.01162020 -TEPP uses these sources, together with the AICPA Trust Services Criteria cited below, as readiness inputs for purpose-bound retention, deletion, and legal hold. They are not self-certification authority. Persistence migration `0007` records policy, hold, deletion requests, and evidence tombstones; it does not assert that a deployment is lawful under GDPR Article 17 or attested under SOC 2. +TEPP uses these sources, together with the AICPA Trust Services Criteria cited below, as readiness inputs for purpose-bound retention, deletion, and legal hold. They are not self-certification authority. Persistence migration `0007` records policy, hold, deletion requests, and evidence tombstones; it does not assert that a deployment is lawful under GDPR Article 17 or attested under SOC 2. Untrusted intake still requires a purpose-bound grant; identity and size bounds are not that grant (Voigt & Von dem Bussche, 2017). ## AI risk, management systems, and assurance readiness diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 295fbae0..58f1690d 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,6 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | +| Untrusted intake grant presence | `intake_authorization` | accepted-target | active PR | refuse missing grant + refuse bounds-as-authorization + recovery vs accept-all | ADR 0009; AGENTS.md | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index c7b1ecf5..6fbedb26 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "intake_authorization", ) REQUIRED_CI_SNIPPETS: tuple[str, ...] = ( From 89dca1c7cd2c804d4f5d93df4a40041772eaa72b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:06:28 +0900 Subject: [PATCH 3/7] style: apply workspace rustfmt --- crates/intake_authorization/src/intake.rs | 4 ++-- crates/intake_authorization/src/lib.rs | 8 ++++---- .../tests/intake_authorization_contract.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/intake_authorization/src/intake.rs b/crates/intake_authorization/src/intake.rs index 1ac19b76..9bf3198a 100644 --- a/crates/intake_authorization/src/intake.rs +++ b/crates/intake_authorization/src/intake.rs @@ -107,8 +107,8 @@ pub fn identity_recovery_rate( #[cfg(test)] mod tests { use super::{ - identity_recovery_rate, refuse_bounds_as_authorization, refuse_intake_without_grant, - GrantPresence, IntakeKind, + GrantPresence, IntakeKind, identity_recovery_rate, refuse_bounds_as_authorization, + refuse_intake_without_grant, }; use crate::IntakeAuthorizationError; diff --git a/crates/intake_authorization/src/lib.rs b/crates/intake_authorization/src/lib.rs index e69358e2..cc7d9b67 100644 --- a/crates/intake_authorization/src/lib.rs +++ b/crates/intake_authorization/src/lib.rs @@ -12,13 +12,13 @@ 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; -/// 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; diff --git a/crates/intake_authorization/tests/intake_authorization_contract.rs b/crates/intake_authorization/tests/intake_authorization_contract.rs index 4d64bd44..cf31fe9b 100644 --- a/crates/intake_authorization/tests/intake_authorization_contract.rs +++ b/crates/intake_authorization/tests/intake_authorization_contract.rs @@ -1,8 +1,8 @@ //! Untrusted intake fails closed without a grant; bounds are not authorization. use intake_authorization::{ - identity_recovery_rate, refuse_bounds_as_authorization, refuse_intake_without_grant, - GrantPresence, IntakeAuthorizationError, IntakeKind, + GrantPresence, IntakeAuthorizationError, IntakeKind, identity_recovery_rate, + refuse_bounds_as_authorization, refuse_intake_without_grant, }; #[test] From 387e42d784e228391291c94d0e173d083a4e99c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:26:31 +0900 Subject: [PATCH 4/7] test: derive crate contract from workspace manifest --- tests/quality/test_check_docstrings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/quality/test_check_docstrings.py b/tests/quality/test_check_docstrings.py index 2c11f7a5..da59f799 100644 --- a/tests/quality/test_check_docstrings.py +++ b/tests/quality/test_check_docstrings.py @@ -11,6 +11,7 @@ from unittest import mock from scripts import check_docstrings as docstrings +from scripts import check_workspace_contract as workspace_contract REPOSITORY_ROOT = Path(__file__).resolve().parents[2] @@ -24,7 +25,7 @@ def test_live_repository_is_documented(self) -> None: sources = docstrings.rust_sources(REPOSITORY_ROOT) crate_roots = sorted(REPOSITORY_ROOT.glob("crates/*/src/lib.rs")) - self.assertEqual(len(crate_roots), 10) + self.assertEqual(len(crate_roots), len(workspace_contract.EXPECTED_CRATES)) self.assertTrue(set(crate_roots).issubset(sources)) self.assertGreaterEqual(len(sources), len(crate_roots)) self.assertEqual(docstrings.validate_repository(REPOSITORY_ROOT), []) From 5082f9425a2a6c94153bae7a0a672e761de69701 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:43:45 +0900 Subject: [PATCH 5/7] docs: remove duplicate traceability rows --- docs/TRACEABILITY.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 25388a7e..cdca93fb 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -31,10 +31,7 @@ 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 | future `interpretation_gateway` | accepted-target | -| adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | | purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `provider_receipt` field-code disclosure audit on the active PR; persistence/live HTTP remaining | active-PR | -| tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | future service/persistence boundaries | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | | 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 | From 46b28497a36336bade4ce3fccc86f713f535128d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:49:53 +0900 Subject: [PATCH 6/7] fix: register provider receipt in workspace --- Cargo.toml | 2 ++ scripts/check_workspace_contract.py | 1 + 2 files changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index f15023a8..02b0e32d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/provider_receipt", "crates/intake_authorization", ] default-members = [ @@ -24,6 +25,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/provider_receipt", "crates/intake_authorization", ] diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index 6fbedb26..78569bd1 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "provider_receipt", "intake_authorization", ) From a562adde8375426ac75e1681d85b90d280d5b4ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:26:00 +0900 Subject: [PATCH 7/7] docs(workspace): register provider receipt crate --- CHANGELOG.md | 3 +++ README.md | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3196fb94..41ea3d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ 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). - `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. diff --git a/README.md b/README.md index 84bc31d4..2adc1954 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ implemented in Rust. ## Current implementation state This branch establishes the Task 1 Rust workspace and quality-gate foundation. -The eleven bounded crates compile independently but intentionally expose no +The twelve bounded crates compile independently but intentionally expose no placeholder production APIs. Domain behavior begins in Task 2 with immutable evidence identifiers and source records. @@ -22,6 +22,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api +crates/provider_receipt crates/intake_authorization ```