diff --git a/CHANGELOG.md b/CHANGELOG.md index 1093841d4..8e84819b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Time-bounded managed extension admission for isolated Agent Task profiles with an explicit half-open validity window and caller-supplied trusted evaluation time; invalid, not-yet-valid, and expired policy states fail closed before exact extension allow-list membership is considered. - Fail-closed managed extension admission for isolated Agent Task profiles: an empty policy admits no extension, only exact canonical `ExtensionId` allow-list membership is accepted, duplicate entries cannot widen authority, and successful profile admission remains separate from `ExtensionAgentGrant` capability. - Explicit reduced-assurance classification for attached human tabs when trusted adapter evidence says an existing extension can influence page state; the narrow rule does not detect extensions, prove extension absence, grant Agent authority, or turn an unclassified context into high-assurance evidence. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 3dda94b02..2d2d8f687 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -26,6 +26,9 @@ use originweave_core::{ #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentTaskExtensionPolicy { managed_extensions: BTreeSet, + valid_from: u64, + valid_until: u64, + maximum_window: u64, } impl AgentTaskExtensionPolicy { @@ -33,13 +36,28 @@ impl AgentTaskExtensionPolicy { /// /// Duplicate identifiers collapse to one exact managed identity. An empty /// iterator therefore represents the default policy that admits no extension. + /// `valid_from` is inclusive and `valid_until` is exclusive. `maximum_window` + /// is the reviewed local ceiling for that interval. All three values are opaque + /// timestamps or durations in the same caller-defined trusted time domain and + /// units supplied to [`evaluate_agent_task_extension`]. This constructor does + /// not authenticate policy provenance or attest a clock; an invalid, empty, or + /// overlong validity window is retained so evaluation can fail closed + /// deterministically. #[must_use] - pub fn new(managed_extensions: I) -> Self + pub fn new( + managed_extensions: I, + valid_from: u64, + valid_until: u64, + maximum_window: u64, + ) -> Self where I: IntoIterator, { Self { managed_extensions: managed_extensions.into_iter().collect(), + valid_from, + valid_until, + maximum_window, } } } @@ -51,19 +69,45 @@ pub enum AgentTaskExtensionDecision { AllowManagedExtension, /// The extension identity is absent from the managed allow-list. DenyNotManaged, + /// The configured validity window is empty/reversed or its local ceiling is zero. + DenyInvalidPolicyWindow, + /// The configured policy validity interval exceeds the reviewed local maximum. + DenyPolicyWindowExceedsMaximum, + /// The trusted evaluation time precedes the policy validity window. + DenyPolicyNotYetValid, + /// The trusted evaluation time is at or beyond the policy expiry boundary. + DenyPolicyExpired, } /// Evaluate extension admission without minting OriginWeave Agent capability. /// /// This pure boundary answers only whether the exact canonical extension may be -/// present in the caller's managed Agent Task profile. Chromium permissions, -/// installation state, native messaging, and [`originweave_core::ExtensionAgentGrant`] -/// remain separate authorities. +/// present in the caller's managed Agent Task profile at `trusted_time`. +/// `trusted_time`, [`AgentTaskExtensionPolicy::new`] `valid_from`, `valid_until`, +/// and `maximum_window` must use one caller-attested time domain and compatible +/// units; this function does not read or attest a clock. The validity window is +/// half-open (`valid_from <= trusted_time < valid_until`) and must not exceed the +/// reviewed local maximum. Chromium permissions, installation state, native +/// messaging, and [`originweave_core::ExtensionAgentGrant`] remain separate +/// authorities. #[must_use] pub fn evaluate_agent_task_extension( extension_id: &ExtensionId, policy: &AgentTaskExtensionPolicy, + trusted_time: u64, ) -> AgentTaskExtensionDecision { + if policy.valid_from >= policy.valid_until || policy.maximum_window == 0 { + return AgentTaskExtensionDecision::DenyInvalidPolicyWindow; + } + if policy.valid_until - policy.valid_from > policy.maximum_window { + return AgentTaskExtensionDecision::DenyPolicyWindowExceedsMaximum; + } + if trusted_time < policy.valid_from { + return AgentTaskExtensionDecision::DenyPolicyNotYetValid; + } + if trusted_time >= policy.valid_until { + return AgentTaskExtensionDecision::DenyPolicyExpired; + } if policy.managed_extensions.contains(extension_id) { AgentTaskExtensionDecision::AllowManagedExtension } else { diff --git a/crates/originweave-policy/tests/agent_task_extension_policy.rs b/crates/originweave-policy/tests/agent_task_extension_policy.rs index 3549560ec..fae16ade5 100644 --- a/crates/originweave-policy/tests/agent_task_extension_policy.rs +++ b/crates/originweave-policy/tests/agent_task_extension_policy.rs @@ -26,11 +26,11 @@ fn origin() -> Origin { #[test] fn empty_agent_task_extension_policy_denies_every_extension() { - let policy = AgentTaskExtensionPolicy::new([]); + let policy = AgentTaskExtensionPolicy::new([], 10, 20, 10); let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); assert_eq!( - evaluate_agent_task_extension(&extension, &policy), + evaluate_agent_task_extension(&extension, &policy, 15), AgentTaskExtensionDecision::DenyNotManaged ); } @@ -39,25 +39,96 @@ fn empty_agent_task_extension_policy_denies_every_extension() { fn managed_agent_task_extension_policy_allows_only_exact_identifiers() { let allowed = extension_id("abcdefghijklmnopabcdefghijklmnop"); let other = extension_id("bcdefghijklmnopabcdefghijklmnopa"); - let policy = AgentTaskExtensionPolicy::new([allowed.clone(), allowed.clone()]); + let policy = AgentTaskExtensionPolicy::new([allowed.clone(), allowed.clone()], 10, 20, 10); assert_eq!( - evaluate_agent_task_extension(&allowed, &policy), + evaluate_agent_task_extension(&allowed, &policy, 10), AgentTaskExtensionDecision::AllowManagedExtension ); assert_eq!( - evaluate_agent_task_extension(&other, &policy), + evaluate_agent_task_extension(&other, &policy, 19), AgentTaskExtensionDecision::DenyNotManaged ); } +#[test] +fn managed_agent_task_extension_policy_fails_closed_outside_its_validity_window() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10); + + assert_eq!( + evaluate_agent_task_extension(&extension, &policy, 9), + AgentTaskExtensionDecision::DenyPolicyNotYetValid + ); + assert_eq!( + evaluate_agent_task_extension(&extension, &policy, 20), + AgentTaskExtensionDecision::DenyPolicyExpired + ); + assert_eq!( + evaluate_agent_task_extension(&extension, &policy, u64::MAX), + AgentTaskExtensionDecision::DenyPolicyExpired + ); +} + +#[test] +fn invalid_managed_extension_policy_window_fails_closed_before_membership() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let reversed = AgentTaskExtensionPolicy::new([extension.clone()], 20, 10, 10); + let empty = AgentTaskExtensionPolicy::new([extension.clone()], 20, 20, 10); + let zero_maximum = AgentTaskExtensionPolicy::new([extension.clone()], 20, 21, 0); + + assert_eq!( + evaluate_agent_task_extension(&extension, &reversed, 15), + AgentTaskExtensionDecision::DenyInvalidPolicyWindow + ); + assert_eq!( + evaluate_agent_task_extension(&extension, &empty, 20), + AgentTaskExtensionDecision::DenyInvalidPolicyWindow + ); + assert_eq!( + evaluate_agent_task_extension(&extension, &zero_maximum, 20), + AgentTaskExtensionDecision::DenyInvalidPolicyWindow + ); +} + +#[test] +fn managed_extension_policy_window_cannot_exceed_local_maximum() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let exact = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10); + let overlong = AgentTaskExtensionPolicy::new([extension.clone()], 10, 21, 10); + + assert_eq!( + evaluate_agent_task_extension(&extension, &exact, 19), + AgentTaskExtensionDecision::AllowManagedExtension + ); + assert_eq!( + evaluate_agent_task_extension(&extension, &overlong, 19), + AgentTaskExtensionDecision::DenyPolicyWindowExceedsMaximum + ); +} + +#[test] +fn maximum_timestamp_window_remains_half_open_without_overflow() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let policy = AgentTaskExtensionPolicy::new([extension.clone()], u64::MAX - 1, u64::MAX, 1); + + assert_eq!( + evaluate_agent_task_extension(&extension, &policy, u64::MAX - 1), + AgentTaskExtensionDecision::AllowManagedExtension + ); + assert_eq!( + evaluate_agent_task_extension(&extension, &policy, u64::MAX), + AgentTaskExtensionDecision::DenyPolicyExpired + ); +} + #[test] fn managed_agent_task_extension_admission_does_not_mint_agent_capability() { let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); - let policy = AgentTaskExtensionPolicy::new([extension.clone()]); + let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10); assert_eq!( - evaluate_agent_task_extension(&extension, &policy), + evaluate_agent_task_extension(&extension, &policy, 15), AgentTaskExtensionDecision::AllowManagedExtension );