diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 2d2d8f687..8ac0ae50f 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -18,13 +18,15 @@ pub use sensitive_data::{ use std::collections::BTreeSet; use originweave_core::{ - ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, ExtensionId, - InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode, + ActionRequest, ApprovalEvidence, ApprovalScope, BrowserSessionId, Capability, ExecutionPurpose, + ExtensionId, InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, + SessionMode, }; /// Exact extension identities that may be present in one managed Agent Task profile. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentTaskExtensionPolicy { + browser_session: BrowserSessionId, managed_extensions: BTreeSet, valid_from: u64, valid_until: u64, @@ -34,17 +36,20 @@ pub struct AgentTaskExtensionPolicy { impl AgentTaskExtensionPolicy { /// Build one fail-closed Agent Task extension admission policy. /// - /// 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. + /// `browser_session` binds this policy to one OriginWeave browser-session + /// authority. It does not prove which Chromium profile is attached to that + /// session or authenticate enterprise-policy provenance. 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. The time values and duration use one + /// caller-defined trusted time domain and compatible units supplied to + /// [`evaluate_agent_task_extension`]. This constructor does not authenticate + /// policy provenance or attest a clock; invalid, empty, or overlong validity + /// windows are retained so evaluation can fail closed deterministically. #[must_use] pub fn new( + browser_session: BrowserSessionId, managed_extensions: I, valid_from: u64, valid_until: u64, @@ -54,6 +59,7 @@ impl AgentTaskExtensionPolicy { I: IntoIterator, { Self { + browser_session, managed_extensions: managed_extensions.into_iter().collect(), valid_from, valid_until, @@ -77,25 +83,34 @@ pub enum AgentTaskExtensionDecision { DenyPolicyNotYetValid, /// The trusted evaluation time is at or beyond the policy expiry boundary. DenyPolicyExpired, + /// The current OriginWeave browser session differs from the policy-bound session. + DenySessionMismatch, } /// 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 at `trusted_time`. +/// present in the policy-bound Agent Task session 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. +/// reviewed local maximum. The current session is checked before policy-window +/// or allow-list evaluation so a policy cannot be replayed across OriginWeave +/// browser sessions or used there as a policy-state or extension-membership +/// oracle. This does not attest Chromium profile identity. 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, + current_session: BrowserSessionId, trusted_time: u64, ) -> AgentTaskExtensionDecision { + if policy.browser_session != current_session { + return AgentTaskExtensionDecision::DenySessionMismatch; + } if policy.valid_from >= policy.valid_until || policy.maximum_window == 0 { return AgentTaskExtensionDecision::DenyInvalidPolicyWindow; } diff --git a/crates/originweave-policy/tests/agent_task_extension_policy.rs b/crates/originweave-policy/tests/agent_task_extension_policy.rs index fae16ade5..429dde811 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([], 10, 20, 10); + let policy = AgentTaskExtensionPolicy::new(session(31), [], 10, 20, 10); let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); assert_eq!( - evaluate_agent_task_extension(&extension, &policy, 15), + evaluate_agent_task_extension(&extension, &policy, session(31), 15), AgentTaskExtensionDecision::DenyNotManaged ); } @@ -39,33 +39,84 @@ 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()], 10, 20, 10); + let policy = + AgentTaskExtensionPolicy::new(session(31), [allowed.clone(), allowed.clone()], 10, 20, 10); assert_eq!( - evaluate_agent_task_extension(&allowed, &policy, 10), + evaluate_agent_task_extension(&allowed, &policy, session(31), 10), AgentTaskExtensionDecision::AllowManagedExtension ); assert_eq!( - evaluate_agent_task_extension(&other, &policy, 19), + evaluate_agent_task_extension(&other, &policy, session(31), 19), AgentTaskExtensionDecision::DenyNotManaged ); } +#[test] +fn managed_agent_task_extension_policy_is_not_reusable_across_sessions() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10); + + assert_eq!( + evaluate_agent_task_extension(&extension, &policy, session(37), 15), + AgentTaskExtensionDecision::DenySessionMismatch + ); + assert_eq!( + evaluate_agent_task_extension( + &extension_id("bcdefghijklmnopabcdefghijklmnopa"), + &policy, + session(37), + 15, + ), + AgentTaskExtensionDecision::DenySessionMismatch + ); +} + +#[test] +fn mismatched_session_cannot_probe_policy_window_state() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let current_session = session(37); + + let invalid = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 10, 10); + assert_eq!( + evaluate_agent_task_extension(&extension, &invalid, current_session, 15), + AgentTaskExtensionDecision::DenySessionMismatch + ); + + let overlong = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 21, 10); + assert_eq!( + evaluate_agent_task_extension(&extension, &overlong, current_session, 15), + AgentTaskExtensionDecision::DenySessionMismatch + ); + + let not_yet_valid = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10); + assert_eq!( + evaluate_agent_task_extension(&extension, ¬_yet_valid, current_session, 9), + AgentTaskExtensionDecision::DenySessionMismatch + ); + + let expired = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10); + assert_eq!( + evaluate_agent_task_extension(&extension, &expired, current_session, 20), + AgentTaskExtensionDecision::DenySessionMismatch + ); +} + #[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); + let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10); assert_eq!( - evaluate_agent_task_extension(&extension, &policy, 9), + evaluate_agent_task_extension(&extension, &policy, session(31), 9), AgentTaskExtensionDecision::DenyPolicyNotYetValid ); assert_eq!( - evaluate_agent_task_extension(&extension, &policy, 20), + evaluate_agent_task_extension(&extension, &policy, session(31), 20), AgentTaskExtensionDecision::DenyPolicyExpired ); assert_eq!( - evaluate_agent_task_extension(&extension, &policy, u64::MAX), + evaluate_agent_task_extension(&extension, &policy, session(31), u64::MAX), AgentTaskExtensionDecision::DenyPolicyExpired ); } @@ -73,20 +124,20 @@ fn managed_agent_task_extension_policy_fails_closed_outside_its_validity_window( #[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); + let reversed = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 10, 10); + let empty = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 20, 10); + let zero_maximum = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 21, 0); assert_eq!( - evaluate_agent_task_extension(&extension, &reversed, 15), + evaluate_agent_task_extension(&extension, &reversed, session(31), 15), AgentTaskExtensionDecision::DenyInvalidPolicyWindow ); assert_eq!( - evaluate_agent_task_extension(&extension, &empty, 20), + evaluate_agent_task_extension(&extension, &empty, session(31), 20), AgentTaskExtensionDecision::DenyInvalidPolicyWindow ); assert_eq!( - evaluate_agent_task_extension(&extension, &zero_maximum, 20), + evaluate_agent_task_extension(&extension, &zero_maximum, session(31), 20), AgentTaskExtensionDecision::DenyInvalidPolicyWindow ); } @@ -94,15 +145,15 @@ fn invalid_managed_extension_policy_window_fails_closed_before_membership() { #[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); + let exact = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10); + let overlong = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 21, 10); assert_eq!( - evaluate_agent_task_extension(&extension, &exact, 19), + evaluate_agent_task_extension(&extension, &exact, session(31), 19), AgentTaskExtensionDecision::AllowManagedExtension ); assert_eq!( - evaluate_agent_task_extension(&extension, &overlong, 19), + evaluate_agent_task_extension(&extension, &overlong, session(31), 19), AgentTaskExtensionDecision::DenyPolicyWindowExceedsMaximum ); } @@ -110,14 +161,15 @@ fn managed_extension_policy_window_cannot_exceed_local_maximum() { #[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); + let policy = + AgentTaskExtensionPolicy::new(session(31), [extension.clone()], u64::MAX - 1, u64::MAX, 1); assert_eq!( - evaluate_agent_task_extension(&extension, &policy, u64::MAX - 1), + evaluate_agent_task_extension(&extension, &policy, session(31), u64::MAX - 1), AgentTaskExtensionDecision::AllowManagedExtension ); assert_eq!( - evaluate_agent_task_extension(&extension, &policy, u64::MAX), + evaluate_agent_task_extension(&extension, &policy, session(31), u64::MAX), AgentTaskExtensionDecision::DenyPolicyExpired ); } @@ -125,10 +177,10 @@ fn maximum_timestamp_window_remains_half_open_without_overflow() { #[test] fn managed_agent_task_extension_admission_does_not_mint_agent_capability() { let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); - let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10); + let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10); assert_eq!( - evaluate_agent_task_extension(&extension, &policy, 15), + evaluate_agent_task_extension(&extension, &policy, session(31), 15), AgentTaskExtensionDecision::AllowManagedExtension );