From 23f128fc84b2d2b5b4c188211575ef69c5c5d070 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 21:21:29 +0900 Subject: [PATCH 01/16] test(extension): require managed Agent Task allow-list --- .../tests/agent_task_extension_policy.rs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 crates/originweave-core/tests/agent_task_extension_policy.rs diff --git a/crates/originweave-core/tests/agent_task_extension_policy.rs b/crates/originweave-core/tests/agent_task_extension_policy.rs new file mode 100644 index 000000000..5dd5ed32b --- /dev/null +++ b/crates/originweave-core/tests/agent_task_extension_policy.rs @@ -0,0 +1,68 @@ +#![allow(clippy::expect_used)] + +use originweave_core::{ + AgentTaskExtensionDecision, AgentTaskExtensionPolicy, BrowserSessionId, BrowsingContextId, + ExtensionAccessDecision, ExtensionAccessRequest, ExtensionAgentCapability, ExtensionId, + evaluate_agent_task_extension, evaluate_extension_access, +}; + +fn extension_id(value: &str) -> ExtensionId { + ExtensionId::parse(value).expect("valid extension identifier") +} + +fn session(value: u64) -> BrowserSessionId { + BrowserSessionId::new(value).expect("nonzero browser session") +} + +fn context(value: u64) -> BrowsingContextId { + BrowsingContextId::new(value).expect("nonzero browsing context") +} + +#[test] +fn empty_agent_task_extension_policy_denies_every_extension() { + let policy = AgentTaskExtensionPolicy::new([]); + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + + assert_eq!( + evaluate_agent_task_extension(&extension, &policy), + AgentTaskExtensionDecision::DenyNotManaged + ); +} + +#[test] +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()]); + + assert_eq!( + evaluate_agent_task_extension(&allowed, &policy), + AgentTaskExtensionDecision::AllowManagedExtension + ); + assert_eq!( + evaluate_agent_task_extension(&other, &policy), + AgentTaskExtensionDecision::DenyNotManaged + ); +} + +#[test] +fn managed_agent_task_extension_admission_does_not_mint_agent_capability() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let policy = AgentTaskExtensionPolicy::new([extension.clone()]); + + assert_eq!( + evaluate_agent_task_extension(&extension, &policy), + AgentTaskExtensionDecision::AllowManagedExtension + ); + + let request = ExtensionAccessRequest::new( + extension, + session(31), + context(37), + ExtensionAgentCapability::ProposeTypedAction, + ); + assert_eq!( + evaluate_extension_access(&request, None), + ExtensionAccessDecision::DenyMissingGrant + ); +} From 7b1da43cfa3f859e22fd27d8b03be956208223e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 21:40:33 +0900 Subject: [PATCH 02/16] test(extension): place managed admission at policy boundary --- .../tests/agent_task_extension_policy.rs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 crates/originweave-policy/tests/agent_task_extension_policy.rs diff --git a/crates/originweave-policy/tests/agent_task_extension_policy.rs b/crates/originweave-policy/tests/agent_task_extension_policy.rs new file mode 100644 index 000000000..8ea2f0de5 --- /dev/null +++ b/crates/originweave-policy/tests/agent_task_extension_policy.rs @@ -0,0 +1,70 @@ +#![allow(clippy::expect_used)] + +use originweave_core::{ + BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest, + ExtensionAgentCapability, ExtensionId, evaluate_extension_access, +}; +use originweave_policy::{ + AgentTaskExtensionDecision, AgentTaskExtensionPolicy, evaluate_agent_task_extension, +}; + +fn extension_id(value: &str) -> ExtensionId { + ExtensionId::parse(value).expect("valid extension identifier") +} + +fn session(value: u64) -> BrowserSessionId { + BrowserSessionId::new(value).expect("nonzero browser session") +} + +fn context(value: u64) -> BrowsingContextId { + BrowsingContextId::new(value).expect("nonzero browsing context") +} + +#[test] +fn empty_agent_task_extension_policy_denies_every_extension() { + let policy = AgentTaskExtensionPolicy::new([]); + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + + assert_eq!( + evaluate_agent_task_extension(&extension, &policy), + AgentTaskExtensionDecision::DenyNotManaged + ); +} + +#[test] +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()]); + + assert_eq!( + evaluate_agent_task_extension(&allowed, &policy), + AgentTaskExtensionDecision::AllowManagedExtension + ); + assert_eq!( + evaluate_agent_task_extension(&other, &policy), + AgentTaskExtensionDecision::DenyNotManaged + ); +} + +#[test] +fn managed_agent_task_extension_admission_does_not_mint_agent_capability() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let policy = AgentTaskExtensionPolicy::new([extension.clone()]); + + assert_eq!( + evaluate_agent_task_extension(&extension, &policy), + AgentTaskExtensionDecision::AllowManagedExtension + ); + + let request = ExtensionAccessRequest::new( + extension, + session(31), + context(37), + ExtensionAgentCapability::ProposeTypedAction, + ); + assert_eq!( + evaluate_extension_access(&request, None), + ExtensionAccessDecision::DenyMissingGrant + ); +} From e476f595f4266c09c4ccd54da4f2547a22fdf7cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 21:41:16 +0900 Subject: [PATCH 03/16] feat(extension): enforce managed Agent Task extension admission --- crates/originweave-policy/src/lib.rs | 53 +++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 243ae8ce7..3dda94b02 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -15,11 +15,62 @@ pub use sensitive_data::{ evaluate_handle_use, }; +use std::collections::BTreeSet; + use originweave_core::{ - ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, + ActionRequest, ApprovalEvidence, ApprovalScope, 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 { + managed_extensions: BTreeSet, +} + +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. + #[must_use] + pub fn new(managed_extensions: I) -> Self + where + I: IntoIterator, + { + Self { + managed_extensions: managed_extensions.into_iter().collect(), + } + } +} + +/// Result of evaluating one extension identity for an isolated Agent Task profile. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AgentTaskExtensionDecision { + /// The exact canonical extension identity appears in the managed allow-list. + AllowManagedExtension, + /// The extension identity is absent from the managed allow-list. + DenyNotManaged, +} + +/// 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. +#[must_use] +pub fn evaluate_agent_task_extension( + extension_id: &ExtensionId, + policy: &AgentTaskExtensionPolicy, +) -> AgentTaskExtensionDecision { + if policy.managed_extensions.contains(extension_id) { + AgentTaskExtensionDecision::AllowManagedExtension + } else { + AgentTaskExtensionDecision::DenyNotManaged + } +} + /// The result of evaluating one typed action request. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Decision { From fb33d211d701de96b1f94daebd7c9bbadd642240 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 21:41:35 +0900 Subject: [PATCH 04/16] test(extension): remove misplaced core admission test --- .../tests/agent_task_extension_policy.rs | 68 ------------------- 1 file changed, 68 deletions(-) delete mode 100644 crates/originweave-core/tests/agent_task_extension_policy.rs diff --git a/crates/originweave-core/tests/agent_task_extension_policy.rs b/crates/originweave-core/tests/agent_task_extension_policy.rs deleted file mode 100644 index 5dd5ed32b..000000000 --- a/crates/originweave-core/tests/agent_task_extension_policy.rs +++ /dev/null @@ -1,68 +0,0 @@ -#![allow(clippy::expect_used)] - -use originweave_core::{ - AgentTaskExtensionDecision, AgentTaskExtensionPolicy, BrowserSessionId, BrowsingContextId, - ExtensionAccessDecision, ExtensionAccessRequest, ExtensionAgentCapability, ExtensionId, - evaluate_agent_task_extension, evaluate_extension_access, -}; - -fn extension_id(value: &str) -> ExtensionId { - ExtensionId::parse(value).expect("valid extension identifier") -} - -fn session(value: u64) -> BrowserSessionId { - BrowserSessionId::new(value).expect("nonzero browser session") -} - -fn context(value: u64) -> BrowsingContextId { - BrowsingContextId::new(value).expect("nonzero browsing context") -} - -#[test] -fn empty_agent_task_extension_policy_denies_every_extension() { - let policy = AgentTaskExtensionPolicy::new([]); - let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); - - assert_eq!( - evaluate_agent_task_extension(&extension, &policy), - AgentTaskExtensionDecision::DenyNotManaged - ); -} - -#[test] -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()]); - - assert_eq!( - evaluate_agent_task_extension(&allowed, &policy), - AgentTaskExtensionDecision::AllowManagedExtension - ); - assert_eq!( - evaluate_agent_task_extension(&other, &policy), - AgentTaskExtensionDecision::DenyNotManaged - ); -} - -#[test] -fn managed_agent_task_extension_admission_does_not_mint_agent_capability() { - let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); - let policy = AgentTaskExtensionPolicy::new([extension.clone()]); - - assert_eq!( - evaluate_agent_task_extension(&extension, &policy), - AgentTaskExtensionDecision::AllowManagedExtension - ); - - let request = ExtensionAccessRequest::new( - extension, - session(31), - context(37), - ExtensionAgentCapability::ProposeTypedAction, - ); - assert_eq!( - evaluate_extension_access(&request, None), - ExtensionAccessDecision::DenyMissingGrant - ); -} From a0d767377a737b656ca8d095057b39be76b98a6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 21:45:50 +0900 Subject: [PATCH 05/16] docs(extension): record managed Agent Task admission --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 569bb0cf1..5b934daab 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 +- 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. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - 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. @@ -74,4 +75,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file From f4521bacf98f052f275fc87d0f75c108f6e096e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 06:24:51 +0900 Subject: [PATCH 06/16] test(extension): require managed policy freshness --- .../tests/agent_task_extension_policy.rs | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/crates/originweave-policy/tests/agent_task_extension_policy.rs b/crates/originweave-policy/tests/agent_task_extension_policy.rs index 8ea2f0de5..f82a6249a 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([]); + let policy = AgentTaskExtensionPolicy::new([], 10, 20); let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); assert_eq!( - evaluate_agent_task_extension(&extension, &policy), + evaluate_agent_task_extension(&extension, &policy, 15), AgentTaskExtensionDecision::DenyNotManaged ); } @@ -35,25 +35,75 @@ 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); 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); + + 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); + let empty = AgentTaskExtensionPolicy::new([extension.clone()], 20, 20); + + assert_eq!( + evaluate_agent_task_extension(&extension, &reversed, 15), + AgentTaskExtensionDecision::DenyInvalidPolicyWindow + ); + assert_eq!( + evaluate_agent_task_extension(&extension, &empty, 20), + AgentTaskExtensionDecision::DenyInvalidPolicyWindow + ); +} + +#[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); + + 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); assert_eq!( - evaluate_agent_task_extension(&extension, &policy), + evaluate_agent_task_extension(&extension, &policy, 15), AgentTaskExtensionDecision::AllowManagedExtension ); From 66dbc574518740b8c09a558f7b54e7f5629256fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 06:27:57 +0900 Subject: [PATCH 07/16] feat(extension): fail closed on stale managed policy --- crates/originweave-policy/src/lib.rs | 36 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 3dda94b02..53d89119a 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -26,6 +26,8 @@ use originweave_core::{ #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentTaskExtensionPolicy { managed_extensions: BTreeSet, + valid_from: u64, + valid_until: u64, } impl AgentTaskExtensionPolicy { @@ -33,13 +35,20 @@ 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. 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. #[must_use] - pub fn new(managed_extensions: I) -> Self + pub fn new(managed_extensions: I, valid_from: u64, valid_until: u64) -> Self where I: IntoIterator, { Self { managed_extensions: managed_extensions.into_iter().collect(), + valid_from, + valid_until, } } } @@ -51,19 +60,38 @@ pub enum AgentTaskExtensionDecision { AllowManagedExtension, /// The extension identity is absent from the managed allow-list. DenyNotManaged, + /// The configured policy validity window is empty or reversed. + DenyInvalidPolicyWindow, + /// 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`, 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 +/// [`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 { + return AgentTaskExtensionDecision::DenyInvalidPolicyWindow; + } + 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 { From 759d2f04d81dcf52bca29b88d860fb9aaeca56e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 07:16:58 +0900 Subject: [PATCH 08/16] docs(changelog): record extension policy freshness --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b934daab..d89bd3675 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. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. From b63fb4d36c7665275d48a34c9e182191d912190d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 02:09:25 +0900 Subject: [PATCH 09/16] test(extension): bound managed policy lifetime --- .../tests/agent_task_extension_policy.rs | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/crates/originweave-policy/tests/agent_task_extension_policy.rs b/crates/originweave-policy/tests/agent_task_extension_policy.rs index f82a6249a..f60ed0ccd 100644 --- a/crates/originweave-policy/tests/agent_task_extension_policy.rs +++ b/crates/originweave-policy/tests/agent_task_extension_policy.rs @@ -22,7 +22,7 @@ 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([], 10, 20, 10); let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); assert_eq!( @@ -35,7 +35,7 @@ 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([allowed.clone(), allowed.clone()], 10, 20, 10); assert_eq!( evaluate_agent_task_extension(&allowed, &policy, 10), @@ -50,7 +50,7 @@ fn managed_agent_task_extension_policy_allows_only_exact_identifiers() { #[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([extension.clone()], 10, 20, 10); assert_eq!( evaluate_agent_task_extension(&extension, &policy, 9), @@ -69,8 +69,9 @@ 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([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), @@ -80,12 +81,37 @@ fn invalid_managed_extension_policy_window_fails_closed_before_membership() { 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); + let policy = AgentTaskExtensionPolicy::new( + [extension.clone()], + u64::MAX - 1, + u64::MAX, + 1, + ); assert_eq!( evaluate_agent_task_extension(&extension, &policy, u64::MAX - 1), @@ -100,7 +126,7 @@ 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([extension.clone()], 10, 20, 10); assert_eq!( evaluate_agent_task_extension(&extension, &policy, 15), From 0962a34bd344292d7fa52753b9fdfacab8620943 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 02:11:48 +0900 Subject: [PATCH 10/16] style(extension): format policy lifetime regression --- .../tests/agent_task_extension_policy.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/originweave-policy/tests/agent_task_extension_policy.rs b/crates/originweave-policy/tests/agent_task_extension_policy.rs index f60ed0ccd..984fd68b8 100644 --- a/crates/originweave-policy/tests/agent_task_extension_policy.rs +++ b/crates/originweave-policy/tests/agent_task_extension_policy.rs @@ -106,12 +106,7 @@ 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([extension.clone()], u64::MAX - 1, u64::MAX, 1); assert_eq!( evaluate_agent_task_extension(&extension, &policy, u64::MAX - 1), From 9ea75cb0aba2b5273a4b5f35adb31776437ecc5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 02:16:05 +0900 Subject: [PATCH 11/16] feat(extension): bound managed policy lifetime --- crates/originweave-policy/src/lib.rs | 42 +++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 53d89119a..2d2d8f687 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -28,6 +28,7 @@ pub struct AgentTaskExtensionPolicy { managed_extensions: BTreeSet, valid_from: u64, valid_until: u64, + maximum_window: u64, } impl AgentTaskExtensionPolicy { @@ -35,13 +36,20 @@ 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. 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. + /// `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, valid_from: u64, valid_until: u64) -> Self + pub fn new( + managed_extensions: I, + valid_from: u64, + valid_until: u64, + maximum_window: u64, + ) -> Self where I: IntoIterator, { @@ -49,6 +57,7 @@ impl AgentTaskExtensionPolicy { managed_extensions: managed_extensions.into_iter().collect(), valid_from, valid_until, + maximum_window, } } } @@ -60,8 +69,10 @@ pub enum AgentTaskExtensionDecision { AllowManagedExtension, /// The extension identity is absent from the managed allow-list. DenyNotManaged, - /// The configured policy validity window is empty or reversed. + /// 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. @@ -72,20 +83,25 @@ pub enum AgentTaskExtensionDecision { /// /// This pure boundary answers only whether the exact canonical extension may be /// present in the caller's managed Agent Task profile 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 -/// [`originweave_core::ExtensionAgentGrant`] remain separate authorities. +/// `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 { + 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; } From d7d2ea4fae6321cee9e4860ed81762e3a0061671 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 07:03:39 +0900 Subject: [PATCH 12/16] chore(stack): refresh managed-extension dependency snapshot From d7d7ea299af3c1d65a150a74a9fc4b3390344b70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 07:17:04 +0900 Subject: [PATCH 13/16] fix(stack): preserve live managed-extension ancestry --- crates/originweave-core/src/lib.rs | 24 +++++++++++++++++++ .../tests/native_messaging_authority.rs | 5 ++++ 2 files changed, 29 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index de817f130..b8cce88af 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -1125,6 +1125,18 @@ impl NativeMessagingHostGrant { host_name, } } + + /// Return the extension identity granted native-messaging access. + #[must_use] + pub const fn extension_id(&self) -> &ExtensionId { + &self.extension_id + } + + /// Return the exact native-messaging host identity in this grant. + #[must_use] + pub const fn host_name(&self) -> &NativeMessagingHostName { + &self.host_name + } } /// One extension request to connect to an exact native-messaging host. @@ -1143,6 +1155,18 @@ impl NativeMessagingAccessRequest { host_name, } } + + /// Return the extension identity requesting native-messaging access. + #[must_use] + pub const fn extension_id(&self) -> &ExtensionId { + &self.extension_id + } + + /// Return the exact native-messaging host identity requested. + #[must_use] + pub const fn host_name(&self) -> &NativeMessagingHostName { + &self.host_name + } } /// Result of evaluating native-messaging access against one explicit host grant. diff --git a/crates/originweave-core/tests/native_messaging_authority.rs b/crates/originweave-core/tests/native_messaging_authority.rs index f0a830cda..5f4f611f1 100644 --- a/crates/originweave-core/tests/native_messaging_authority.rs +++ b/crates/originweave-core/tests/native_messaging_authority.rs @@ -54,7 +54,12 @@ fn native_messaging_requires_an_explicit_exact_extension_and_host_grant() { let other_host = host_name("com.contextualwisdom.other_host"); let grant = NativeMessagingHostGrant::new(allowed_extension.clone(), allowed_host.clone()); + assert_eq!(grant.extension_id(), &allowed_extension); + assert_eq!(grant.host_name(), &allowed_host); + let exact = NativeMessagingAccessRequest::new(allowed_extension.clone(), allowed_host.clone()); + assert_eq!(exact.extension_id(), &allowed_extension); + assert_eq!(exact.host_name(), &allowed_host); assert_eq!( evaluate_native_messaging_access(&exact, Some(&grant)), NativeMessagingAccessDecision::Allow From 0de1ba22c208b23ab1b4e5a7eb42785f5299431f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 20:18:14 +0900 Subject: [PATCH 14/16] docs: restore managed extension policy changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e2b3591..1093841d4 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 +- 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. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. From 891e8b7271d4e1cfcda615053efe54ddf4ce2627 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 20:21:39 +0900 Subject: [PATCH 15/16] docs: restore managed extension freshness changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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. From a3cfb7cc763841fa6953b459bd7c6f4047cd6c6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:38:36 +0900 Subject: [PATCH 16/16] test(policy): align extension request with current authority contract --- .../tests/agent_task_extension_policy.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/originweave-policy/tests/agent_task_extension_policy.rs b/crates/originweave-policy/tests/agent_task_extension_policy.rs index 8ea2f0de5..3549560ec 100644 --- a/crates/originweave-policy/tests/agent_task_extension_policy.rs +++ b/crates/originweave-policy/tests/agent_task_extension_policy.rs @@ -2,7 +2,7 @@ use originweave_core::{ BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest, - ExtensionAgentCapability, ExtensionId, evaluate_extension_access, + ExtensionAgentCapability, ExtensionId, Origin, evaluate_extension_access, }; use originweave_policy::{ AgentTaskExtensionDecision, AgentTaskExtensionPolicy, evaluate_agent_task_extension, @@ -20,6 +20,10 @@ fn context(value: u64) -> BrowsingContextId { BrowsingContextId::new(value).expect("nonzero browsing context") } +fn origin() -> Origin { + Origin::parse("https://app.example").expect("valid controlled origin") +} + #[test] fn empty_agent_task_extension_policy_denies_every_extension() { let policy = AgentTaskExtensionPolicy::new([]); @@ -61,6 +65,8 @@ fn managed_agent_task_extension_admission_does_not_mint_agent_capability() { extension, session(31), context(37), + origin(), + 100, ExtensionAgentCapability::ProposeTypedAction, ); assert_eq!(