From 277df965602a97b1c221df2fc7a228ff5ac6c540 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:55:26 +0900 Subject: [PATCH 1/3] test(policy): prove extension grant cannot widen agent authority --- .../tests/extension_policy_isolation.rs | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 crates/originweave-policy/tests/extension_policy_isolation.rs diff --git a/crates/originweave-policy/tests/extension_policy_isolation.rs b/crates/originweave-policy/tests/extension_policy_isolation.rs new file mode 100644 index 000000000..cf65e0a2a --- /dev/null +++ b/crates/originweave-policy/tests/extension_policy_isolation.rs @@ -0,0 +1,148 @@ +#![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(7).expect("nonzero browser session") +} + +fn browsing_context() -> BrowsingContextId { + BrowsingContextId::new(11).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_only_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_does_not_widen_agent_origin_authority() { + let grant = action_proposal_grant(); + assert_extension_can_only_propose(&grant); + + let allowed = origin("https://app.example"); + let forbidden = origin("https://outside.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::Navigate]), + BTreeSet::from([allowed.clone()]), + BTreeSet::new(), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Navigate, + allowed, + forbidden, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::OriginNotReadable) + ); +} + +#[test] +fn explicit_extension_grant_does_not_supply_agent_action_capability() { + let grant = action_proposal_grant(); + assert_extension_can_only_propose(&grant); + + let site = origin("https://app.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::Observe]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Navigate, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::MissingCapability(Capability::Navigate)) + ); +} + +#[test] +fn untrusted_extension_content_cannot_become_a_policy_instruction() { + let grant = action_proposal_grant(); + assert_extension_can_only_propose(&grant); + + let site = origin("https://app.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::Navigate]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Navigate, + site.clone(), + site, + InstructionSource::WebContent, + SecretDelivery::None, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::UntrustedInstructionSource) + ); +} From a57873b3688984711918be17aadd348ed9fb12a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:05:55 +0900 Subject: [PATCH 2/3] test(policy): keep extension proposals outside secret authority --- .../tests/extension_policy_isolation.rs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/originweave-policy/tests/extension_policy_isolation.rs b/crates/originweave-policy/tests/extension_policy_isolation.rs index cf65e0a2a..79557e49b 100644 --- a/crates/originweave-policy/tests/extension_policy_isolation.rs +++ b/crates/originweave-policy/tests/extension_policy_isolation.rs @@ -146,3 +146,63 @@ fn untrusted_extension_content_cannot_become_a_policy_instruction() { Decision::Deny(DenialReason::UntrustedInstructionSource) ); } + +#[test] +fn explicit_extension_grant_cannot_turn_raw_secret_delivery_into_a_fill_capability() { + let grant = action_proposal_grant(); + assert_extension_can_only_propose(&grant); + + let site = origin("https://app.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::FillSecret]), + BTreeSet::from([site.clone()]), + BTreeSet::from([site.clone()]), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::FillSecret, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::RawValue, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::SecretBrokerRequired) + ); +} + +#[test] +fn explicit_extension_grant_cannot_attach_secret_material_to_non_secret_action() { + let grant = action_proposal_grant(); + assert_extension_can_only_propose(&grant); + + let site = origin("https://app.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::Navigate]), + BTreeSet::from([site.clone()]), + BTreeSet::new(), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let proposed = ActionRequest::new( + ActionKind::Navigate, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::RawValue, + intent(), + ); + + assert_eq!( + evaluate(&proposed, &context), + Decision::Deny(DenialReason::UnexpectedSecretMaterial) + ); +} From 3690bf0a351b77957071f5399e9a31cec5f39e0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 07:28:31 +0900 Subject: [PATCH 3/3] test(policy): align extension isolation grant scope --- .../originweave-policy/tests/extension_policy_isolation.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/originweave-policy/tests/extension_policy_isolation.rs b/crates/originweave-policy/tests/extension_policy_isolation.rs index 79557e49b..f32d8733c 100644 --- a/crates/originweave-policy/tests/extension_policy_isolation.rs +++ b/crates/originweave-policy/tests/extension_policy_isolation.rs @@ -13,6 +13,9 @@ use originweave_policy::{Decision, DenialReason, evaluate}; const VALID_INTENT: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const EXTENSION_ORIGIN: &str = "https://extension.example"; +const UNEXPIRED_NOW_EPOCH_SECONDS: u64 = 1_700_000_000; +const UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS: u64 = 1_700_000_600; fn extension_id() -> ExtensionId { ExtensionId::parse("abcdefghijklmnopabcdefghijklmnop").expect("valid extension id") @@ -39,6 +42,8 @@ fn action_proposal_grant() -> ExtensionAgentGrant { extension_id(), browser_session(), browsing_context(), + origin(EXTENSION_ORIGIN), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ExtensionAgentCapability::ProposeTypedAction], ) } @@ -48,6 +53,8 @@ fn assert_extension_can_only_propose(grant: &ExtensionAgentGrant) { extension_id(), browser_session(), browsing_context(), + origin(EXTENSION_ORIGIN), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ProposeTypedAction, ); assert_eq!(