diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 79bb5c34..1f6eb5e9 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 | | `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 | diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c230394..2ac12180 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). +- `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). diff --git a/Cargo.lock b/Cargo.lock index aa7dd001..ec2a0036 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -759,6 +759,10 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "intake_authorization" +version = "0.1.0" + [[package]] name = "interpretation_gateway" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f97046f2..56ef8db0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ 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", @@ -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", diff --git a/README.md b/README.md index 7902a6fc..89b425a3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -42,6 +43,8 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api +crates/provider_receipt +crates/intake_authorization crates/summarizes_edge crates/outcome_order crates/retrospective_edge 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..9bf3198a --- /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::{ + 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) + ); + } +} diff --git a/crates/intake_authorization/src/lib.rs b/crates/intake_authorization/src/lib.rs new file mode 100644 index 00000000..cc7d9b67 --- /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; +/// 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; 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..cf31fe9b --- /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::{ + 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) + ); +} diff --git a/docs/PRIVACY_DATA_GOVERNANCE.md b/docs/PRIVACY_DATA_GOVERNANCE.md index 8ffe0258..c7ae877e 100644 --- a/docs/PRIVACY_DATA_GOVERNANCE.md +++ b/docs/PRIVACY_DATA_GOVERNANCE.md @@ -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. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index b783d331..968f633d 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,11 +30,11 @@ The full APA 7th standards/literature register remains `docs/research/standards- | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | | posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_fit` ESEM loading and DSEM lag gates on the active PR; `psychometric_core` input gates remain #49; invariance/multilevel remain accepted-target | active-PR | | 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 | `prediction_contradiction` bounded Allen promotion gate on the active PR (`refuse_promotion` requires coverage; `refuse_contradiction_or_adjacency` is not promotion authority; remaining TDT/CHRONOS tasks stay accepted-target) | active-PR | -| 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, and credential-free contextual-orchestrator binding on protected main; live execution and learned conductor calibration remain future | partial | -| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization, elevated re-identification, and provider-payload minimization are implemented-main; migration `0007` retention/deletion/legal-hold SQL contracts are implemented-main; `encrypted_mapping` AES-256-GCM envelope is active on this PR; deployment/provider evidence remains accepted-target | active-PR | -| 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 | +| 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` | `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; `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 (PR #42 implemented-main); loopback live listener on the active PR; production TLS remaining | partial | | contextual-orchestrator interpretation port boundary | ADR 0010/0011; LLM orchestration | `docs/connectors/contextual-orchestrator-interpretation-port.md`; live port remaining | partial | diff --git a/docs/adr/0009-purpose-bound-pii-governance.md b/docs/adr/0009-purpose-bound-pii-governance.md index 3c96737d..13503b57 100644 --- a/docs/adr/0009-purpose-bound-pii-governance.md +++ b/docs/adr/0009-purpose-bound-pii-governance.md @@ -1,6 +1,7 @@ # ADR 0009 — Purpose-bound PII governance without blanket masking **Decision status:** Accepted +**Implementation maturity:** partial — persistence retention/deletion/legal-hold (migration `0007`) and purpose-bound provider-payload minimization are implemented-main; untrusted-intake grant presence in `intake_authorization` is on the active PR and is not implemented-main until exact-head checks, review, and protected-main integration complete; deployment/provider-region evidence remains accepted-target **Implementation maturity:** active-PR — `encrypted_mapping` seals source identity with AES-256-GCM authenticated encryption and an operating-system-generated nonce; persistence retention/deletion/legal-hold (migration `0007`) and purpose-bound provider-payload minimization are implemented-main; persistence/KMS and remaining authorization adapters stay accepted-target; deployment/provider-region evidence remains accepted-target **Implementation maturity:** partial — persistence retention/deletion/legal-hold (migration `0007`) and purpose-bound provider-payload minimization are implemented-main; provider-disclosure receipts with field-code-only evidence are on this active PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; deployment/provider-region evidence remains accepted-target **Implementation maturity:** active-PR — persistence retention/deletion/legal-hold (migration `0007`) and purpose-bound provider-payload minimization are implemented-main; `operational_log::try_record` and inspected `audit_event` inserts are on this active PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; deployment/provider-region evidence remains accepted-target diff --git a/docs/adr/README.md b/docs/adr/README.md index db65d722..c57375e0 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -38,6 +38,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | Identities/spans are implemented-main; inbound size/depth/identity/provenance refusal is `payload_bound` on the active PR. ADR 0013 governs persistence/split authority. | | [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) and provider-payload minimization are implemented-main; authorization/export and deployment evidence remain accepted-target. | | [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`) and provider-payload minimization are implemented-main; untrusted-intake grant presence is `intake_authorization` on the active PR; deployment evidence remains accepted-target. | | [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | active-PR | `encrypted_mapping` AES-256-GCM envelope on the active PR; persistence retention/deletion/legal-hold (`0007`) and provider-payload minimization are implemented-main; persistence/KMS and remaining adapters stay accepted-target. Controls are not a certification claim. | | [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Retention/deletion/legal-hold and provider-payload minimization are implemented-main; provider-disclosure receipts are active-PR; deployment evidence remains accepted-target. | | [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | active-PR | Persistence retention/deletion/legal-hold (`0007`) and provider-payload minimization are implemented-main; `operational_log::try_record` and inspected `audit_event` inserts are on the active PR; tenant/purpose/role/lifetime storage, live HTTP, and certification evidence remain accepted-target. | 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 2d0a5885..e8cd09c4 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -125,13 +125,17 @@ 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. A summary is a PROV derivation of the source document, not a state transition and not a reuse of the source identity (Moreau & Missier, 2013). +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. + ## Privacy lifecycle, retention, and legal hold 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). ## Privacy and derived-data sensitivity diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 4edf01b7..c1cad65b 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -35,6 +35,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Mention-confidence Brier score | `event_core` | active-PR | calibration vs binary truth | perfect 0 / half 0.25 RMSE | ADR 0003; `docs/research/mention-confidence-brier.md` | | Checkpoint is not the estimator | `checkpoint_authority` | accepted-target | active PR | refuse checkpoint-as-estimator + unvalidated artifact + recovery vs estimator collapse | ADR 0001/0014 | | 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 | | Untrusted payload identity/provenance/size/depth | `payload_bound` | accepted-target | active PR | refuse missing identity/provenance and oversize/over-deep payloads + recovery vs accept-all | AGENTS.md untrusted-boundary | | System-clock identity | `system_clock` | active-PR | this PR | recovered system flags vs event-time stand-in | ADR 0002 | | Event-clock identity | `event_clock` | active-PR | this PR | recovered event flags vs assertion-time stand-in | ADR 0002 | diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index 01f00545..dacec89f 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,8 @@ "tepp_simulation", "validation_core", "tepp_api", + "provider_receipt", + "intake_authorization", "summarizes_edge", "outcome_order", "retrospective_edge",