diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db6..cb2b7cbc7 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 | +| `purpose_authorization` | purpose-bound grants; blanket PII masking is 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 0f83a9137..ec83975dd 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 +- `purpose_authorization` purpose-bound grants: a grant authorizes one processing purpose for one principal, cannot be reused across purposes, cannot be replaced by blanket PII masking, and recovered purposes match known truth at a higher computed rate than a collapsed single-purpose assignment. - `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). - `persistence_postgres` audit-event SQL contracts: append-only insert that refuses empty, oversized, or hostile `action_code` values before SQL is rendered. diff --git a/Cargo.lock b/Cargo.lock index 372a55f43..490ba0143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,6 +856,13 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "purpose_authorization" +version = "0.1.0" +dependencies = [ + "uuid", +] + [[package]] name = "quote" version = "1.0.47" diff --git a/Cargo.toml b/Cargo.toml index 925659406..094effe2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/purpose_authorization", ] default-members = [ "crates/evidence_core", @@ -23,6 +24,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/purpose_authorization", ] [workspace.package] diff --git a/README.md b/README.md index ae74015d3..f84e6aae5 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/purpose_authorization ``` ## Local verification diff --git a/crates/purpose_authorization/Cargo.toml b/crates/purpose_authorization/Cargo.toml new file mode 100644 index 000000000..d3841e487 --- /dev/null +++ b/crates/purpose_authorization/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "purpose_authorization" +description = "Purpose-bound authorization grants that refuse blanket masking." +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 + +[dependencies] +uuid = { workspace = true } + +[lints] +workspace = true diff --git a/crates/purpose_authorization/src/error.rs b/crates/purpose_authorization/src/error.rs new file mode 100644 index 000000000..9e9cdf7fa --- /dev/null +++ b/crates/purpose_authorization/src/error.rs @@ -0,0 +1,60 @@ +//! Fail-closed purpose-authorization errors. + +use std::fmt; + +/// A fail-closed purpose-authorization error. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PurposeAuthorizationError { + /// A grant was used for a purpose it does not authorize. + CrossPurposeUse, + /// Blanket PII masking was offered as a substitute for authorization. + BlanketMaskIsNotAuthorization, + /// An unknown purpose wire name was supplied. + UnknownPurpose, + /// Purpose slices were empty or length-mismatched. + InvalidPurposePayload, +} + +impl fmt::Display for PurposeAuthorizationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::CrossPurposeUse => "authorization grant used for a different purpose", + Self::BlanketMaskIsNotAuthorization => "blanket mask is not authorization", + Self::UnknownPurpose => "unknown processing purpose", + Self::InvalidPurposePayload => "invalid purpose payload", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for PurposeAuthorizationError {} + +#[cfg(test)] +mod tests { + use super::PurposeAuthorizationError; + + #[test] + fn error_messages_are_stable() { + for (error, message) in [ + ( + PurposeAuthorizationError::CrossPurposeUse, + "authorization grant used for a different purpose", + ), + ( + PurposeAuthorizationError::BlanketMaskIsNotAuthorization, + "blanket mask is not authorization", + ), + ( + PurposeAuthorizationError::UnknownPurpose, + "unknown processing purpose", + ), + ( + PurposeAuthorizationError::InvalidPurposePayload, + "invalid purpose payload", + ), + ] { + assert_eq!(error.to_string(), message); + } + } +} diff --git a/crates/purpose_authorization/src/grant.rs b/crates/purpose_authorization/src/grant.rs new file mode 100644 index 000000000..50f762547 --- /dev/null +++ b/crates/purpose_authorization/src/grant.rs @@ -0,0 +1,74 @@ +//! Purpose-bound grants held by an opaque principal. + +use crate::{PurposeAuthorizationError, PurposeCode, refuse_cross_purpose_use}; +use uuid::Uuid; + +/// Opaque principal that holds a purpose grant. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct PrincipalId(Uuid); + +impl PrincipalId { + /// Reconstruct from a UUID. + #[must_use] + pub const fn from_uuid(value: Uuid) -> Self { + Self(value) + } + + /// Borrow the UUID value. + #[must_use] + pub const fn as_uuid(self) -> Uuid { + self.0 + } +} + +/// One purpose-bound authorization grant. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AuthorizationGrant { + purpose: PurposeCode, + principal: PrincipalId, +} + +impl AuthorizationGrant { + /// Bind a principal to one processing purpose. + #[must_use] + pub const fn new(purpose: PurposeCode, principal: PrincipalId) -> Self { + Self { purpose, principal } + } + + /// Return the granted purpose. + #[must_use] + pub const fn purpose(self) -> PurposeCode { + self.purpose + } + + /// Return the holding principal. + #[must_use] + pub const fn principal(self) -> PrincipalId { + self.principal + } + + /// Authorize a requested purpose against this grant. + /// + /// # Errors + /// + /// Returns [`PurposeAuthorizationError::CrossPurposeUse`] when the + /// requested purpose differs. + pub fn authorize(self, requested: PurposeCode) -> Result<(), PurposeAuthorizationError> { + refuse_cross_purpose_use(self.purpose, requested) + } +} + +#[cfg(test)] +mod tests { + use super::{AuthorizationGrant, PrincipalId}; + use crate::PurposeCode; + use uuid::Uuid; + + #[test] + fn grant_accessors_round_trip() { + let principal = PrincipalId::from_uuid(Uuid::from_u128(8)); + let grant = AuthorizationGrant::new(PurposeCode::ExportFulfillment, principal); + assert_eq!(grant.purpose(), PurposeCode::ExportFulfillment); + assert_eq!(grant.principal().as_uuid(), Uuid::from_u128(8)); + } +} diff --git a/crates/purpose_authorization/src/lib.rs b/crates/purpose_authorization/src/lib.rs new file mode 100644 index 000000000..5c7634c32 --- /dev/null +++ b/crates/purpose_authorization/src/lib.rs @@ -0,0 +1,27 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +#![allow(clippy::cast_precision_loss)] +//! Purpose-bound authorization grants that refuse blanket PII masking. +//! +//! A grant authorizes one processing purpose for one principal. It cannot be +//! reused for another purpose, and masking identifiers is not authorization +//! (ADR 0009). + +mod error; +mod grant; +mod purpose; + +/// Fail-closed purpose-authorization errors. +pub use error::PurposeAuthorizationError; +/// One purpose-bound grant. +pub use grant::AuthorizationGrant; +/// Opaque principal identity. +pub use grant::PrincipalId; +/// Closed processing-purpose vocabulary. +pub use purpose::PurposeCode; +/// Fraction of recovered purposes that match known truth. +pub use purpose::purpose_recovery_rate; +/// Refuse to treat blanket PII masking as authorization. +pub use purpose::refuse_blanket_mask_as_authorization; +/// Refuse to use a grant for a different purpose. +pub use purpose::refuse_cross_purpose_use; diff --git a/crates/purpose_authorization/src/purpose.rs b/crates/purpose_authorization/src/purpose.rs new file mode 100644 index 000000000..1c453d9ef --- /dev/null +++ b/crates/purpose_authorization/src/purpose.rs @@ -0,0 +1,121 @@ +//! Closed processing-purpose vocabulary and recovery. + +use crate::PurposeAuthorizationError; + +/// Closed processing-purpose vocabulary bound to TEPP retention purposes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PurposeCode { + /// Psychometric and statistical analysis. + PsychometricAnalysis, + /// Legal or contractual preservation. + LegalPreservation, + /// Operations and audit review. + OperationsAudit, + /// Authorized export fulfillment. + ExportFulfillment, +} + +impl PurposeCode { + /// Stable wire name. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::PsychometricAnalysis => "psychometric_analysis", + Self::LegalPreservation => "legal_preservation", + Self::OperationsAudit => "operations_audit", + Self::ExportFulfillment => "export_fulfillment", + } + } + + /// Parse a stable wire purpose name. + /// + /// # Errors + /// + /// Returns [`PurposeAuthorizationError::UnknownPurpose`] for unrecognized names. + pub fn from_wire_name(name: &str) -> Result { + match name { + "psychometric_analysis" => Ok(Self::PsychometricAnalysis), + "legal_preservation" => Ok(Self::LegalPreservation), + "operations_audit" => Ok(Self::OperationsAudit), + "export_fulfillment" => Ok(Self::ExportFulfillment), + _ => Err(PurposeAuthorizationError::UnknownPurpose), + } + } +} + +/// Fraction of recovered purposes that match known truth. +/// +/// # Errors +/// +/// Returns [`PurposeAuthorizationError::InvalidPurposePayload`] when either +/// slice is empty or the lengths differ. +pub fn purpose_recovery_rate( + truth: &[PurposeCode], + decided: &[PurposeCode], +) -> Result { + if truth.is_empty() || truth.len() != decided.len() { + return Err(PurposeAuthorizationError::InvalidPurposePayload); + } + let mut matches = 0_u32; + for (truth_purpose, decided_purpose) in truth.iter().zip(decided) { + if truth_purpose == decided_purpose { + matches += 1; + } + } + Ok(f64::from(matches) / truth.len() as f64) +} + +/// Explicit refusal to use a grant for a different purpose. +/// +/// # Errors +/// +/// Returns [`PurposeAuthorizationError::CrossPurposeUse`] when the purposes +/// differ. +pub fn refuse_cross_purpose_use( + granted: PurposeCode, + requested: PurposeCode, +) -> Result<(), PurposeAuthorizationError> { + if granted == requested { + Ok(()) + } else { + Err(PurposeAuthorizationError::CrossPurposeUse) + } +} + +/// Explicit refusal to treat blanket PII masking as authorization. +/// +/// # Errors +/// +/// Always returns [`PurposeAuthorizationError::BlanketMaskIsNotAuthorization`]. +pub fn refuse_blanket_mask_as_authorization() -> Result<(), PurposeAuthorizationError> { + Err(PurposeAuthorizationError::BlanketMaskIsNotAuthorization) +} + +#[cfg(test)] +mod tests { + use super::{PurposeCode, purpose_recovery_rate}; + use crate::PurposeAuthorizationError; + + #[test] + fn wire_names_round_trip() { + for purpose in [ + PurposeCode::PsychometricAnalysis, + PurposeCode::LegalPreservation, + PurposeCode::OperationsAudit, + PurposeCode::ExportFulfillment, + ] { + assert_eq!( + PurposeCode::from_wire_name(purpose.wire_name()).expect("round trip"), + purpose + ); + } + assert_eq!( + PurposeCode::from_wire_name("marketing"), + Err(PurposeAuthorizationError::UnknownPurpose) + ); + assert_eq!( + purpose_recovery_rate(&[PurposeCode::OperationsAudit], &[]), + Err(PurposeAuthorizationError::InvalidPurposePayload) + ); + } +} diff --git a/crates/purpose_authorization/tests/crate_contract.rs b/crates/purpose_authorization/tests/crate_contract.rs new file mode 100644 index 000000000..e0bae78cf --- /dev/null +++ b/crates/purpose_authorization/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `purpose_authorization` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "purpose_authorization"); +} diff --git a/crates/purpose_authorization/tests/purpose_grant_contract.rs b/crates/purpose_authorization/tests/purpose_grant_contract.rs new file mode 100644 index 000000000..1b0c2ab92 --- /dev/null +++ b/crates/purpose_authorization/tests/purpose_grant_contract.rs @@ -0,0 +1,69 @@ +//! Purpose grants cannot be reused across purposes or replaced by blanket masking. + +use purpose_authorization::{ + AuthorizationGrant, PrincipalId, PurposeAuthorizationError, PurposeCode, purpose_recovery_rate, + refuse_blanket_mask_as_authorization, refuse_cross_purpose_use, +}; +use uuid::Uuid; + +#[test] +fn a_grant_cannot_authorize_a_different_purpose_or_a_blanket_mask() { + let grant = AuthorizationGrant::new( + PurposeCode::PsychometricAnalysis, + PrincipalId::from_uuid(Uuid::from_u128(3)), + ); + assert_eq!( + grant.authorize(PurposeCode::ExportFulfillment), + Err(PurposeAuthorizationError::CrossPurposeUse) + ); + assert_eq!( + refuse_cross_purpose_use( + PurposeCode::PsychometricAnalysis, + PurposeCode::LegalPreservation + ), + Err(PurposeAuthorizationError::CrossPurposeUse) + ); + assert_eq!( + refuse_blanket_mask_as_authorization(), + Err(PurposeAuthorizationError::BlanketMaskIsNotAuthorization) + ); + grant + .authorize(PurposeCode::PsychometricAnalysis) + .expect("same purpose"); +} + +#[test] +fn recovered_purposes_match_known_truth_better_than_a_single_purpose() { + let truth = [ + PurposeCode::PsychometricAnalysis, + PurposeCode::LegalPreservation, + PurposeCode::OperationsAudit, + ]; + let recovered = truth; + let collapsed = [ + PurposeCode::PsychometricAnalysis, + PurposeCode::PsychometricAnalysis, + PurposeCode::PsychometricAnalysis, + ]; + let recovered_rate = purpose_recovery_rate(&truth, &recovered).expect("recovered"); + let collapsed_rate = purpose_recovery_rate(&truth, &collapsed).expect("collapsed"); + let expected = { + let mut matches = 0_u32; + for (truth_purpose, decided_purpose) in truth.iter().zip(recovered.iter()) { + if truth_purpose == decided_purpose { + 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_purpose_payloads_fail_closed() { + assert_eq!( + purpose_recovery_rate(&[], &[]), + Err(PurposeAuthorizationError::InvalidPurposePayload) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index afada87ae..658b5f592 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` | `purpose_authorization` grant/cross-purpose/blanket-mask gates on the active PR; persistence/export adapters 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 26fa3ad0c..b3faf6a74 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 — `purpose_authorization` binds one purpose to one principal and refuses cross-purpose use and blanket masking; remaining persistence/export adapters remain accepted-target **Date:** 2026-08-10 **Supersedes:** None. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1a9a7b315..a7338bb32 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 | Purpose grants in `purpose_authorization` on the active PR; remaining persistence/export adapters and deployment evidence remain accepted-target. Not a certification claim. | | [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/purpose-bound-authorization.md b/docs/research/purpose-bound-authorization.md new file mode 100644 index 000000000..29d1d9dcd --- /dev/null +++ b/docs/research/purpose-bound-authorization.md @@ -0,0 +1,35 @@ +# Purpose-bound authorization grants (doctoring) + +## Scope + +`purpose_authorization` binds one processing purpose to one principal. A grant +cannot authorize a different purpose, and blanket PII masking is not +authorization. Recovery is the computed share of recovered purposes that match +known truth. + +This slice does not implement export adapters, persistence of grants, or a +legal sufficiency claim. + +## Authority + +### Normative TEPP contract + +- `docs/adr/0009-purpose-bound-pii-governance.md` — purpose-bound + authorization without blanket masking; identity/role/linkage remain + scientifically required when authorized. +- `docs/PRIVACY_DATA_GOVERNANCE.md` — opaque analytical identifiers, + separately protected identity mapping, and auditable privileged access. + +### Supporting literature + +International Organization for Standardization and International +Electrotechnical Commission. (2019). *Information technology—Security +techniques—Privacy framework* (ISO/IEC Standard No. 29100:2011/Amd 1:2018). +Purpose specification and use limitation are privacy-engineering controls. +They do **not** certify TEPP and do not authorize replacing authorization +with a global mask. + +International Organization for Standardization and International +Electrotechnical Commission. (2019). *Information security, cybersecurity and +privacy protection—Privacy information management systems—Requirements* +(ISO/IEC Standard No. 27701:2019). diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index b4b144684..04457ef06 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -114,6 +114,8 @@ National Institute of Standards and Technology. (n.d.). *AI risk management fram 한국인터넷진흥원. (n.d.). *클라우드서비스 보안인증제 제도소개*. Retrieved August 11, 2026, from https://isms.kisa.or.kr/main/csap/intro/index.jsp +International Organization for Standardization and International Electrotechnical Commission. (2011). *Information technology—Security techniques—Privacy framework* (ISO/IEC Standard No. 29100:2011). Purpose specification and use limitation inform `purpose_authorization`; they are not a certification claim. + TEPP uses these sources as management/risk/readiness inputs, not as self-certification authority. ISO/IEC 42001:2023 and ISO/IEC 23894:2023 are published international standards (International Organization for Standardization, 2023a, 2023b). NIST AI RMF 1.0 remains the published framework while NIST is preparing a revision (Tabassi, 2023; National Institute of Standards and Technology, n.d.); the repository tracks the revision but does not silently treat an unpublished successor as normative. AICPA Trust Services Criteria are readiness inputs rather than self-issued attestation (American Institute of Certified Public Accountants, 2023). KISA currently describes CSAP service types as IaaS, SaaS, and DaaS and grades as high, medium, and low, while noting that the high and medium grades await later implementation (한국인터넷진흥원, n.d.). CSAP and SOC 2 evidence depend on actual deployment/organization controls and independent assessment. ## Security, accessibility, and software supply chain diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index e367a798f..ff821970a 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 | +| Purpose-bound authorization grants | `purpose_authorization` | active-PR | this PR | cross-purpose + blanket-mask refusal | ADR 0009; not a certification claim | | 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 c7b1ecf58..b833e2a86 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "purpose_authorization", ) REQUIRED_CI_SNIPPETS: tuple[str, ...] = (