From 2403344abf4b9ec8d36b58f48f3ab2abe591cff4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 13:28:53 +0900 Subject: [PATCH 1/5] test(policy): prove extension grants cannot widen mutation authority --- .../tests/extension_mutation_isolation.rs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 crates/originweave-policy/tests/extension_mutation_isolation.rs diff --git a/crates/originweave-policy/tests/extension_mutation_isolation.rs b/crates/originweave-policy/tests/extension_mutation_isolation.rs new file mode 100644 index 00000000..4a872ce4 --- /dev/null +++ b/crates/originweave-policy/tests/extension_mutation_isolation.rs @@ -0,0 +1,118 @@ +#![allow(clippy::expect_used)] + +use std::collections::BTreeSet; + +use originweave_core::{ + ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, BrowserSessionId, + BrowsingContextId, Capability, ExecutionPurpose, ExtensionAccessDecision, + ExtensionAccessRequest, ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, + InstructionSource, Origin, PolicyContext, RobotsDecision, SecretDelivery, SessionMode, + evaluate_extension_access, +}; +use originweave_policy::{Decision, DenialReason, evaluate}; + +const VALID_INTENT: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +fn extension_id() -> ExtensionId { + ExtensionId::parse("abcdefghijklmnopabcdefghijklmnop").expect("valid extension id") +} + +fn browser_session() -> BrowserSessionId { + BrowserSessionId::new(17).expect("nonzero browser session") +} + +fn browsing_context() -> BrowsingContextId { + BrowsingContextId::new(23).expect("nonzero browsing context") +} + +fn origin(value: &str) -> Origin { + Origin::parse(value).expect("valid test origin") +} + +fn intent() -> ActionIntentDigest { + ActionIntentDigest::parse(VALID_INTENT).expect("valid intent digest") +} + +fn action_proposal_grant() -> ExtensionAgentGrant { + ExtensionAgentGrant::new( + extension_id(), + browser_session(), + browsing_context(), + [ExtensionAgentCapability::ProposeTypedAction], + ) +} + +fn assert_extension_can_propose(grant: &ExtensionAgentGrant) { + let request = ExtensionAccessRequest::new( + extension_id(), + browser_session(), + browsing_context(), + ExtensionAgentCapability::ProposeTypedAction, + ); + assert_eq!( + evaluate_extension_access(&request, Some(grant)), + ExtensionAccessDecision::Allow + ); +} + +#[test] +fn explicit_extension_grant_cannot_authorize_cross_origin_mutation() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let source = origin("https://source.example"); + let target = origin("https://target.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::Submit]), + BTreeSet::from([source.clone(), target.clone()]), + BTreeSet::from([target.clone()]), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Submit, + source, + target, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::CrossOriginMutation) + ); +} + +#[test] +fn explicit_extension_grant_cannot_supply_missing_write_origin_authority() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let site = origin("https://app.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::Submit]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Submit, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::OriginNotWritable) + ); +} From fda8ece43f131db3f30431039079c7896e7c8479 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 13:35:22 +0900 Subject: [PATCH 2/5] test(policy): cover crawler and R5 extension isolation --- .../tests/extension_mutation_isolation.rs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/originweave-policy/tests/extension_mutation_isolation.rs b/crates/originweave-policy/tests/extension_mutation_isolation.rs index 4a872ce4..0b81b643 100644 --- a/crates/originweave-policy/tests/extension_mutation_isolation.rs +++ b/crates/originweave-policy/tests/extension_mutation_isolation.rs @@ -116,3 +116,63 @@ fn explicit_extension_grant_cannot_supply_missing_write_origin_authority() { Decision::Deny(DenialReason::OriginNotWritable) ); } + +#[test] +fn explicit_extension_grant_cannot_turn_crawler_mode_into_mutation_authority() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let site = origin("https://public.example"); + let context = PolicyContext::new( + SessionMode::Crawler, + ExecutionPurpose::PublicCrawl, + BTreeSet::from([Capability::Submit]), + BTreeSet::from([site.clone()]), + BTreeSet::from([site.clone()]), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Submit, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::CrawlerMutation) + ); +} + +#[test] +fn explicit_extension_grant_cannot_delegate_forbidden_r5_action() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let site = origin("https://consent.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::LegalConsent]), + BTreeSet::from([site.clone()]), + BTreeSet::from([site.clone()]), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::LegalConsent, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::ForbiddenRisk) + ); +} From 8133bc91a80105f132aa41b58c4fcff79a91ecb5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 13:48:57 +0900 Subject: [PATCH 3/5] test(policy): prove extension grants cannot control human mode --- .../tests/extension_mutation_isolation.rs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/originweave-policy/tests/extension_mutation_isolation.rs b/crates/originweave-policy/tests/extension_mutation_isolation.rs index 0b81b643..2499c8cf 100644 --- a/crates/originweave-policy/tests/extension_mutation_isolation.rs +++ b/crates/originweave-policy/tests/extension_mutation_isolation.rs @@ -176,3 +176,33 @@ fn explicit_extension_grant_cannot_delegate_forbidden_r5_action() { Decision::Deny(DenialReason::ForbiddenRisk) ); } + +#[test] +fn explicit_extension_grant_cannot_turn_human_mode_into_agent_control() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let site = origin("https://human.example"); + let context = PolicyContext::new( + SessionMode::Human, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::Navigate]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::NotApplicable, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Navigate, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::HumanModeNotAgentControlled) + ); +} From ac8b27ee69229070c382ca2199eaf9ec8b1b12db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 14:42:33 +0900 Subject: [PATCH 4/5] test(policy): bind extension proposals to crawl policy gates --- .../tests/extension_mutation_isolation.rs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/crates/originweave-policy/tests/extension_mutation_isolation.rs b/crates/originweave-policy/tests/extension_mutation_isolation.rs index 2499c8cf..6c85b983 100644 --- a/crates/originweave-policy/tests/extension_mutation_isolation.rs +++ b/crates/originweave-policy/tests/extension_mutation_isolation.rs @@ -147,6 +147,126 @@ fn explicit_extension_grant_cannot_turn_crawler_mode_into_mutation_authority() { ); } +#[test] +fn explicit_extension_grant_cannot_pair_agent_task_with_public_crawl_purpose() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let site = origin("https://public.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::PublicCrawl, + BTreeSet::from([Capability::Observe]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Observe, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::ModePurposeMismatch) + ); +} + +#[test] +fn explicit_extension_grant_cannot_bypass_disallowed_robots_policy() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let site = origin("https://public.example"); + let context = PolicyContext::new( + SessionMode::Crawler, + ExecutionPurpose::PublicCrawl, + BTreeSet::from([Capability::Observe]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::Disallowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Observe, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::RobotsDisallowed) + ); +} + +#[test] +fn explicit_extension_grant_cannot_bypass_unknown_robots_policy() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let site = origin("https://public.example"); + let context = PolicyContext::new( + SessionMode::Crawler, + ExecutionPurpose::PublicCrawl, + BTreeSet::from([Capability::Observe]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::Unknown, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Observe, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::RobotsUnknown) + ); +} + +#[test] +fn explicit_extension_grant_cannot_bypass_missing_robots_policy() { + let grant = action_proposal_grant(); + assert_extension_can_propose(&grant); + + let site = origin("https://public.example"); + let context = PolicyContext::new( + SessionMode::Crawler, + ExecutionPurpose::PublicCrawl, + BTreeSet::from([Capability::Observe]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::NotApplicable, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Observe, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::RobotsNotApplicable) + ); +} + #[test] fn explicit_extension_grant_cannot_delegate_forbidden_r5_action() { let grant = action_proposal_grant(); From 0d492564aa61c9094f1315ee4e234b46a1e63a6c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:14:52 +0900 Subject: [PATCH 5/5] test(policy): clarify independent extension and action boundaries --- .../tests/extension_mutation_isolation.rs | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/crates/originweave-policy/tests/extension_mutation_isolation.rs b/crates/originweave-policy/tests/extension_mutation_isolation.rs index 6c85b983..97d9b184 100644 --- a/crates/originweave-policy/tests/extension_mutation_isolation.rs +++ b/crates/originweave-policy/tests/extension_mutation_isolation.rs @@ -1,5 +1,12 @@ #![allow(clippy::expect_used)] +//! Keep extension proposal-grant evaluation separate from ordinary action policy. +//! +//! OriginWeave does not yet implement an adapter that converts an extension proposal into an +//! [`ActionRequest`]. These regressions therefore prove two independent fail-closed boundaries: +//! the exact extension/session/context grant permits only `ProposeTypedAction`, while an ordinary +//! user-sourced action request remains subject to the core policy decision shown in each test. + use std::collections::BTreeSet; use originweave_core::{ @@ -43,7 +50,7 @@ fn action_proposal_grant() -> ExtensionAgentGrant { ) } -fn assert_extension_can_propose(grant: &ExtensionAgentGrant) { +fn assert_proposal_grant_is_independently_allowed(grant: &ExtensionAgentGrant) { let request = ExtensionAccessRequest::new( extension_id(), browser_session(), @@ -57,9 +64,9 @@ fn assert_extension_can_propose(grant: &ExtensionAgentGrant) { } #[test] -fn explicit_extension_grant_cannot_authorize_cross_origin_mutation() { +fn extension_proposal_grant_is_independent_of_cross_origin_mutation_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let source = origin("https://source.example"); let target = origin("https://target.example"); @@ -88,9 +95,9 @@ fn explicit_extension_grant_cannot_authorize_cross_origin_mutation() { } #[test] -fn explicit_extension_grant_cannot_supply_missing_write_origin_authority() { +fn extension_proposal_grant_is_independent_of_write_origin_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let site = origin("https://app.example"); let context = PolicyContext::new( @@ -118,9 +125,9 @@ fn explicit_extension_grant_cannot_supply_missing_write_origin_authority() { } #[test] -fn explicit_extension_grant_cannot_turn_crawler_mode_into_mutation_authority() { +fn extension_proposal_grant_is_independent_of_crawler_mutation_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let site = origin("https://public.example"); let context = PolicyContext::new( @@ -148,9 +155,9 @@ fn explicit_extension_grant_cannot_turn_crawler_mode_into_mutation_authority() { } #[test] -fn explicit_extension_grant_cannot_pair_agent_task_with_public_crawl_purpose() { +fn extension_proposal_grant_is_independent_of_mode_purpose_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let site = origin("https://public.example"); let context = PolicyContext::new( @@ -178,9 +185,9 @@ fn explicit_extension_grant_cannot_pair_agent_task_with_public_crawl_purpose() { } #[test] -fn explicit_extension_grant_cannot_bypass_disallowed_robots_policy() { +fn extension_proposal_grant_is_independent_of_disallowed_robots_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let site = origin("https://public.example"); let context = PolicyContext::new( @@ -208,9 +215,9 @@ fn explicit_extension_grant_cannot_bypass_disallowed_robots_policy() { } #[test] -fn explicit_extension_grant_cannot_bypass_unknown_robots_policy() { +fn extension_proposal_grant_is_independent_of_unknown_robots_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let site = origin("https://public.example"); let context = PolicyContext::new( @@ -238,9 +245,9 @@ fn explicit_extension_grant_cannot_bypass_unknown_robots_policy() { } #[test] -fn explicit_extension_grant_cannot_bypass_missing_robots_policy() { +fn extension_proposal_grant_is_independent_of_missing_robots_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let site = origin("https://public.example"); let context = PolicyContext::new( @@ -268,9 +275,9 @@ fn explicit_extension_grant_cannot_bypass_missing_robots_policy() { } #[test] -fn explicit_extension_grant_cannot_delegate_forbidden_r5_action() { +fn extension_proposal_grant_is_independent_of_non_delegable_r5_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let site = origin("https://consent.example"); let context = PolicyContext::new( @@ -298,9 +305,9 @@ fn explicit_extension_grant_cannot_delegate_forbidden_r5_action() { } #[test] -fn explicit_extension_grant_cannot_turn_human_mode_into_agent_control() { +fn extension_proposal_grant_is_independent_of_human_mode_policy() { let grant = action_proposal_grant(); - assert_extension_can_propose(&grant); + assert_proposal_grant_is_independently_allowed(&grant); let site = origin("https://human.example"); let context = PolicyContext::new(