Skip to content
Merged
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -27,18 +28,19 @@ 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

- Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result.
- 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.
Expand All @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions crates/originweave-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
252 changes: 252 additions & 0 deletions crates/originweave-policy/src/sensitive_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
//! 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.
///
/// 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,
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.
///
/// 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,
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.
///
/// An incomplete or malformed authority fails closed even when both sides contain
/// the same invalid identifier.
#[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.
///
/// A later field reclassification creates a different [`SensitiveDataAuthority`]
/// and therefore requires a newly authorized handle.
#[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.
///
/// 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,
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.
///
/// 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,
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
}
}
43 changes: 43 additions & 0 deletions crates/originweave-policy/tests/handle_classification.rs
Original file line number Diff line number Diff line change
@@ -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
);
}
Loading
Loading