diff --git a/CHANGELOG.md b/CHANGELOG.md index d89bd36..47bbe8b 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 +- Session-bound managed extension admission for isolated Agent Task profiles: policy is tied to one exact OriginWeave `BrowserSessionId`, mismatched sessions fail closed before allow-list membership is considered, and the boundary remains distinct from Chromium profile attestation and Agent capability grants. - 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. diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 53d8911..b385048 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, @@ -33,19 +35,28 @@ 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. Both values are - /// opaque timestamps in the same caller-defined trusted time domain that will - /// be supplied to [`evaluate_agent_task_extension`]. This constructor does not - /// authenticate policy provenance or attest a clock; an invalid or empty - /// 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. Both values are opaque timestamps + /// in the same caller-defined trusted time domain supplied to + /// [`evaluate_agent_task_extension`]. This constructor does not authenticate + /// policy provenance or attest a clock; an invalid or empty validity window is + /// retained so evaluation can fail closed deterministically. #[must_use] - pub fn new(managed_extensions: I, valid_from: u64, valid_until: u64) -> Self + pub fn new( + browser_session: BrowserSessionId, + managed_extensions: I, + valid_from: u64, + valid_until: u64, + ) -> Self where I: IntoIterator, { Self { + browser_session, managed_extensions: managed_extensions.into_iter().collect(), valid_from, valid_until, @@ -66,21 +77,27 @@ 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`, and `valid_until` /// must use the same caller-attested time domain; this function does not read or /// attest a clock. The validity window is half-open: `valid_from <= trusted_time < -/// valid_until`. Chromium permissions, installation state, native messaging, and +/// valid_until`. The current session is checked before allow-list membership so a +/// policy cannot be replayed across OriginWeave browser sessions or used there as +/// an 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.valid_from >= policy.valid_until { @@ -92,6 +109,9 @@ pub fn evaluate_agent_task_extension( if trusted_time >= policy.valid_until { return AgentTaskExtensionDecision::DenyPolicyExpired; } + if policy.browser_session != current_session { + return AgentTaskExtensionDecision::DenySessionMismatch; + } 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 f82a624..acee680 100644 --- a/crates/originweave-policy/tests/agent_task_extension_policy.rs +++ b/crates/originweave-policy/tests/agent_task_extension_policy.rs @@ -22,11 +22,11 @@ fn context(value: u64) -> BrowsingContextId { #[test] fn empty_agent_task_extension_policy_denies_every_extension() { - let policy = AgentTaskExtensionPolicy::new([], 10, 20); + let policy = AgentTaskExtensionPolicy::new(session(31), [], 10, 20); 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 ); } @@ -35,33 +35,54 @@ 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); + let policy = + AgentTaskExtensionPolicy::new(session(31), [allowed.clone(), allowed.clone()], 10, 20); 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); + + 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 managed_agent_task_extension_policy_fails_closed_outside_its_validity_window() { let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); - let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20); + let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20); 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 ); } @@ -69,15 +90,15 @@ 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); - let empty = AgentTaskExtensionPolicy::new([extension.clone()], 20, 20); + let reversed = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 10); + let empty = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 20); 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 ); } @@ -85,14 +106,15 @@ fn invalid_managed_extension_policy_window_fails_closed_before_membership() { #[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); + let policy = + AgentTaskExtensionPolicy::new(session(31), [extension.clone()], u64::MAX - 1, u64::MAX); 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 ); } @@ -100,10 +122,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); + let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20); assert_eq!( - evaluate_agent_task_extension(&extension, &policy, 15), + evaluate_agent_task_extension(&extension, &policy, session(31), 15), AgentTaskExtensionDecision::AllowManagedExtension );