From 31df67140239f83596a45ad1d9875a71ecbae85d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:05:11 +0900 Subject: [PATCH 1/9] feat(policy): restore purpose-bound sensitive authority on current main --- .../originweave-policy/src/sensitive_data.rs | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 crates/originweave-policy/src/sensitive_data.rs diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs new file mode 100644 index 000000000..0e38f19ac --- /dev/null +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -0,0 +1,220 @@ +//! Purpose-bound sensitive-data disclosure and opaque-handle authority. +//! +//! This module carries authority metadata only. It never stores or exposes the +//! protected value itself, performs no I/O, and grants no authority from ambient +//! session, network, repository, or model state. + +use originweave_core::Origin; + +const MAX_AUTHORITY_IDENTIFIER_BYTES: usize = 128; + +/// Classification applied to one protected field before disclosure policy runs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DataClassification { + /// Public information that does not require sensitive-data handling. + PublicData, + /// Internal information that is not intended for unrestricted disclosure. + InternalData, + /// Personal information associated with an identifiable person. + PersonalData, + /// Sensitive personal information requiring stronger disclosure controls. + SensitivePersonalData, + /// Authentication, authorization, or other credential material. + CredentialData, + /// Payment or financial account material. + PaymentData, +} + +/// The strongest disclosure action an exact authority scope permits. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisclosureDecision { + /// No disclosure is authorized. + DenyAccess, + /// Only an opaque broker handle may cross the policy boundary. + OpaqueHandleOnly, + /// Only a derived value may cross the policy boundary. + DerivedValueOnly, + /// A bounded subset of the field may be disclosed. + PartialFieldDisclosure, + /// The complete field may be disclosed to the exact bound destination. + FullFieldDisclosure, + /// Human approval is required before any requested disclosure. + HumanApprovalRequired, + /// Two independent controls must authorize the requested disclosure. + DualControlRequired, +} + +/// Exact authority metadata for one classified sensitive-data field use. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SensitiveDataAuthority { + tenant_id: String, + task_id: String, + field_id: String, + purpose_id: String, + destination: Origin, + classification: DataClassification, +} + +impl SensitiveDataAuthority { + /// Build one exact classified authority value without carrying protected data. + #[must_use] + pub fn new( + tenant_id: &str, + task_id: &str, + field_id: &str, + purpose_id: &str, + destination: Origin, + classification: DataClassification, + ) -> Self { + Self { + tenant_id: tenant_id.to_owned(), + task_id: task_id.to_owned(), + field_id: field_id.to_owned(), + purpose_id: purpose_id.to_owned(), + destination, + classification, + } + } + + fn is_complete(&self) -> bool { + authority_identifier_is_valid(&self.tenant_id) + && authority_identifier_is_valid(&self.task_id) + && authority_identifier_is_valid(&self.field_id) + && authority_identifier_is_valid(&self.purpose_id) + } +} + +fn authority_identifier_is_valid(identifier: &str) -> bool { + !identifier.is_empty() + && identifier.len() <= MAX_AUTHORITY_IDENTIFIER_BYTES + && identifier.bytes().any(|byte| byte.is_ascii_alphanumeric()) + && identifier + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) +} + +/// One requested disclosure, without carrying the protected field value. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SensitiveDataRequest { + authority: SensitiveDataAuthority, +} + +impl SensitiveDataRequest { + /// Build a disclosure request from one exact classified authority value. + #[must_use] + pub const fn new(authority: SensitiveDataAuthority) -> Self { + Self { authority } + } +} + +/// Explicit authority for one requested sensitive-data disclosure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DisclosureScope { + authority: SensitiveDataAuthority, + decision: DisclosureDecision, +} + +impl DisclosureScope { + /// Build an exact disclosure authority scope and its maximum permitted outcome. + #[must_use] + pub const fn new(authority: SensitiveDataAuthority, decision: DisclosureDecision) -> Self { + Self { authority, decision } + } +} + +/// Evaluate disclosure only from the exact request and explicit authority scope. +#[must_use] +pub fn evaluate_disclosure( + request: &SensitiveDataRequest, + scope: &DisclosureScope, +) -> DisclosureDecision { + if !request.authority.is_complete() + || !scope.authority.is_complete() + || request.authority != scope.authority + { + DisclosureDecision::DenyAccess + } else { + scope.decision + } +} + +/// Result of evaluating one attempted use of an opaque sensitive-value handle. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HandleUseDecision { + /// The supplied exact scope, classification, expiry, and prior-use count permit broker admission. + Authorized, + /// Tenant, task, field, purpose, destination, or classification did not match the handle scope. + ScopeMismatch, + /// The handle is no longer valid at the supplied trusted time. + Expired, + /// The bounded use count has already been consumed. + UseLimitReached, +} + +/// Authority metadata attached to an opaque sensitive-value handle. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SensitiveValueHandleScope { + authority: SensitiveDataAuthority, + expires_at_epoch_seconds: u64, + max_uses: u32, +} + +impl SensitiveValueHandleScope { + /// Build an opaque-handle scope with exact authority, exclusive expiry, and bounded use count. + #[must_use] + pub const fn new( + authority: SensitiveDataAuthority, + expires_at_epoch_seconds: u64, + max_uses: u32, + ) -> Self { + Self { + authority, + expires_at_epoch_seconds, + max_uses, + } + } +} + +/// One proposed use of an opaque sensitive-value handle. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HandleUseRequest { + authority: SensitiveDataAuthority, + now_epoch_seconds: u64, + uses_so_far: u32, +} + +impl HandleUseRequest { + /// Build a handle-use evaluation request from trusted time and authoritative broker state. + #[must_use] + pub const fn new( + authority: SensitiveDataAuthority, + now_epoch_seconds: u64, + uses_so_far: u32, + ) -> Self { + Self { + authority, + now_epoch_seconds, + uses_so_far, + } + } +} + +/// Evaluate whether authoritative broker state is admissible for one handle use. +#[must_use] +pub fn evaluate_handle_use( + request: &HandleUseRequest, + scope: &SensitiveValueHandleScope, +) -> HandleUseDecision { + if !request.authority.is_complete() + || !scope.authority.is_complete() + || request.authority != scope.authority + { + HandleUseDecision::ScopeMismatch + } else if request.now_epoch_seconds >= scope.expires_at_epoch_seconds { + HandleUseDecision::Expired + } else if request.uses_so_far >= scope.max_uses { + HandleUseDecision::UseLimitReached + } else { + HandleUseDecision::Authorized + } +} From 6c72b252cab1b4c42bf6e5d23da150e7e830563f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:05:21 +0900 Subject: [PATCH 2/9] test(policy): preserve classification-bound handles --- .../tests/handle_classification.rs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 crates/originweave-policy/tests/handle_classification.rs diff --git a/crates/originweave-policy/tests/handle_classification.rs b/crates/originweave-policy/tests/handle_classification.rs new file mode 100644 index 000000000..243ea2b08 --- /dev/null +++ b/crates/originweave-policy/tests/handle_classification.rs @@ -0,0 +1,43 @@ +#![allow(clippy::expect_used)] + +use originweave_core::Origin; +use originweave_policy::{ + DataClassification, HandleUseDecision, HandleUseRequest, SensitiveDataAuthority, + SensitiveValueHandleScope, evaluate_handle_use, +}; + +fn destination() -> Origin { + Origin::parse("https://shipping.example").expect("canonical destination") +} + +fn authority(classification: DataClassification) -> SensitiveDataAuthority { + SensitiveDataAuthority::new( + "tenant_alpha", + "task_ship_order", + "shipping_address", + "fulfill_order", + destination(), + classification, + ) +} + +#[test] +fn opaque_handle_use_requires_the_exact_data_classification() { + let scope = + SensitiveValueHandleScope::new(authority(DataClassification::PersonalData), 2_000, 2); + let permitted = HandleUseRequest::new(authority(DataClassification::PersonalData), 1_999, 0); + let reclassified = HandleUseRequest::new( + authority(DataClassification::SensitivePersonalData), + 1_999, + 0, + ); + + assert_eq!( + evaluate_handle_use(&permitted, &scope), + HandleUseDecision::Authorized + ); + assert_eq!( + evaluate_handle_use(&reclassified, &scope), + HandleUseDecision::ScopeMismatch + ); +} From 37bae5c44b59b06df9d1400fe92351f7dfffcbfb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:05:51 +0900 Subject: [PATCH 3/9] test(policy): restore exact sensitive-data authority regressions --- .../tests/sensitive_data_policy.rs | 390 ++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 crates/originweave-policy/tests/sensitive_data_policy.rs diff --git a/crates/originweave-policy/tests/sensitive_data_policy.rs b/crates/originweave-policy/tests/sensitive_data_policy.rs new file mode 100644 index 000000000..60e3f81fa --- /dev/null +++ b/crates/originweave-policy/tests/sensitive_data_policy.rs @@ -0,0 +1,390 @@ +#![allow(clippy::expect_used)] + +use originweave_core::Origin; +use originweave_policy::{ + DataClassification, DisclosureDecision, DisclosureScope, HandleUseDecision, HandleUseRequest, + SensitiveDataAuthority, SensitiveDataRequest, SensitiveValueHandleScope, evaluate_disclosure, + evaluate_handle_use, +}; + +const TENANT: &str = "tenant_alpha"; +const TASK: &str = "task_ship_order"; +const FIELD: &str = "shipping_address"; +const PURPOSE: &str = "fulfill_order"; +const DESTINATION: &str = "https://shipping.example"; + +#[derive(Clone, Copy)] +struct AuthorityCase<'a> { + tenant: &'a str, + task: &'a str, + field: &'a str, + purpose: &'a str, + destination: &'a str, +} + +fn authority_case<'a>( + tenant: &'a str, + task: &'a str, + field: &'a str, + purpose: &'a str, + destination: &'a str, +) -> AuthorityCase<'a> { + AuthorityCase { tenant, task, field, purpose, destination } +} + +fn exact_authority() -> AuthorityCase<'static> { + authority_case(TENANT, TASK, FIELD, PURPOSE, DESTINATION) +} + +fn origin(input: &str) -> Origin { + Origin::parse(input).expect("test origin must be valid") +} + +fn sensitive_authority( + authority: AuthorityCase<'_>, + classification: DataClassification, +) -> SensitiveDataAuthority { + SensitiveDataAuthority::new( + authority.tenant, + authority.task, + authority.field, + authority.purpose, + origin(authority.destination), + classification, + ) +} + +fn disclosure_request( + authority: AuthorityCase<'_>, + classification: DataClassification, +) -> SensitiveDataRequest { + SensitiveDataRequest::new(sensitive_authority(authority, classification)) +} + +fn disclosure_scope( + authority: AuthorityCase<'_>, + classification: DataClassification, + decision: DisclosureDecision, +) -> DisclosureScope { + DisclosureScope::new(sensitive_authority(authority, classification), decision) +} + +fn handle_scope( + authority: AuthorityCase<'_>, + classification: DataClassification, +) -> SensitiveValueHandleScope { + SensitiveValueHandleScope::new(sensitive_authority(authority, classification), 2_000, 2) +} + +fn handle_use( + authority: AuthorityCase<'_>, + classification: DataClassification, + now: u64, + uses: u32, +) -> HandleUseRequest { + HandleUseRequest::new(sensitive_authority(authority, classification), now, uses) +} + +fn assert_disclosure_denied(authority: AuthorityCase<'_>, classification: DataClassification) { + let permitted = disclosure_scope( + exact_authority(), + DataClassification::PersonalData, + DisclosureDecision::FullFieldDisclosure, + ); + assert_eq!( + evaluate_disclosure(&disclosure_request(authority, classification), &permitted), + DisclosureDecision::DenyAccess + ); +} + +fn assert_handle_scope_mismatch(authority: AuthorityCase<'_>, classification: DataClassification) { + let scope = handle_scope(exact_authority(), DataClassification::PersonalData); + assert_eq!( + evaluate_handle_use(&handle_use(authority, classification, 1_999, 0), &scope), + HandleUseDecision::ScopeMismatch + ); +} + +#[test] +fn disclosure_is_bound_to_every_exact_authority_dimension() { + let exact = exact_authority(); + let permitted = disclosure_scope( + exact, + DataClassification::PersonalData, + DisclosureDecision::FullFieldDisclosure, + ); + assert_eq!( + evaluate_disclosure( + &disclosure_request(exact, DataClassification::PersonalData), + &permitted, + ), + DisclosureDecision::FullFieldDisclosure + ); + + assert_disclosure_denied( + authority_case("tenant_beta", TASK, FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_disclosure_denied( + authority_case(TENANT, "task_other", FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_disclosure_denied( + authority_case(TENANT, TASK, "customer_email", PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_disclosure_denied( + authority_case(TENANT, TASK, FIELD, "marketing", DESTINATION), + DataClassification::PersonalData, + ); + assert_disclosure_denied( + authority_case(TENANT, TASK, FIELD, PURPOSE, "https://other.example"), + DataClassification::PersonalData, + ); + assert_disclosure_denied(exact, DataClassification::SensitivePersonalData); +} + +#[test] +fn sensitive_destination_uses_the_canonical_origin_boundary() { + let canonical = authority_case(TENANT, TASK, FIELD, PURPOSE, "HTTPS://Shipping.Example:443"); + assert_eq!( + evaluate_disclosure( + &disclosure_request(canonical, DataClassification::PersonalData), + &disclosure_scope( + exact_authority(), + DataClassification::PersonalData, + DisclosureDecision::FullFieldDisclosure, + ), + ), + DisclosureDecision::FullFieldDisclosure + ); + + assert_disclosure_denied( + authority_case(TENANT, TASK, FIELD, PURPOSE, "https://shipping.example:8443"), + DataClassification::PersonalData, + ); + + for invalid in [ + "https://user@shipping.example", + "https://shipping.example/path", + "https://shipping.example\n", + "https://배송.example", + "https://127.1", + "http://shipping.example", + ] { + assert!(Origin::parse(invalid).is_err(), "unexpected origin: {invalid}"); + } + assert!(Origin::parse("http://127.0.0.1").is_ok()); +} + +#[test] +fn every_supported_disclosure_outcome_is_preserved_by_exact_scope() { + let exact = exact_authority(); + let request = disclosure_request(exact, DataClassification::PersonalData); + for decision in [ + DisclosureDecision::DenyAccess, + DisclosureDecision::OpaqueHandleOnly, + DisclosureDecision::DerivedValueOnly, + DisclosureDecision::PartialFieldDisclosure, + DisclosureDecision::FullFieldDisclosure, + DisclosureDecision::HumanApprovalRequired, + DisclosureDecision::DualControlRequired, + ] { + assert_eq!( + evaluate_disclosure( + &request, + &disclosure_scope(exact, DataClassification::PersonalData, decision), + ), + decision + ); + } +} + +#[test] +fn opaque_handle_use_is_bound_to_scope_classification_expiry_and_use_count() { + let exact = exact_authority(); + let scope = handle_scope(exact, DataClassification::PersonalData); + assert_eq!( + evaluate_handle_use( + &handle_use(exact, DataClassification::PersonalData, 1_999, 1), + &scope, + ), + HandleUseDecision::Authorized + ); + assert_handle_scope_mismatch( + authority_case(TENANT, TASK, FIELD, PURPOSE, "https://other.example"), + DataClassification::PersonalData, + ); + assert_handle_scope_mismatch(exact, DataClassification::SensitivePersonalData); + assert_eq!( + evaluate_handle_use( + &handle_use(exact, DataClassification::PersonalData, 2_000, 1), + &scope, + ), + HandleUseDecision::Expired + ); + assert_eq!( + evaluate_handle_use( + &handle_use(exact, DataClassification::PersonalData, 1_999, 2), + &scope, + ), + HandleUseDecision::UseLimitReached + ); +} + +#[test] +fn handle_scope_mismatch_covers_every_authority_dimension() { + assert_handle_scope_mismatch( + authority_case("tenant_beta", TASK, FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_handle_scope_mismatch( + authority_case(TENANT, "task_other", FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_handle_scope_mismatch( + authority_case(TENANT, TASK, "customer_email", PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_handle_scope_mismatch( + authority_case(TENANT, TASK, FIELD, "marketing", DESTINATION), + DataClassification::PersonalData, + ); + assert_handle_scope_mismatch( + authority_case(TENANT, TASK, FIELD, PURPOSE, "https://other.example"), + DataClassification::PersonalData, + ); + assert_handle_scope_mismatch(exact_authority(), DataClassification::CredentialData); +} + +#[test] +fn incomplete_authority_never_grants_disclosure_or_handle_use() { + assert_disclosure_denied( + authority_case("", TASK, FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_disclosure_denied( + authority_case(TENANT, "", FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_disclosure_denied( + authority_case(TENANT, TASK, "", PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_disclosure_denied( + authority_case(TENANT, TASK, FIELD, "", DESTINATION), + DataClassification::PersonalData, + ); + + let exact = exact_authority(); + let request = disclosure_request(exact, DataClassification::PersonalData); + let incomplete_scope = disclosure_scope( + authority_case("", TASK, FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + DisclosureDecision::FullFieldDisclosure, + ); + assert_eq!( + evaluate_disclosure(&request, &incomplete_scope), + DisclosureDecision::DenyAccess + ); + + let incomplete_handle_scope = handle_scope( + authority_case("", TASK, FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); + assert_eq!( + evaluate_handle_use( + &handle_use(exact, DataClassification::PersonalData, 1_999, 0), + &incomplete_handle_scope, + ), + HandleUseDecision::ScopeMismatch + ); + assert_handle_scope_mismatch( + authority_case(TENANT, "", FIELD, PURPOSE, DESTINATION), + DataClassification::PersonalData, + ); +} + +#[test] +fn authority_identifiers_are_bounded_ascii_policy_tokens() { + let exact_maximum = "a".repeat(128); + let valid = authority_case( + &exact_maximum, + "task.ship-order:v1", + FIELD, + "fulfill-order", + DESTINATION, + ); + assert_eq!( + evaluate_disclosure( + &disclosure_request(valid, DataClassification::PersonalData), + &disclosure_scope( + valid, + DataClassification::PersonalData, + DisclosureDecision::FullFieldDisclosure, + ), + ), + DisclosureDecision::FullFieldDisclosure + ); + + let oversized = "a".repeat(129); + for punctuation_only in [":", "...", "_-_"] { + assert_invalid_equal_authority(authority_case( + punctuation_only, + TASK, + FIELD, + PURPOSE, + DESTINATION, + )); + } + assert_invalid_equal_authority(authority_case( + "tenant alpha", + TASK, + FIELD, + PURPOSE, + DESTINATION, + )); + assert_invalid_equal_authority(authority_case( + TENANT, + "task\nship_order", + FIELD, + PURPOSE, + DESTINATION, + )); + assert_invalid_equal_authority(authority_case( + TENANT, + TASK, + "배송주소", + PURPOSE, + DESTINATION, + )); + assert_invalid_equal_authority(authority_case(TENANT, TASK, FIELD, &oversized, DESTINATION)); + + let invalid_handle_authority = + authority_case("tenant alpha", TASK, FIELD, PURPOSE, DESTINATION); + assert_eq!( + evaluate_handle_use( + &handle_use( + invalid_handle_authority, + DataClassification::PersonalData, + 1_999, + 0, + ), + &handle_scope(invalid_handle_authority, DataClassification::PersonalData), + ), + HandleUseDecision::ScopeMismatch + ); +} + +fn assert_invalid_equal_authority(authority: AuthorityCase<'_>) { + let request = disclosure_request(authority, DataClassification::PersonalData); + let scope = disclosure_scope( + authority, + DataClassification::PersonalData, + DisclosureDecision::FullFieldDisclosure, + ); + assert_eq!( + evaluate_disclosure(&request, &scope), + DisclosureDecision::DenyAccess + ); +} From 3318f804709053e137c13b61e4ca7bed83ed6921 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:06:13 +0900 Subject: [PATCH 4/9] feat(policy): export purpose-bound sensitive authority --- crates/originweave-policy/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index c7df83f87..243ae8ce7 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -7,6 +7,14 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] +mod sensitive_data; + +pub use sensitive_data::{ + DataClassification, DisclosureDecision, DisclosureScope, HandleUseDecision, HandleUseRequest, + SensitiveDataAuthority, SensitiveDataRequest, SensitiveValueHandleScope, evaluate_disclosure, + evaluate_handle_use, +}; + use originweave_core::{ ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode, From 9614c72b2dca4f3cef24f88e349288484af0962a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:06:32 +0900 Subject: [PATCH 5/9] docs(adr): restore purpose-bound sensitive authority decision --- ...-purpose-bound-sensitive-data-authority.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 docs/adr/0007-purpose-bound-sensitive-data-authority.md diff --git a/docs/adr/0007-purpose-bound-sensitive-data-authority.md b/docs/adr/0007-purpose-bound-sensitive-data-authority.md new file mode 100644 index 000000000..acb499fac --- /dev/null +++ b/docs/adr/0007-purpose-bound-sensitive-data-authority.md @@ -0,0 +1,73 @@ +# ADR 0007: Purpose-bound sensitive-data authority + +- Status: Accepted for the first authority kernel +- Date: 2026-08-09 + +## Context + +Enterprise browser workflows sometimes require real personal or otherwise protected values. Blanket masking can make a legitimate shipment, form fill, customer contact, reconciliation, or incident workflow impossible. Ambient raw access has the opposite failure mode: values can propagate into models, logs, traces, exports, support channels, or unrelated services merely because a caller already has network or session access. + +OriginWeave therefore treats each sensitive-data disclosure as a separate resource-access decision. Network location, session ownership, repository membership, administrator status, or possession of a model credential does not grant disclosure authority. This follows the same explicit-authority architecture used for origins, resolved destinations, TCP peers, TLS identities, actions, and approvals. + +NIST SP 800-207A models application and service identities as policy inputs rather than relying on network location. The GDPR purpose-limitation and data-minimisation principles likewise require processing to remain tied to specified purposes and limited to what is necessary for them. OWASP logging guidance warns that personal and other sensitive information can become a secondary exposure through application logs. These sources motivate the architecture; this ADR does not itself establish legal compliance or certification. + +## Decision + +The first implementation lives inside `originweave-policy` as a bounded preparatory authority kernel while the repository's lockfile-governance constraint prevents safely adding the separately versioned `originweave-sensitive-data` workspace crate required by the complete issue. The final issue remains open until that standalone crate and service contract exist. + +The kernel carries authority metadata but never the protected value. A disclosure request and its explicit scope are bound to tenant identity, task identity, field identity, declared business purpose, canonical destination `Origin`, and data classification. + +Tenant, task, field, and purpose identifiers are policy tokens, not arbitrary display text. Each must be 1–128 bytes of ASCII alphanumeric characters plus `.`, `_`, `:`, or `-`. Missing, oversized, whitespace-bearing, control-bearing, Unicode, or otherwise malformed authority identifiers fail closed even when the request and scope contain the same invalid bytes. This bounds memory and serialization surface and prevents malformed identifiers from becoming authority merely through equality. The destination must already have crossed the canonical `Origin` parser boundary, so credentials, paths, malformed or ambiguous hosts, unsupported insecure remote schemes, Unicode/control input, and browser-special numeric-host spellings cannot be smuggled into the sensitive-data authority as arbitrary text. + +An exact match may return only the explicitly configured disclosure decision: deny, opaque handle only, derived value only, partial field disclosure, full field disclosure, human approval required, or dual control required. Any authority mismatch or invalid authority fails closed to denial. `HumanApprovalRequired` and `DualControlRequired` are not execution permissions: the caller must collect the required independent approval evidence and re-evaluate the exact same tenant, task, field, purpose, destination, and classification scope before any trusted broker, browser fill, export, or model-disclosure path can proceed. `DenyAccess` terminates the disclosure path. + +Opaque handle use is separately bound to tenant, task, field, purpose, canonical destination, data classification, exclusive expiry time, and maximum use count. A field reclassification therefore invalidates the prior handle authority even when every other identifier is unchanged; the caller must obtain a newly authorized handle for the new classification. `evaluate_handle_use` is intentionally a pure admission predicate: it compares authority, classification, trusted-time input, and broker-recorded prior-use count, but it does not own mutable handle state, resolve a handle, consume a use, or return the protected value. It must never be treated as standalone enforcement by an untrusted caller. + +The later trusted broker or browser adapter is the stateful enforcement boundary. Before resolving any protected value, it must obtain trusted time and authoritative, caller-unforgeable handle state; atomically compare the exact scope, classification, exclusive expiry, and current use count; and reserve or increment the use count in the same transaction that grants the use. Concurrent or replayed requests therefore compete for one authoritative count rather than reusing a stale caller-supplied count. Once a use has been successfully reserved, a downstream browser/action failure does not silently refund that use unless a separately specified compensating transaction is both safe and auditable. At the expiry boundary (`now >= expires_at`) no new reservation is permitted. Immediately before release, the broker rechecks that the reserved handle, requested scope, and classification still match and that revocation or lifecycle state has not invalidated the disclosure. + +The first kernel intentionally does not implement storage, encryption, tokenization, model disclosure, provider or region policy, retention, audit persistence, break-glass access, or a broker. Those remain separate authority and lifecycle boundaries rather than being inferred from this primitive. + +## Consequences + +- Raw protected bytes are structurally absent from the first policy API. +- A caller with the wrong tenant, task, field, purpose, destination, or classification cannot reuse another disclosure scope or opaque handle. +- A later field reclassification fails closed against an older handle instead of inheriting the old disclosure class. +- Missing, oversized, whitespace-bearing, control-bearing, Unicode, or otherwise malformed tenant, task, field, or purpose identifiers cannot become authority through equality with another invalid scope; destination validity is guaranteed by the canonical `Origin` boundary. +- A stale, reclassified, or exhausted opaque handle fails closed in the pure predicate, while the future broker must enforce classification, expiry, and use-count consumption atomically before value resolution. +- Approval-required disclosure outcomes cannot fall through directly to execution; the exact scope is re-evaluated after approval evidence is obtained. +- Later UI, connector, model, export, and browser-fill adapters can reuse the same explicit decision boundary without inheriting ambient authority. +- The complete enterprise gap is not closed by this kernel; independently reusable storage/broker/service contracts, evidence, lifecycle controls, and end-to-end tests are still required. + +## Rejected alternatives + +### Blanket masking + +Rejected because some authorized operational workflows require the real value and a permanent masked copy can diverge from the authoritative record. + +### Ambient trusted-network or session access + +Rejected because network or session membership is not a sufficient authorization decision and creates confused-deputy and propagation risk. + +### Sending every protected value through the model + +Rejected because many actions can operate through opaque handles or deterministic trusted adapters. Model disclosure must remain a separately governed exceptional path. + +### Classification-free opaque handles + +Rejected because a handle issued while a field is classified as ordinary personal data could otherwise be reused after the same field is reclassified as sensitive personal, credential, or payment data. Classification is an authority dimension, not mutable display metadata. + +### Caller-managed handle-use counters + +Rejected because two concurrent callers can present the same stale `uses_so_far` value and both appear admissible. The mutable count, trusted clock, revocation state, and compare-and-increment operation belong to the trusted broker's authoritative state boundary. + +## Verification + +Tests must prove exact-scope disclosure, canonical destination behavior, denial on every authority-dimension mismatch, fail-closed behavior for missing or malformed authority, acceptance at the exact 128-byte identifier bound, rejection beyond that bound, rejection of whitespace/control/Unicode identifiers, every supported disclosure result, opaque-handle classification mismatch, expiry, use-count exhaustion, and destination mismatch. The broker slice must add classification-change, concurrency, replay, expiry-boundary, post-reservation failure, revocation, and atomic compare-and-increment tests before any protected-value resolution is described as implemented. Production function, line, region, and branch coverage remains exactly 100%. + +## References + +Chandramouli, R., & Butcher, Z. (2023). *A zero trust architecture model for access control in cloud-native applications in multi-cloud environments* (NIST Special Publication 800-207A). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-207A + +European Parliament & Council of the European Union. (2016). *Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)*. *Official Journal of the European Union, L 119*, 1–88. https://eur-lex.europa.eu/eli/reg/2016/679/oj + +OWASP Foundation. (n.d.). *Logging cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 9, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html From 847fa750857bf9b0a58d8a93845d4ede1dc1caae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:09:43 +0900 Subject: [PATCH 6/9] docs(changelog): record purpose-bound sensitive authority --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba3fed6f..4c3e1ddd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. +- Purpose-bound sensitive-data policy that carries no protected value bytes, binds disclosure to exact tenant, task, field, purpose, canonical destination, and classification authority, constrains tenant/task/field/purpose identifiers to 1–128 byte ASCII policy tokens, preserves seven explicit disclosure outcomes, and evaluates opaque-handle admission against that same classification-bound destination scope plus exclusive expiry and maximum use count. - Session- and context-bound node authority with nonzero browser-session, browsing-context, and document-epoch identities, adapter-local node identifiers, exact canonical-origin binding, and deterministic cross-session, cross-context, cross-origin, and stale-epoch rejection before a future browser adapter acts on an observed node. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Explicit bounded proxy/PAC route authority in `originweave-destination`: direct-only by default, separately allow-listed Chromium-compatible proxy server identifiers and PAC source origins, independent authorization for PAC-selected DIRECT versus proxy routes, exact canonical target/proxy/PAC evidence, and no DNS, socket, PAC execution, CONNECT, authentication, or Chromium side effects. @@ -27,7 +28,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state. - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge. -- Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, TLS service-identity, session/context node-authority, and hourly agent credential-boundary ADR documentation. +- Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, TLS service-identity, session/context node-authority, purpose-bound sensitive-data, and hourly agent credential-boundary ADR documentation. ### Changed @@ -35,10 +36,11 @@ All notable changes to OriginWeave are documented in this file. The format follo - Separated proxy-server routing identity from web-origin identity: HTTP, HTTPS, SOCKS4, SOCKS5, and QUIC proxy schemes retain their own canonical authority, so an ordinary remote HTTP proxy is representable without weakening the web-origin HTTPS requirement. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. - Separated exact TCP peer proof from authenticated TLS service identity; an observed peer becomes an authenticated HTTPS stream only after explicit-root, fixed-time, SAN-bound WebPKI verification over that same stream. +- Separated sensitive-data disclosure from network, session, repository, administrator, and model-credential authority; later storage, broker, model-disclosure, evidence, and lifecycle modules must consume explicit field-level policy instead of inferring raw-value access. - Restricted direct TCP retries to an explicit transient operating-system error allow-list; deterministic permission, input, and local-address failures now stop after the first attempt while retaining the original error source. - Replaced single resource-pressure directives with a cumulative mitigation plan so simultaneous RAM, VRAM, frame, model, and admission pressure cannot discard required actions. - Changed generic network capture from finite deny-lists or safe-name allow-lists to unconditional value redaction. Typed metadata values and bodies now require a separate schema-specific capture contract. -- Updated the first Chromium slice to distinguish implemented origin, destination, direct TCP, and TLS identity kernels from the remaining trusted DNS adapter, proxy/PAC, HTTP budget, MIME, download, and Chromium integration required before safe navigation can be claimed. +- Updated the first Chromium slice to distinguish implemented origin, destination, direct TCP, TLS identity, node-authority, and sensitive-data policy kernels from the remaining trusted DNS adapter, proxy/PAC execution, HTTP budget, MIME, download, trusted sensitive-value broker, and Chromium integration required before safe navigation and protected-value disclosure can be claimed. - Separated hourly product PR publication authority from the organization review and merge system, added live default-branch and release-blocker rechecks immediately before publication, exhaustively paginate release-blocker results in bounded 100-item API pages before filtering pull-request entries so labeled PRs cannot mask a real blocking issue on any later page, and made a missing dedicated `OPENCODE_PR_TOKEN` fail closed after a verified change instead of producing a green publication no-op. - Moved autonomous-agent Cargo targets and Python bytecode caches outside the proposed source tree and prefetched locked Cargo dependencies for offline verification. - Split deterministic open-PR, release-blocker, and dry-run evaluation from the conditional NVIDIA credential step so stopped runs never receive `NVIDIA_NIM_API_KEY`; made a missing `NVIDIA_NIM_API_KEY` fail closed after deterministic governance selects the model-backed path instead of silently skipping all remaining work with a green result; replaced post-model raw-key rematerialization with a runner-only length, SHA-256, and rolling-hash fingerprint used solely for exact leak detection; bounded untrusted `PR_MESSAGE.md` before byte-wise leak scanning; stat-size-check model-controlled workspace files against the one-mebibyte per-file bound before any full byte comparison used to discover changed files; added an evidence-first RCA, feasibility, materially distinct corrective-action, and exact-command revalidation contract; reset every fallback model to the pristine source tree; classified model timeouts, model or tool failures, and credential-broker failures before retry; emitted bounded broker diagnostics when broker failure makes retry infeasible; made final cleanup use the privilege required for the UID-65532-owned model configuration; and expanded the job budget to 180 minutes so all three advertised 35-minute model attempts plus independent verification can actually execute without weakening fail-closed egress. @@ -49,6 +51,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. +- Sensitive-data policy denies disclosure when tenant, task, field, business purpose, canonical destination, or classification differs from the explicit scope; malformed, oversized, whitespace-bearing, control-bearing, or non-ASCII authority identifiers also fail closed even when both sides match. Opaque-handle evaluation fails closed on the same bounded destination and classification scope, including field reclassification, expiry, or exhausted use count, and the first authority API contains no protected value bytes. Later broker/service adapters must consume this same scope and atomically enforce authoritative use state before value resolution. - Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision. - State-changing actions are same-origin by default. - R3 and R4 approvals are bound to the exact action, target origin, and immutable digest of the complete canonical action intent; R5 legal consent is non-delegable. From e0753b0caa15741c31244109f595e8e9c0049ca0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:10:02 +0900 Subject: [PATCH 7/9] docs(doctoring): trace sensitive-data authority evidence --- docs/doctoring/sensitive-data-authority.md | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/doctoring/sensitive-data-authority.md diff --git a/docs/doctoring/sensitive-data-authority.md b/docs/doctoring/sensitive-data-authority.md new file mode 100644 index 000000000..a3c05b64a --- /dev/null +++ b/docs/doctoring/sensitive-data-authority.md @@ -0,0 +1,26 @@ +# Purpose-bound sensitive-data authority doctoring + +- **Status:** Implemented policy-kernel evidence; broker/storage/lifecycle remain planned under issue #10. +- **Decision:** [`../adr/0007-purpose-bound-sensitive-data-authority.md`](../adr/0007-purpose-bound-sensitive-data-authority.md) + +## Evidence boundary + +OriginWeave treats protected-value disclosure as explicit resource authorization rather than as a consequence of network location, session ownership, administrator status, repository membership, or possession of a model credential. The first Rust policy kernel carries only authority metadata and binds disclosure/opaque-handle admission to tenant, task, field, business purpose, canonical destination origin, and data classification. + +This evidence supports the architectural direction; it does **not** prove legal compliance, CSAP certification, SOC 2 conformity, or that the planned trusted broker/storage lifecycle is implemented. + +## Primary and authoritative references — APA 7th + +Chandramouli, R., & Butcher, Z. (2023). *A zero trust architecture model for access control in cloud-native applications in multi-cloud environments* (NIST Special Publication 800-207A). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-207A + +European Parliament & Council of the European Union. (2016). *Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)*. *Official Journal of the European Union, L 119*, 1–88. https://eur-lex.europa.eu/eli/reg/2016/679/oj + +OWASP Foundation. (n.d.). *Logging cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 9, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html + +Rose, S., Borchert, O., Mitchell, S., & Connelly, S. (2020). *Zero trust architecture* (NIST Special Publication 800-207). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-207 + +## Test traceability + +- `crates/originweave-policy/tests/sensitive_data_policy.rs` — exact authority dimensions, malformed identifier fail-closed behavior, canonical origin binding, disclosure outcomes, expiry/use-count boundaries. +- `crates/originweave-policy/tests/handle_classification.rs` — field reclassification invalidates prior opaque-handle authority. +- The later broker slice must add atomic compare-and-increment, replay, concurrent-use, trusted-time, revocation, and post-reservation failure tests before value resolution is described as implemented. From 4480f1729eb967d097a2e5c32e28bd25949c8ec6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:11:04 +0900 Subject: [PATCH 8/9] docs(policy): restore complete sensitive authority rustdoc --- .../originweave-policy/src/sensitive_data.rs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs index 0e38f19ac..ffb7130d7 100644 --- a/crates/originweave-policy/src/sensitive_data.rs +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -45,6 +45,11 @@ pub enum DisclosureDecision { } /// Exact authority metadata for one classified sensitive-data field use. +/// +/// The value contains no protected field bytes. It combines the tenant, task, +/// field, business purpose, canonical destination, and data classification so +/// disclosure, opaque-handle issuance, and opaque-handle use cannot silently +/// diverge on one of those authority dimensions. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SensitiveDataAuthority { tenant_id: String, @@ -57,6 +62,11 @@ pub struct SensitiveDataAuthority { impl SensitiveDataAuthority { /// Build one exact classified authority value without carrying protected data. + /// + /// Tenant, task, field, and purpose identifiers are admitted only as 1–128 + /// byte ASCII policy tokens using alphanumeric characters plus `.`, `_`, `:`, + /// and `-`. Each token must contain at least one alphanumeric character. + /// Invalid identifiers remain fail-closed when the authority is used. #[must_use] pub fn new( tenant_id: &str, @@ -118,11 +128,17 @@ impl DisclosureScope { /// Build an exact disclosure authority scope and its maximum permitted outcome. #[must_use] pub const fn new(authority: SensitiveDataAuthority, decision: DisclosureDecision) -> Self { - Self { authority, decision } + Self { + authority, + decision, + } } } /// Evaluate disclosure only from the exact request and explicit authority scope. +/// +/// An incomplete or malformed authority fails closed even when both sides contain +/// the same invalid identifier. #[must_use] pub fn evaluate_disclosure( request: &SensitiveDataRequest, @@ -161,6 +177,9 @@ pub struct SensitiveValueHandleScope { impl SensitiveValueHandleScope { /// Build an opaque-handle scope with exact authority, exclusive expiry, and bounded use count. + /// + /// A later field reclassification creates a different [`SensitiveDataAuthority`] + /// and therefore requires a newly authorized handle. #[must_use] pub const fn new( authority: SensitiveDataAuthority, @@ -185,6 +204,10 @@ pub struct HandleUseRequest { impl HandleUseRequest { /// Build a handle-use evaluation request from trusted time and authoritative broker state. + /// + /// The eventual broker must supply these state values from its own trusted, + /// caller-unforgeable storage; accepting this struct does not make arbitrary + /// caller input authoritative. #[must_use] pub const fn new( authority: SensitiveDataAuthority, @@ -200,6 +223,15 @@ impl HandleUseRequest { } /// Evaluate whether authoritative broker state is admissible for one handle use. +/// +/// This pure function does not consume a use, mutate broker state, resolve a +/// handle, or release a protected value. It is therefore not standalone +/// enforcement. A trusted broker must obtain trusted time and caller-unforgeable +/// handle state, atomically reserve or increment the use count before value +/// resolution, and recheck the reserved authority immediately before disclosure. +/// Missing or malformed authority identifiers fail closed as a scope mismatch. +/// The authority destination must already have crossed the canonical [`Origin`] +/// boundary. #[must_use] pub fn evaluate_handle_use( request: &HandleUseRequest, From 476abb02446f7c79ef8e237d03363025281b0aee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:11:55 +0900 Subject: [PATCH 9/9] style(policy): restore canonical sensitive-data regression formatting --- .../tests/sensitive_data_policy.rs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_data_policy.rs b/crates/originweave-policy/tests/sensitive_data_policy.rs index 60e3f81fa..467fb8973 100644 --- a/crates/originweave-policy/tests/sensitive_data_policy.rs +++ b/crates/originweave-policy/tests/sensitive_data_policy.rs @@ -29,7 +29,13 @@ fn authority_case<'a>( purpose: &'a str, destination: &'a str, ) -> AuthorityCase<'a> { - AuthorityCase { tenant, task, field, purpose, destination } + AuthorityCase { + tenant, + task, + field, + purpose, + destination, + } } fn exact_authority() -> AuthorityCase<'static> { @@ -160,7 +166,13 @@ fn sensitive_destination_uses_the_canonical_origin_boundary() { ); assert_disclosure_denied( - authority_case(TENANT, TASK, FIELD, PURPOSE, "https://shipping.example:8443"), + authority_case( + TENANT, + TASK, + FIELD, + PURPOSE, + "https://shipping.example:8443", + ), DataClassification::PersonalData, ); @@ -172,7 +184,10 @@ fn sensitive_destination_uses_the_canonical_origin_boundary() { "https://127.1", "http://shipping.example", ] { - assert!(Origin::parse(invalid).is_err(), "unexpected origin: {invalid}"); + assert!( + Origin::parse(invalid).is_err(), + "unexpected origin: {invalid}" + ); } assert!(Origin::parse("http://127.0.0.1").is_ok()); }